From 11622fed5494b40c911cc808059749321aa3eb45 Mon Sep 17 00:00:00 2001 From: user Date: Sat, 18 Jul 2026 21:16:55 +0400 Subject: [PATCH] feat(core): add Result type with tests and build setup --- Makefile | 34 +++++ core/include/result.hpp | 226 +++++++++++++++++++++++++++++++++ tests/app/core/result_test.cpp | 114 +++++++++++++++++ 3 files changed, 374 insertions(+) create mode 100644 Makefile create mode 100644 core/include/result.hpp create mode 100644 tests/app/core/result_test.cpp diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a511f1a --- /dev/null +++ b/Makefile @@ -0,0 +1,34 @@ +CXX := gcc + +CXXFLAGS := -std=c++17 \ + -Wall \ + -Wextra \ + -Werror \ + -Icore/include \ + -Ilogger/include + +LDFLAGS := -lstdc++ + +BUILD_DIR := build + +TEST_BIN := $(BUILD_DIR)/result_test + +RESULT_TEST_SRC := tests/app/core/result_test.cpp + + +.PHONY: all test clean + +all: test + + +test: $(TEST_BIN) + ./$(TEST_BIN) + + +$(TEST_BIN): $(RESULT_TEST_SRC) + @mkdir -p $(BUILD_DIR) + $(CXX) $(CXXFLAGS) $< -o $@ $(LDFLAGS) + + +clean: + rm -rf $(BUILD_DIR) diff --git a/core/include/result.hpp b/core/include/result.hpp new file mode 100644 index 0000000..eb752e7 --- /dev/null +++ b/core/include/result.hpp @@ -0,0 +1,226 @@ +#pragma once + +#include +#include +#include + +namespace core { + +/** + * @brief Контейнер результата выполнения операции. + * + * BasicResult хранит одно из двух состояний: + * - успешный результат типа T; + * - ошибку типа E. + * + * Создание объекта возможно только через фабричные методы: + * Success() и Failure(), что делает намерение вызова явным. + * + * @tparam T Тип успешного результата. + * @tparam E Тип ошибки. + * + * @note Копирование запрещено. + * Объект поддерживает только перемещение. + */ +template class [[nodiscard]] BasicResult { +public: + /** + * @brief Создает успешный результат. + * + * @param value Значение успешного результата. + * + * @return Объект BasicResult в состоянии успеха. + */ + static BasicResult Success(T value) { + return BasicResult(SuccessTag{}, std::move(value)); + } + + /** + * @brief Создает результат с ошибкой. + * + * @param error Значение ошибки. + * + * @return Объект BasicResult в состоянии ошибки. + */ + static BasicResult Failure(E error) { + return BasicResult(FailureTag{}, std::move(error)); + } + + /** + * @brief Запрещает копирование. + */ + BasicResult(const BasicResult &) = delete; + + /** + * @brief Запрещает копирующее присваивание. + */ + BasicResult &operator=(const BasicResult &) = delete; + + /** + * @brief Разрешает перемещение. + */ + BasicResult(BasicResult &&) noexcept = default; + + /** + * @brief Разрешает перемещающее присваивание. + */ + BasicResult &operator=(BasicResult &&) noexcept = default; + + /** + * @brief Проверяет успешность результата. + * + * @return true, если результат содержит значение T. + * @return false, если результат содержит ошибку E. + */ + [[nodiscard]] + bool isSuccess() const { + return data_.index() == 0; + } + + /** + * @brief Проверяет наличие ошибки в результате. + * + * @return true, если результат содержит ошибку E. + * @return false, если результат содержит значение T. + */ + [[nodiscard]] + bool isFailure() const { + return data_.index() == 1; + } + + /** + * @brief Возвращает значение успешного результата. + * + * @return Константная ссылка на значение типа T. + * + * @warning Метод должен вызываться только после проверки + * isSuccess(). + * + * @throws std::bad_variant_access + * если объект содержит ошибку. + */ + [[nodiscard]] + const T &getValue() const { + return std::get<0>(data_); + } + + /** + * @brief Возвращает изменяемое значение успешного результата. + * + * @return Ссылка на значение типа T. + * + * @warning Метод должен вызываться только после проверки + * isSuccess(). + */ + [[nodiscard]] + T &getValue() { + return std::get<0>(data_); + } + + /** + * @brief Возвращает ошибку результата. + * + * @return Константная ссылка на ошибку типа E. + * + * @warning Метод должен вызываться только если isSuccess() + * возвращает false. + * + * @throws std::bad_variant_access + * если объект содержит успешный результат. + */ + [[nodiscard]] + const E &getError() const { + return std::get<1>(data_); + } + + /** + * @brief Возвращает изменяемую ошибку результата. + * + * @return Ссылка на ошибку типа E. + * + * @warning Метод должен вызываться только если isSuccess() + * возвращает false. + */ + [[nodiscard]] + E &getError() { + return std::get<1>(data_); + } + +private: + /** + * @brief Вспомогательный тип для выбора конструктора успеха. + */ + struct SuccessTag {}; + + /** + * @brief Вспомогательный тип для выбора конструктора ошибки. + */ + struct FailureTag {}; + + /** + * @brief Внутренний конструктор успешного результата. + * + * Доступен только через Success(). + */ + explicit BasicResult(SuccessTag, T value) + : data_(std::in_place_index<0>, std::move(value)) {} + /** + * @brief Внутренний конструктор результата с ошибкой. + * + * Доступен только через Failure(). + */ + explicit BasicResult(FailureTag, E error) + : data_(std::in_place_index<1>, std::move(error)) {} + +private: + /** + * @brief Хранилище результата. + * + * Первый вариант — успешное значение T. + * Второй вариант — ошибка E. + */ + std::variant data_; +}; + +/** + * @brief Результат операции с текстовой ошибкой. + * + * Упрощенный вариант BasicResult, где ошибка всегда представлена + * строкой std::string. + * + * Пример: + * + * @code + * core::Result load() + * { + * return core::Result::Success(100); + * } + * + * core::Result parse() + * { + * return core::Result::Failure("Invalid format"); + * } + * @endcode + * + * @tparam T Тип успешного результата. + */ +template using Result = BasicResult; + +/** + * @brief Статус выполнения операции. + * + * Используется для операций, где важно только состояние: + * успех или сообщение об ошибке. + * + * Пример: + * + * @code + * core::Status save() + * { + * return core::Status::Success(true); + * } + * @endcode + */ +using Status = Result; + +} // namespace core diff --git a/tests/app/core/result_test.cpp b/tests/app/core/result_test.cpp new file mode 100644 index 0000000..dad0e13 --- /dev/null +++ b/tests/app/core/result_test.cpp @@ -0,0 +1,114 @@ +#include +#include +#include + +#include "result.hpp" + +namespace { + +void testSuccessCreation() { + auto result = core::Result::Success(42); + + assert(result.isSuccess()); + assert(result.getValue() == 42); +} + +void testFailureCreation() { + auto result = core::Result::Failure("Invalid value"); + + assert(result.isFailure()); + assert(result.getError() == "Invalid value"); +} + +void testStatusSuccess() { + auto status = core::Status::Success(true); + + assert(status.isSuccess()); + assert(status.getValue()); +} + +void testStatusFailure() { + auto status = core::Status::Failure("File not found"); + + assert(status.isFailure()); + assert(status.getError() == "File not found"); +} + +void testMoveConstruction() { + auto source = core::Result::Success("hello"); + + auto moved = std::move(source); + + assert(moved.isSuccess()); + assert(moved.getValue() == "hello"); +} + +void testMoveAssignment() { + auto first = core::Result::Success(10); + auto second = core::Result::Failure("error"); + + second = std::move(first); + + assert(second.isSuccess()); + assert(second.getValue() == 10); +} + +void testDifferentTypes() { + struct User { + int id; + std::string name; + }; + + auto result = core::Result::Success(User{1, "Alex"}); + + assert(result.isSuccess()); + assert(result.getValue().id == 1); + assert(result.getValue().name == "Alex"); +} + +void testValueModification() { + auto result = core::Result::Success(10); + + result.getValue() = 20; + + assert(result.getValue() == 20); +} + +void testErrorModification() { + auto result = core::Result::Failure("old error"); + + result.getError() = "new error"; + + assert(result.getError() == "new error"); +} + +void testBasicResultWithCustomError() { + enum class ErrorCode { NotFound, PermissionDenied }; + + using CustomResult = core::BasicResult; + + auto result = CustomResult::Failure(ErrorCode::PermissionDenied); + + assert(result.isFailure()); + assert(result.getError() == ErrorCode::PermissionDenied); +} + +void runAllTests() { + testSuccessCreation(); + testFailureCreation(); + testStatusSuccess(); + testStatusFailure(); + testMoveConstruction(); + testMoveAssignment(); + testDifferentTypes(); + testValueModification(); + testErrorModification(); + testBasicResultWithCustomError(); +} + +} // namespace + +int main() { + runAllTests(); + return 0; +}