diff --git a/error/v1/CMakeLists.txt b/error/v1/CMakeLists.txt new file mode 100644 index 0000000..4c07bd1 --- /dev/null +++ b/error/v1/CMakeLists.txt @@ -0,0 +1,80 @@ +cmake_minimum_required(VERSION 3.18) + +project(error LANGUAGES CXX) + +# Import third-party dependencies. +find_package(Boost REQUIRED CONFIG COMPONENTS headers) +find_package(fmt REQUIRED CONFIG) +find_package(GTest REQUIRED CONFIG) + +# Enable some compiler warnings (supported by gcc & clang). +set(warnings + -Wall + -Wconversion + -Wextra + -Wformat=2 + -Wold-style-cast + -Woverloaded-virtual + -Wshadow + -Wsign-conversion + -Wuninitialized + -Wunused + ) +string(REPLACE ";" " " warnings "${warnings}") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${warnings}") + +add_library(error SHARED) +add_library(caesar::error ALIAS error) + +# Require C++17. +target_compile_features(error PUBLIC cxx_std_17) + +# Add sources. +set(sources + caesar/error/detail/no_value_policy.cpp + caesar/error/domain_error.cpp + caesar/error/error_category.cpp + caesar/error/error_code.cpp + caesar/error/error.cpp + caesar/error/expected.cpp + caesar/error/out_of_range.cpp + caesar/error/runtime_error.cpp + ) +target_sources(error PRIVATE ${sources}) + +# Add include dirs. +target_include_directories( + error + PUBLIC + $ + ) + +# Link to imported targets. +target_link_libraries( + error + PUBLIC + Boost::headers + PRIVATE + fmt::fmt + ) + +# Add test sources. +set(tests + test/error_code_test.cpp + test/error_test.cpp + test/expected_test.cpp + ) + +add_executable(error-test ${tests}) + +target_link_libraries( + error-test + PRIVATE + caesar::error + GTest::gmock_main + ) + +# Register tests with CTest. +enable_testing() +include(GoogleTest) +gtest_discover_tests(error-test) diff --git a/error/v1/caesar/error.hpp b/error/v1/caesar/error.hpp new file mode 100644 index 0000000..c639cab --- /dev/null +++ b/error/v1/caesar/error.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include "error/domain_error.hpp" +#include "error/error_category.hpp" +#include "error/error_code.hpp" +#include "error/error.hpp" +#include "error/expected.hpp" +#include "error/out_of_range.hpp" +#include "error/runtime_error.hpp" diff --git a/error/v1/caesar/error/detail/no_value_policy.cpp b/error/v1/caesar/error/detail/no_value_policy.cpp new file mode 100644 index 0000000..3959e8e --- /dev/null +++ b/error/v1/caesar/error/detail/no_value_policy.cpp @@ -0,0 +1 @@ +#include "no_value_policy.hpp" diff --git a/error/v1/caesar/error/detail/no_value_policy.hpp b/error/v1/caesar/error/detail/no_value_policy.hpp new file mode 100644 index 0000000..92a196a --- /dev/null +++ b/error/v1/caesar/error/detail/no_value_policy.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include "../error.hpp" +#include "../runtime_error.hpp" + +#include + +namespace caesar::detail { + +namespace outcome = BOOST_OUTCOME_V2_NAMESPACE; + +/** + * \private + * A mixin type that defines how invalid access to Expected object members is + * handled + */ +struct NoValuePolicy : public outcome::policy::base { + /** \private Called on each access to Expected::value */ + template + static constexpr void + wide_value_check(Expected&& self) + { + if (not base::_has_value(self)) { + base::_error(std::forward(self)).throw_exception(); + } + } + + /** \private Called on each access to Expected::error */ + template + static constexpr void + wide_error_check(Expected&& self) + { + if (not base::_has_error(std::forward(self))) { + Error(RuntimeError::BadExpectedAccess).throw_exception(); + } + } +}; + +} // namespace caesar::detail diff --git a/error/v1/caesar/error/domain_error.cpp b/error/v1/caesar/error/domain_error.cpp new file mode 100644 index 0000000..ba9632b --- /dev/null +++ b/error/v1/caesar/error/domain_error.cpp @@ -0,0 +1,27 @@ +#include "domain_error.hpp" + +#include "error.hpp" + +#include + +namespace caesar { + +const char* +DomainErrorCategory::description(int value) const noexcept +{ + const auto e = static_cast(value); + + switch (e) { + case DomainError::DivisionByZero: return "Division by zero"; + } + + return ""; +} + +void +DomainErrorCategory::throw_exception(const Error& error) const +{ + throw std::domain_error(error.message()); +} + +} // namespace caesar diff --git a/error/v1/caesar/error/domain_error.hpp b/error/v1/caesar/error/domain_error.hpp new file mode 100644 index 0000000..305cc3f --- /dev/null +++ b/error/v1/caesar/error/domain_error.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include "error_category.hpp" +#include "error_code.hpp" + +namespace caesar { + +/** + * Error code used to indicate domain errors, i.e. situations where the inputs + * are outside of the domain on which an operation is defined. + * + * \see DomainErrorCategory + * \see ErrorCode + */ +enum class DomainError { + DivisionByZero = 1, +}; + +/** + * Error category associated with domain errors + * + * \see DomainError + * \see ErrorCategory + */ +class DomainErrorCategory : public ErrorCategory { + using Base = ErrorCategory; + +public: + /** The associated error code enumeration type */ + using ErrorCodeEnum = DomainError; + + /** \copydoc ErrorCategory::name() */ + const char* + name() const noexcept override + { + return "DomainError"; + } + + /** \copydoc ErrorCategory::description(int) */ + const char* + description(int value) const noexcept override; + + /** \copydoc ErrorCategory::throw_exception(const Error&) */ + [[noreturn]] void + throw_exception(const Error& error) const override; +}; + +/** Singleton instance of the class */ +static constexpr DomainErrorCategory domain_error_category; + +/** \private Enables implicit construction of ErrorCode from DomainError */ +template<> +struct IsErrorCodeEnum : public std::true_type {}; + +/** \private Enables implicit construction of ErrorCode from DomainError */ +constexpr ErrorCode +make_error_code(DomainError e) noexcept +{ + return {static_cast(e), &domain_error_category}; +} + +} // namespace caesar diff --git a/error/v1/caesar/error/error.cpp b/error/v1/caesar/error/error.cpp new file mode 100644 index 0000000..f66e66d --- /dev/null +++ b/error/v1/caesar/error/error.cpp @@ -0,0 +1,21 @@ +#include "error.hpp" + +#include + +namespace caesar { + +std::string +Error::message() const +{ + return fmt::format("{}:{}: {}: {}", file(), line(), + error_code().category()->name(), + error_code().description()); +} + +void +Error::throw_exception() const +{ + error_code().category()->throw_exception(*this); +} + +} // namespace caesar diff --git a/error/v1/caesar/error/error.hpp b/error/v1/caesar/error/error.hpp new file mode 100644 index 0000000..8fa0293 --- /dev/null +++ b/error/v1/caesar/error/error.hpp @@ -0,0 +1,85 @@ +#pragma once + +#include "error_code.hpp" + +#include +#include + +namespace caesar { + +/** + * Encapsulates an error code as well as contextual information about its point + * of origin + * + * \see ErrorCategory + * \see ErrorCode + */ +class Error { + using source_location = std::experimental::source_location; + +public: + /** + * Construct a new Error object. + * + * \param[in] error_code error code + * \param[in] file source file where the error originated + * \param[in] line source line where the error originated + */ + constexpr Error(const ErrorCode& error_code, + const char* file, + int line) noexcept + : error_code_(error_code), file_(file), line_(line) + {} + + /** + * Construct a new Error object. + * + * \param[in] error_code error code + * \param[in] origin source location where the error originated + */ + explicit constexpr Error( + const ErrorCode& error_code, + const source_location& origin = source_location::current()) noexcept + : Error(error_code, origin.file_name(), static_cast(origin.line())) + {} + + /** Return the error code. */ + constexpr const ErrorCode& + error_code() const noexcept + { + return error_code_; + } + + /** Return the source file where the error originated. */ + constexpr const char* + file() const noexcept + { + return file_; + } + + /** Return the source line where the error originated. */ + constexpr int + line() const noexcept + { + return line_; + } + + /** Return an explanatory message. */ + std::string + message() const; + + /** Throw an exception object corresponding to the error. */ + void + throw_exception() const; + + // XXX boost/outcome requires error types to be default-constructible :( + // this constraint is removed in boost v1.76 + Error() = default; + +private: + ErrorCode error_code_; + const char* file_; + int line_; +}; + +} // namespace caesar diff --git a/error/v1/caesar/error/error_category.cpp b/error/v1/caesar/error/error_category.cpp new file mode 100644 index 0000000..c01f67a --- /dev/null +++ b/error/v1/caesar/error/error_category.cpp @@ -0,0 +1 @@ +#include "error_category.hpp" diff --git a/error/v1/caesar/error/error_category.hpp b/error/v1/caesar/error/error_category.hpp new file mode 100644 index 0000000..39c0aec --- /dev/null +++ b/error/v1/caesar/error/error_category.hpp @@ -0,0 +1,73 @@ +#pragma once + +#include + +namespace caesar { + +class Error; + +/** + * Base class for specific error category types + * + * Each derived class defines an interface between a unique error code + * enumeration type and the type-erased ErrorCode class. + * + * The objects of derived error category classes are treated as singletons - + * they are passed by pointer and compared based on their address in memory. + * + * \see Error + * \see ErrorCode + */ +class ErrorCategory { +public: + /** Get a string representation of the error category name. */ + virtual const char* + name() const noexcept = 0; + + /** Return a message describing the error code with the specified value. */ + virtual const char* + description(int value) const noexcept = 0; + + /** Throw an exception object corresponding to the specified error. */ + virtual void + throw_exception(const Error& error) const = 0; + + /** + * Compare two ErrorCategory objects. + * + * ErrorCategory objects are assumed to be singletons and are considered + * equal only if they are the same object. + */ + friend constexpr bool + operator==(const ErrorCategory& lhs, const ErrorCategory& rhs) noexcept + { + return &lhs == &rhs; + } + + /** \copydoc operator==(const ErrorCategory&, const ErrorCategory&) */ + friend constexpr bool + operator!=(const ErrorCategory& lhs, const ErrorCategory& rhs) noexcept + { + return not(lhs == rhs); + } + +protected: + ~ErrorCategory() = default; +}; + +/** + * Checks whether T is an error code enumeration type. + * + * The template may be specialized for additional types to indicate that the + * type may be converted to ErrorCode. + * + * \see ErrorCode + */ +template +struct IsErrorCodeEnum : public std::false_type {}; + +/** Helper variable template for IsErrorCodeEnum */ +template +inline constexpr bool is_error_code_enum_v = IsErrorCodeEnum::value; + +} // namespace caesar diff --git a/error/v1/caesar/error/error_code.cpp b/error/v1/caesar/error/error_code.cpp new file mode 100644 index 0000000..c645fc2 --- /dev/null +++ b/error/v1/caesar/error/error_code.cpp @@ -0,0 +1 @@ +#include "error_code.hpp" diff --git a/error/v1/caesar/error/error_code.hpp b/error/v1/caesar/error/error_code.hpp new file mode 100644 index 0000000..14a240f --- /dev/null +++ b/error/v1/caesar/error/error_code.hpp @@ -0,0 +1,106 @@ +#pragma once + +#include "error_category.hpp" + +namespace caesar { + +/** + * A type-erased error code + * + * ErrorCode facilitates interoperability between any number of domain-specific + * error codes that an application and its third-party dependencies may + * generate. It does so by providing a common type that can be used to represent + * error code enumerations of different types. + * + * ErrorCode stores an integer value and a pointer to an object of type + * ErrorCategory. The ErrorCategory class can be specialized to add support for + * additional error code enumeration types. + * + * \see Error + * \see ErrorCategory + */ +class ErrorCode { +public: + /** + * Construct a new ErrorCode object. + * + * \param[in] value error code value + * \param[in] category associated error category + */ + constexpr ErrorCode(int value, const ErrorCategory* category) noexcept + : value_(value), category_(category) + {} + + /** + * Construct a new ErrorCode object from an error code enumeration object + * as though by `ErrorCode(make_error_code(e))`. + * + * This overload participates in overload resolution only if + * `IsErrorCodeEnum::value == true`. + */ + template>> + constexpr ErrorCode(ErrorCodeEnum e) : ErrorCode(make_error_code(e)) + {} + + /** + * Assign the ErrorCode value from an error code enumeration object as + * though by `*this = make_error_code(e)`. + * + * This overload participates in overload resolution only if + * `IsErrorCodeEnum::value == true`. + */ + template>> + constexpr ErrorCode& + operator=(ErrorCodeEnum e) + { + return *this = make_error_code(e); + } + + /** Return the error code value. */ + constexpr int + value() const noexcept + { + return value_; + } + + /** Return a pointer to the associated error category. */ + constexpr const ErrorCategory* + category() const noexcept + { + return category_; + } + + /** Return a message describing the error code. */ + const char* + description() const noexcept + { + return category()->description(value()); + } + + /** Compare two ErrorCode objects */ + friend constexpr bool + operator==(const ErrorCode& lhs, const ErrorCode& rhs) noexcept + { + return lhs.value() == rhs.value() and + *lhs.category() == *rhs.category(); + } + + /** \copydoc operator==(const ErrorCode&, const ErrorCode&) */ + friend constexpr bool + operator!=(const ErrorCode& lhs, const ErrorCode& rhs) noexcept + { + return not(lhs == rhs); + } + + // XXX boost/outcome requires error types to be default-constructible :( + // this constraint is removed in boost v1.76 + ErrorCode() = default; + +private: + int value_; + const ErrorCategory* category_; +}; + +} // namespace caesar diff --git a/error/v1/caesar/error/expected.cpp b/error/v1/caesar/error/expected.cpp new file mode 100644 index 0000000..869d274 --- /dev/null +++ b/error/v1/caesar/error/expected.cpp @@ -0,0 +1 @@ +#include "expected.hpp" diff --git a/error/v1/caesar/error/expected.hpp b/error/v1/caesar/error/expected.hpp new file mode 100644 index 0000000..3f9c0f6 --- /dev/null +++ b/error/v1/caesar/error/expected.hpp @@ -0,0 +1,103 @@ +#pragma once + +#include "detail/no_value_policy.hpp" +#include "error.hpp" + +#include +#include + +namespace caesar { + +namespace outcome = BOOST_OUTCOME_V2_NAMESPACE; + +/** + * A wrapper that may contain an object of type T or an Error + * + * Expected is intended to be used as a return type for operations that may + * fail. On success, the return value contains the expected result. In case of + * failure, it instead contains an object that describes the error encountered. + * + * Attempting to access the underlying value (or error) if it was not contained + * causes an exception to be thrown. + * + * When an Expected object is contextually converted to bool, the conversion + * returns `true` if the object contains a value and `false` if the object + * contains an error. + * + * \see Error + */ +template +class Expected : public outcome::basic_result { + using Base = outcome::basic_result; + +public: + using Base::Base; + + /** Access the contained value, if present. */ + constexpr T* + operator->() + { + return &Base::value(); + } + + /** \copydoc Expected::operator->() */ + constexpr const T* + operator->() const + { + return &Base::value(); + } + + /** Access the contained value, if present. */ + constexpr T& + operator*() + { + return Base::value(); + } + + /** \copydoc Expected::operator*() */ + constexpr const T& + operator*() const + { + return Base::value(); + } + +#if 0 + // Declare these members for the purpose of documenting them - they actually + // exist in a derived-from class. + + /** */ + template + constexpr Expected(U&& value); + + constexpr Expected(const Error& error); + + /** \copydoc Expected::has_value() */ + explicit constexpr operator bool() const noexcept; + + /** Checks whether the object contains a value. */ + constexpr bool + has_value() const noexcept; + + /** Checks whether the object contains an error. */ + constexpr bool + has_error() const noexcept; + + /** Access the contained value, if present. */ + constexpr T& + value(); + + /** \copydoc Expected::value() */ + constexpr const T& + value() const; + + /** Access the contained error, if present. */ + constexpr Error& + error(); + + /** \copydoc Expected::error() */ + constexpr const Error& + error() const; +#endif +}; + +} // namespace caesar diff --git a/error/v1/caesar/error/out_of_range.cpp b/error/v1/caesar/error/out_of_range.cpp new file mode 100644 index 0000000..25a8332 --- /dev/null +++ b/error/v1/caesar/error/out_of_range.cpp @@ -0,0 +1,27 @@ +#include "out_of_range.hpp" + +#include "error.hpp" + +#include + +namespace caesar { + +const char* +OutOfRangeCategory::description(int value) const noexcept +{ + const auto e = static_cast(value); + + switch (e) { + case OutOfRange::OutOfBoundsAccess: return "Out of bounds access attempted"; + } + + return ""; +} + +void +OutOfRangeCategory::throw_exception(const Error& error) const +{ + throw std::out_of_range(error.message()); +} + +} // namespace caesar diff --git a/error/v1/caesar/error/out_of_range.hpp b/error/v1/caesar/error/out_of_range.hpp new file mode 100644 index 0000000..131d51e --- /dev/null +++ b/error/v1/caesar/error/out_of_range.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include "error_category.hpp" +#include "error_code.hpp" + +namespace caesar { + +/** + * Error code used to indicate errors that are consequence of attempt to access + * elements out of defined range. + * + * \see OutOfRangeCategory + * \see ErrorCode + */ +enum class OutOfRange { + OutOfBoundsAccess = 1, +}; + +/** + * Error category associated with out-of-range errors + * + * \see OutOfRange + * \see ErrorCategory + */ +class OutOfRangeCategory : public ErrorCategory { + using Base = ErrorCategory; + +public: + /** The associated error code enumeration type */ + using ErrorCodeEnum = OutOfRange; + + /** \copydoc ErrorCategory::name() */ + const char* + name() const noexcept override + { + return "OutOfRange"; + } + + /** \copydoc ErrorCategory::description(int) */ + const char* + description(int value) const noexcept override; + + /** \copydoc ErrorCategory::throw_exception(const Error&) */ + [[noreturn]] void + throw_exception(const Error& error) const override; +}; + +/** Singleton instance of the class */ +static constexpr OutOfRangeCategory out_of_range_category; + +/** \private Enables implicit construction of ErrorCode from OutOfRange */ +template<> +struct IsErrorCodeEnum : public std::true_type {}; + +/** \private Enables implicit construction of ErrorCode from OutOfRange */ +constexpr ErrorCode +make_error_code(OutOfRange e) noexcept +{ + return {static_cast(e), &out_of_range_category}; +} + +} // namespace caesar diff --git a/error/v1/caesar/error/runtime_error.cpp b/error/v1/caesar/error/runtime_error.cpp new file mode 100644 index 0000000..dc19490 --- /dev/null +++ b/error/v1/caesar/error/runtime_error.cpp @@ -0,0 +1,28 @@ +#include "runtime_error.hpp" + +#include "error.hpp" + +#include + +namespace caesar { + +const char* +RuntimeErrorCategory::description(int value) const noexcept +{ + const auto e = static_cast(value); + + switch (e) { + case RuntimeError::BadExpectedAccess: + return "Invalid access to Expected::error"; + } + + return ""; +} + +void +RuntimeErrorCategory::throw_exception(const Error& error) const +{ + throw std::runtime_error(error.message()); +} + +} // namespace caesar diff --git a/error/v1/caesar/error/runtime_error.hpp b/error/v1/caesar/error/runtime_error.hpp new file mode 100644 index 0000000..7a02466 --- /dev/null +++ b/error/v1/caesar/error/runtime_error.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include "error_category.hpp" +#include "error_code.hpp" + +namespace caesar { + +/** + * Error code used to indicate + * + * \see RuntimeErrorCategory + * \see ErrorCode + */ +enum class RuntimeError { + BadExpectedAccess = 1, +}; + +/** + * Error category associated with runtime errors + * + * \see RuntimeError + * \see ErrorCategory + */ +class RuntimeErrorCategory : public ErrorCategory { + using Base = ErrorCategory; + +public: + /** The associated error code enumeration type */ + using ErrorCodeEnum = RuntimeError; + + /** \copydoc ErrorCategory::name() */ + const char* + name() const noexcept override + { + return "RuntimeError"; + } + + /** \copydoc ErrorCategory::description(int) */ + const char* + description(int value) const noexcept override; + + /** \copydoc ErrorCategory::throw_exception(const Error&) */ + [[noreturn]] void + throw_exception(const Error& error) const override; +}; + +/** Singleton instance of the class */ +static constexpr RuntimeErrorCategory runtime_error_category; + +/** \private Enables implicit construction of ErrorCode from RuntimeError */ +template<> +struct IsErrorCodeEnum : public std::true_type {}; + +/** \private Enables implicit construction of ErrorCode from RuntimeError */ +constexpr ErrorCode +make_error_code(RuntimeError e) noexcept +{ + return {static_cast(e), &runtime_error_category}; +} + +} // namespace caesar diff --git a/error/v1/test/error_code_test.cpp b/error/v1/test/error_code_test.cpp new file mode 100644 index 0000000..48ab3e1 --- /dev/null +++ b/error/v1/test/error_code_test.cpp @@ -0,0 +1,52 @@ +#include + +#include + +namespace cs = caesar; + +TEST(ErrorCodeTest, FromEnum) +{ + const auto e = cs::DomainError::DivisionByZero; + const cs::ErrorCode error_code = e; + + EXPECT_EQ(error_code.value(), static_cast(e)); + EXPECT_EQ(error_code.category(), &cs::domain_error_category); +} + +TEST(ErrorCodeTest, AssignEnum) +{ + auto error_code = cs::ErrorCode(cs::DomainError::DivisionByZero); + + const auto e = cs::OutOfRange::OutOfBoundsAccess; + error_code = e; + + EXPECT_EQ(error_code.value(), static_cast(e)); + EXPECT_EQ(error_code.category(), &cs::out_of_range_category); +} + +TEST(ErrorCodeTest, Description) +{ + { + const cs::ErrorCode error_code = cs::DomainError::DivisionByZero; + const auto s = std::string(error_code.description()); + + EXPECT_EQ(s, "Division by zero"); + } + + { + const cs::ErrorCode error_code = cs::OutOfRange::OutOfBoundsAccess; + const auto s = std::string(error_code.description()); + + EXPECT_EQ(s, "Out of bounds access attempted"); + } +} + +TEST(ErrorCodeTest, Compare) +{ + const cs::ErrorCode error_code1 = cs::DomainError::DivisionByZero; + const cs::ErrorCode error_code2 = cs::DomainError::DivisionByZero; + const cs::ErrorCode error_code3 = cs::OutOfRange::OutOfBoundsAccess; + + EXPECT_TRUE(error_code1 == error_code2); + EXPECT_TRUE(error_code1 != error_code3); +} diff --git a/error/v1/test/error_test.cpp b/error/v1/test/error_test.cpp new file mode 100644 index 0000000..cbd51c9 --- /dev/null +++ b/error/v1/test/error_test.cpp @@ -0,0 +1,42 @@ +#include + +#include +#include + +namespace cs = caesar; + +TEST(ErrorTest, FromErrorCode) +{ + const auto error_code = cs::DomainError::DivisionByZero; + const auto error = cs::Error(error_code); + + EXPECT_EQ(error.error_code(), error_code); + EXPECT_EQ(error.line(), 11); + EXPECT_THAT(error.file(), testing::EndsWith("error_test.cpp")); +} + +TEST(ErrorTest, Message) +{ + const auto error_code = cs::DomainError::DivisionByZero; + const auto error = cs::Error(error_code); + + const auto s = "error_test.cpp:21: DomainError: Division by zero"; + EXPECT_THAT(error.message(), testing::EndsWith(s)); +} + +TEST(ErrorTest, ThrowException) +{ + { + const auto error_code = cs::DomainError::DivisionByZero; + const auto error = cs::Error(error_code); + + EXPECT_THROW({ error.throw_exception(); }, std::domain_error); + } + + { + const auto error_code = cs::OutOfRange::OutOfBoundsAccess; + const auto error = cs::Error(error_code); + + EXPECT_THROW({ error.throw_exception(); }, std::out_of_range); + } +} diff --git a/error/v1/test/expected_test.cpp b/error/v1/test/expected_test.cpp new file mode 100644 index 0000000..97388f9 --- /dev/null +++ b/error/v1/test/expected_test.cpp @@ -0,0 +1,111 @@ +#include + +#include +#include + +namespace cs = caesar; + +static cs::Expected +safe_divide(int x, int y) +{ + if (y == 0) { + return cs::Error(cs::DomainError::DivisionByZero); + } + return x / y; +} + +TEST(ExpectedTest, HasValue) +{ + { + const auto result = safe_divide(6, 3); + EXPECT_TRUE(result.has_value()); + } + + { + const auto result = safe_divide(1, 0); + EXPECT_FALSE(result.has_value()); + } +} + +TEST(ExpectedTest, Truthiness) +{ + { + const auto result = safe_divide(6, 3); + EXPECT_TRUE(result); + } + + { + const auto result = safe_divide(1, 0); + EXPECT_FALSE(result); + } +} + +TEST(ExpectedTest, HasError) +{ + { + const auto result = safe_divide(6, 3); + EXPECT_FALSE(result.has_error()); + } + + { + const auto result = safe_divide(1, 0); + EXPECT_TRUE(result.has_error()); + } +} + +TEST(ExpectedTest, Value) +{ + { + const auto result = safe_divide(6, 3); + EXPECT_EQ(result.value(), 2); + } + + { + const auto result = safe_divide(1, 0); + EXPECT_THROW({ result.value(); }, std::domain_error); + } +} + +TEST(ExpectedTest, Dereference) +{ + { + const auto result = safe_divide(6, 3); + EXPECT_EQ(*result, 2); + } + + { + const auto result = safe_divide(1, 0); + EXPECT_THROW({ *result; }, std::domain_error); + } +} + +TEST(ExpectedTest, DereferenceMember) +{ + struct Foo { + int bar; + }; + + { + const cs::Expected foo = Foo{123}; + EXPECT_EQ(foo->bar, 123); + } + + { + const cs::Expected foo = + cs::Error(cs::DomainError::DivisionByZero); + EXPECT_THROW({ foo->bar; }, std::domain_error); + } +} + +TEST(ExpectedTest, Error) +{ + { + const auto result = safe_divide(6, 3); + EXPECT_THROW({ result.error(); }, std::runtime_error); + } + + { + const auto result = safe_divide(1, 0); + EXPECT_THROW({ result.value(); }, std::domain_error); + } +}