feat(core): add Result type with tests and build setup
This commit is contained in:
@@ -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)
|
||||
@@ -0,0 +1,226 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief Контейнер результата выполнения операции.
|
||||
*
|
||||
* BasicResult хранит одно из двух состояний:
|
||||
* - успешный результат типа T;
|
||||
* - ошибку типа E.
|
||||
*
|
||||
* Создание объекта возможно только через фабричные методы:
|
||||
* Success() и Failure(), что делает намерение вызова явным.
|
||||
*
|
||||
* @tparam T Тип успешного результата.
|
||||
* @tparam E Тип ошибки.
|
||||
*
|
||||
* @note Копирование запрещено.
|
||||
* Объект поддерживает только перемещение.
|
||||
*/
|
||||
template <typename T, typename E> 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<T, E> data_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Результат операции с текстовой ошибкой.
|
||||
*
|
||||
* Упрощенный вариант BasicResult, где ошибка всегда представлена
|
||||
* строкой std::string.
|
||||
*
|
||||
* Пример:
|
||||
*
|
||||
* @code
|
||||
* core::Result<int> load()
|
||||
* {
|
||||
* return core::Result<int>::Success(100);
|
||||
* }
|
||||
*
|
||||
* core::Result<int> parse()
|
||||
* {
|
||||
* return core::Result<int>::Failure("Invalid format");
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* @tparam T Тип успешного результата.
|
||||
*/
|
||||
template <typename T> using Result = BasicResult<T, std::string>;
|
||||
|
||||
/**
|
||||
* @brief Статус выполнения операции.
|
||||
*
|
||||
* Используется для операций, где важно только состояние:
|
||||
* успех или сообщение об ошибке.
|
||||
*
|
||||
* Пример:
|
||||
*
|
||||
* @code
|
||||
* core::Status save()
|
||||
* {
|
||||
* return core::Status::Success(true);
|
||||
* }
|
||||
* @endcode
|
||||
*/
|
||||
using Status = Result<bool>;
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,114 @@
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "result.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
void testSuccessCreation() {
|
||||
auto result = core::Result<int>::Success(42);
|
||||
|
||||
assert(result.isSuccess());
|
||||
assert(result.getValue() == 42);
|
||||
}
|
||||
|
||||
void testFailureCreation() {
|
||||
auto result = core::Result<int>::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<std::string>::Success("hello");
|
||||
|
||||
auto moved = std::move(source);
|
||||
|
||||
assert(moved.isSuccess());
|
||||
assert(moved.getValue() == "hello");
|
||||
}
|
||||
|
||||
void testMoveAssignment() {
|
||||
auto first = core::Result<int>::Success(10);
|
||||
auto second = core::Result<int>::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<User>::Success(User{1, "Alex"});
|
||||
|
||||
assert(result.isSuccess());
|
||||
assert(result.getValue().id == 1);
|
||||
assert(result.getValue().name == "Alex");
|
||||
}
|
||||
|
||||
void testValueModification() {
|
||||
auto result = core::Result<int>::Success(10);
|
||||
|
||||
result.getValue() = 20;
|
||||
|
||||
assert(result.getValue() == 20);
|
||||
}
|
||||
|
||||
void testErrorModification() {
|
||||
auto result = core::Result<int>::Failure("old error");
|
||||
|
||||
result.getError() = "new error";
|
||||
|
||||
assert(result.getError() == "new error");
|
||||
}
|
||||
|
||||
void testBasicResultWithCustomError() {
|
||||
enum class ErrorCode { NotFound, PermissionDenied };
|
||||
|
||||
using CustomResult = core::BasicResult<int, ErrorCode>;
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user