From 77f0f921577738f9ad3c6ccc24045e184abccda5 Mon Sep 17 00:00:00 2001 From: Bingran Hu Date: Thu, 27 Mar 2025 18:16:35 -0400 Subject: [PATCH] Add Result. Add io_interface --- src/ystdlib/CMakeLists.txt | 1 + src/ystdlib/error_handling/CMakeLists.txt | 4 + src/ystdlib/error_handling/Result.hpp | 50 ++++++++++ .../error_handling/test/test_Result.cpp | 90 ++++++++++++++++++ src/ystdlib/io_interface/CMakeLists.txt | 17 ++++ src/ystdlib/io_interface/ErrorCode.cpp | 37 ++++++++ src/ystdlib/io_interface/ErrorCode.hpp | 25 +++++ src/ystdlib/io_interface/ReaderInterface.cpp | 45 +++++++++ src/ystdlib/io_interface/ReaderInterface.hpp | 95 +++++++++++++++++++ src/ystdlib/io_interface/StreamInterface.hpp | 66 +++++++++++++ src/ystdlib/io_interface/WriterInterface.hpp | 80 ++++++++++++++++ .../test/test_ReaderInterface.cpp | 0 .../test/test_WriterInterface.cpp | 0 taskfile.yaml | 1 + 14 files changed, 511 insertions(+) create mode 100644 src/ystdlib/error_handling/Result.hpp create mode 100644 src/ystdlib/error_handling/test/test_Result.cpp create mode 100644 src/ystdlib/io_interface/CMakeLists.txt create mode 100644 src/ystdlib/io_interface/ErrorCode.cpp create mode 100644 src/ystdlib/io_interface/ErrorCode.hpp create mode 100644 src/ystdlib/io_interface/ReaderInterface.cpp create mode 100644 src/ystdlib/io_interface/ReaderInterface.hpp create mode 100644 src/ystdlib/io_interface/StreamInterface.hpp create mode 100644 src/ystdlib/io_interface/WriterInterface.hpp create mode 100644 src/ystdlib/io_interface/test/test_ReaderInterface.cpp create mode 100644 src/ystdlib/io_interface/test/test_WriterInterface.cpp diff --git a/src/ystdlib/CMakeLists.txt b/src/ystdlib/CMakeLists.txt index 9a52a2e0..b2651234 100644 --- a/src/ystdlib/CMakeLists.txt +++ b/src/ystdlib/CMakeLists.txt @@ -1,2 +1,3 @@ add_subdirectory(containers) add_subdirectory(error_handling) +add_subdirectory(io_interface) diff --git a/src/ystdlib/error_handling/CMakeLists.txt b/src/ystdlib/error_handling/CMakeLists.txt index c5b70129..4af02d45 100644 --- a/src/ystdlib/error_handling/CMakeLists.txt +++ b/src/ystdlib/error_handling/CMakeLists.txt @@ -3,9 +3,13 @@ cpp_library( NAMESPACE ystdlib PUBLIC_HEADERS ErrorCode.hpp + Result.hpp + PUBLIC_LINK_LIBRARIES + outcome::hl TESTS_SOURCES test/constants.hpp test/test_ErrorCode.cpp + test/test_Result.cpp test/types.cpp test/types.hpp ) diff --git a/src/ystdlib/error_handling/Result.hpp b/src/ystdlib/error_handling/Result.hpp new file mode 100644 index 00000000..eb2ff7c0 --- /dev/null +++ b/src/ystdlib/error_handling/Result.hpp @@ -0,0 +1,50 @@ +#ifndef YSTDLIB_ERROR_HANDLING_RESULT_HPP +#define YSTDLIB_ERROR_HANDLING_RESULT_HPP + +#include + +#include +#include +#include + +namespace ystdlib::error_handling { +template +using Result = OUTCOME_V2_NAMESPACE::std_result; + +/** + * Default return value for ystdlib::error_handling::Result when function succeeds. + */ +[[nodiscard]] inline auto success() -> OUTCOME_V2_NAMESPACE::success_type { + return OUTCOME_V2_NAMESPACE::success(); +} + +// NOLINTBEGIN(bugprone-macro-parentheses, cppcoreguidelines-macro-usage) +/** + * This macro works the same way as Rust's `?` operator for error propagation. + */ +#define YSTDLIB_TRY(expr) \ + ({ \ + auto result{(expr)}; \ + if (result.has_error()) { \ + return result.error(); \ + } \ + using ReturnType = decltype(result.value()); \ + static_assert(std::is_move_constructible_v); \ + std::move(result.value()); \ + }) + +/** + * This macro works the same way as Rust's `?` operator for error propagation, without returning + * any value. + */ +#define YSTDLIB_ASSERT_NO_ERROR(expr) \ + ({ \ + if (auto const result{(expr)}; result.has_error()) { \ + return result.error(); \ + } \ + }) +// NOLINTEND(bugprone-macro-parentheses, cppcoreguidelines-macro-usage) +} // namespace ystdlib::error_handling + +#endif // YSTDLIB_ERROR_HANDLING_RESULT_HPP + diff --git a/src/ystdlib/error_handling/test/test_Result.cpp b/src/ystdlib/error_handling/test/test_Result.cpp new file mode 100644 index 00000000..c3025e24 --- /dev/null +++ b/src/ystdlib/error_handling/test/test_Result.cpp @@ -0,0 +1,90 @@ +#include +#include + +#include + +#include + +#include "types.hpp" + +using ystdlib::error_handling::Result; +using ystdlib::error_handling::success; +using ystdlib::error_handling::test::BinaryErrorCode; +using ystdlib::error_handling::test::BinaryErrorCodeEnum; + +namespace { +constexpr int cTestInt{123}; +constexpr auto cVoidFunc = [](bool is_error) -> Result { + if (is_error) { + return BinaryErrorCode{BinaryErrorCodeEnum::Failure}; + } + return success(); +}; +constexpr auto cIntFunc = [](bool is_error) -> Result { + if (is_error) { + return std::errc::bad_message; + } + return cTestInt; +}; +} // namespace + +namespace ystdlib::error_handling::test { +TEST_CASE("test_result_void", "[error_handling][Result]") { + auto const result_no_error{cVoidFunc(false)}; + REQUIRE_FALSE(result_no_error.has_error()); + REQUIRE(std::is_void_v); + + auto const result_has_error{cVoidFunc(true)}; + REQUIRE(result_has_error.has_error()); + REQUIRE(BinaryErrorCode{BinaryErrorCodeEnum::Failure} == result_has_error.error()); +} + +TEST_CASE("test_result_void_in_main", "[error_handling][Result]") { + auto main_func = [&](bool is_error) -> Result { + YSTDLIB_ASSERT_NO_ERROR(cVoidFunc(is_error)); + return success(); + }; + auto const main_no_error{main_func(false)}; + REQUIRE_FALSE(main_no_error.has_error()); + REQUIRE(std::is_void_v); + + auto const main_has_error{main_func(true)}; + REQUIRE(main_has_error.has_error()); + REQUIRE((BinaryErrorCode{BinaryErrorCodeEnum::Failure} == main_has_error.error())); +} + +TEST_CASE("test_result_int", "[error_handling][Result]") { + auto const result_no_error{cIntFunc(false)}; + REQUIRE_FALSE(result_no_error.has_error()); + REQUIRE((cTestInt == result_no_error.value())); + + auto const result_has_error{cIntFunc(true)}; + REQUIRE(result_has_error.has_error()); + REQUIRE(std::errc::bad_message == result_has_error.error()); +} + +TEST_CASE("test_result_int_in_main", "[error_handling][Result]") { + auto main_func = [&](bool is_error) -> Result { + YSTDLIB_ASSERT_NO_ERROR(cIntFunc(is_error)); + return success(); + }; + auto const main_no_error{main_func(false)}; + REQUIRE_FALSE(main_no_error.has_error()); + REQUIRE(std::is_void_v); + + auto const main_has_error{main_func(true)}; + REQUIRE(main_has_error.has_error()); + REQUIRE((std::errc::bad_message == main_has_error.error())); +} + +TEST_CASE("test_result_int_propagate", "[error_handling][Result]") { + auto main_func = [&](bool is_error) -> Result { return YSTDLIB_TRY(cIntFunc(is_error)); }; + auto const main_no_error{main_func(false)}; + REQUIRE_FALSE(main_no_error.has_error()); + REQUIRE((cTestInt == main_no_error.value())); + + auto const main_has_error{main_func(true)}; + REQUIRE(main_has_error.has_error()); + REQUIRE((std::errc::bad_message == main_has_error.error())); +} +} // namespace ystdlib::error_handling::test diff --git a/src/ystdlib/io_interface/CMakeLists.txt b/src/ystdlib/io_interface/CMakeLists.txt new file mode 100644 index 00000000..25d59bfe --- /dev/null +++ b/src/ystdlib/io_interface/CMakeLists.txt @@ -0,0 +1,17 @@ +cpp_library( + NAME io_interface + NAMESPACE ystdlib + PUBLIC_HEADERS + ErrorCode.hpp + ReaderInterface.hpp + WriterInterface.hpp + PRIVATE_SOURCES + ErrorCode.cpp + ReaderInterface.cpp + StreamInterface.hpp + PUBLIC_LINK_LIBRARIES + ystdlib::error_handling + TESTS_SOURCES + test/test_ReaderInterface.cpp + test/test_WriterInterface.cpp +) diff --git a/src/ystdlib/io_interface/ErrorCode.cpp b/src/ystdlib/io_interface/ErrorCode.cpp new file mode 100644 index 00000000..1e906efe --- /dev/null +++ b/src/ystdlib/io_interface/ErrorCode.cpp @@ -0,0 +1,37 @@ +#include "ErrorCode.hpp" + +#include + +#include + +using ystdlib::io_interface::ErrorCodeEnum; +using ErrorCategory = ystdlib::error_handling::ErrorCategory; + +template <> +auto ErrorCategory::name() const noexcept -> char const* { + return "ystdlib::io_interface::ErrorCode"; +} + +template <> +auto ErrorCategory::message(ErrorCodeEnum error_enum) const -> std::string { + switch (error_enum) { + case ErrorCodeEnum::BadParam: + return "Supplied parameters are invalid."; + case ErrorCodeEnum::BufferFull: + return "The output buffer is full and cannot accept more data."; + case ErrorCodeEnum::Corrupt: + return "The data is corrupted or malformed."; + case ErrorCodeEnum::EndOfFile: + return "End of file reached."; + case ErrorCodeEnum::EndOfStream: + return "End of the data stream reached."; + case ErrorCodeEnum::OutOfBounds: + return "Attempted to access data outside of valid bounds."; + case ErrorCodeEnum::OutOfMemory: + return "Memory allocation failed."; + case ErrorCodeEnum::Truncated: + return "The data was unexpectedly truncated or cut off."; + default: + return "Unrecognized ystdlib::io_interface::ErrorCodeEnum."; + } +} diff --git a/src/ystdlib/io_interface/ErrorCode.hpp b/src/ystdlib/io_interface/ErrorCode.hpp new file mode 100644 index 00000000..ea6bea17 --- /dev/null +++ b/src/ystdlib/io_interface/ErrorCode.hpp @@ -0,0 +1,25 @@ +#ifndef YSTDLIB_IO_INTERFACE_ERRORCODE_HPP +#define YSTDLIB_IO_INTERFACE_ERRORCODE_HPP + +#include + +#include + +namespace ystdlib::io_interface { +enum class ErrorCodeEnum : uint8_t { + BadParam, + BufferFull, + Corrupt, + EndOfFile, + EndOfStream, + OutOfBounds, + OutOfMemory, + Truncated +}; + +using ErrorCode = ystdlib::error_handling::ErrorCode; +} // namespace ystdlib::io_interface + +YSTDLIB_ERROR_HANDLING_MARK_AS_ERROR_CODE_ENUM(ystdlib::io_interface::ErrorCodeEnum); + +#endif // YSTDLIB_IO_INTERFACE_ERRORCODE_HPP diff --git a/src/ystdlib/io_interface/ReaderInterface.cpp b/src/ystdlib/io_interface/ReaderInterface.cpp new file mode 100644 index 00000000..65969a50 --- /dev/null +++ b/src/ystdlib/io_interface/ReaderInterface.cpp @@ -0,0 +1,45 @@ +#include "ReaderInterface.hpp" + +#include +#include +#include + +#include + +#include "ErrorCode.hpp" + +using ystdlib::error_handling::Result; + +namespace ystdlib::io_interface { +auto ReaderInterface::read_char() -> ystdlib::error_handling::Result { + char c{}; + YSTDLIB_ASSERT_NO_ERROR(read({&c, 1}, 1, true)); + return c; +} + +auto ReaderInterface::read_string(size_t num_bytes) + -> ystdlib::error_handling::Result { + std::string str(num_bytes, 0); + YSTDLIB_ASSERT_NO_ERROR(read({str.data(), num_bytes}, 1, true)); + return str; +} + +auto ReaderInterface::read_to_delimiter(std::span output_buffer, char delim, bool keep_delim) + -> ystdlib::error_handling::Result { + size_t num_bytes_read{0}; + while (num_bytes_read < output_buffer.size()) { + auto const c{YSTDLIB_TRY(read_char())}; + if (delim == c) { + break; + } + output_buffer[num_bytes_read++] = c; + } + if (num_bytes_read == output_buffer.size()) { + return ErrorCode{ErrorCodeEnum::BufferFull}; + } + if (keep_delim) { + output_buffer[num_bytes_read++] = delim; + } + return num_bytes_read; +} +} // namespace ystdlib::io_interface diff --git a/src/ystdlib/io_interface/ReaderInterface.hpp b/src/ystdlib/io_interface/ReaderInterface.hpp new file mode 100644 index 00000000..a3b07731 --- /dev/null +++ b/src/ystdlib/io_interface/ReaderInterface.hpp @@ -0,0 +1,95 @@ +#ifndef YSTDLIB_IO_INTERFACE_READERINTERFACE_HPP +#define YSTDLIB_IO_INTERFACE_READERINTERFACE_HPP + +#include +#include +#include + +#include + +#include "StreamInterface.hpp" + +namespace ystdlib::io_interface { +class ReaderInterface : public StreamInterface { +public: + // Constructor + ReaderInterface() = default; + + // Delete copy constructor and assignment operator + ReaderInterface(ReaderInterface const&) = delete; + auto operator=(ReaderInterface const&) -> ReaderInterface& = delete; + + // Default move constructor and assignment operator + ReaderInterface(ReaderInterface&&) noexcept = default; + auto operator=(ReaderInterface&&) noexcept -> ReaderInterface& = default; + + // Destructor + ~ReaderInterface() override = default; + + // Methods + /** + * Reads up to the given number of bytes from the underlying medium into the given buffer. + * @param output_buffer + * @param num_bytes + * @param exact If the number of bytes read needs to be equal to the amount requested. + * @return The actual number of bytes read. + * @return ystdlib::io_interface::ErrorCode::BadParam + * @return ystdlib::io_interface::ErrorCode::BufferFull + * @return ystdlib::io_interface::ErrorCode::Corrupt + * @return ystdlib::io_interface::ErrorCode::EndOfFile + * @return ystdlib::io_interface::ErrorCode::EndOfStream + */ + [[nodiscard]] virtual auto + read(std::span output_buffer, size_t num_bytes, bool exact = false) + -> ystdlib::error_handling::Result + = 0; + + /** + * @return The read character from the underlying medium. + * @return ystdlib::io_interface::ErrorCode::Corrupt + * @return ystdlib::io_interface::ErrorCode::EndOfFile + * @return ystdlib::io_interface::ErrorCode::EndOfStream + */ + [[nodiscard]] virtual auto read_char() -> ystdlib::error_handling::Result; + + /** + * @param num_bytes + * @return The read sring from the underlying medium. + * @return ystdlib::io_interface::ErrorCode::Corrupt + * @return ystdlib::io_interface::ErrorCode::EndOfFile + * @return ystdlib::io_interface::ErrorCode::EndOfStream + * @return ystdlib::io_interface::ErrorCode::OutOfMemory if the requested read is too large. + */ + [[nodiscard]] virtual auto read_string(size_t num_bytes) + -> ystdlib::error_handling::Result; + + /** + * @return The read numeric value from the underlying medium. + * @return ystdlib::io_interface::ErrorCode::Corrupt + * @return ystdlib::io_interface::ErrorCode::EndOfFile + * @return ystdlib::io_interface::ErrorCode::EndOfStream + */ + template + [[nodiscard]] auto read_numeric_value() -> ystdlib::error_handling::Result; + + /** + * Reads up to the next delimiter from the underlying medium into the given buffer. + * @param output_buffer + * @param delim The delimiter to stop at. + * @param keep_delim Whether to include the delimiter in the output. + * @return The actual number of bytes read. + * @return Same as ReaderInterface::read. + */ + [[nodiscard]] virtual auto + read_to_delimiter(std::span output_buffer, char delim, bool keep_delim) + -> ystdlib::error_handling::Result; +}; + +template +auto ReaderInterface::read_numeric_value() -> ystdlib::error_handling::Result { + T value{}; + return YSTDLIB_TRY(read({static_cast(&value), sizeof(T)}, sizeof(T), true)); +} +} // namespace ystdlib::io_interface + +#endif // YSTDLIB_IO_INTERFACE_READERINTERFACE_HPP diff --git a/src/ystdlib/io_interface/StreamInterface.hpp b/src/ystdlib/io_interface/StreamInterface.hpp new file mode 100644 index 00000000..31f5674e --- /dev/null +++ b/src/ystdlib/io_interface/StreamInterface.hpp @@ -0,0 +1,66 @@ +#ifndef YSTDLIB_IO_INTERFACE_STREAMINTERFACE_HPP +#define YSTDLIB_IO_INTERFACE_STREAMINTERFACE_HPP + +#include + +#include +#include +#include + +#include + +namespace ystdlib::io_interface { +/** + * Concept that defines an integer-based value type. + * + * TODO: move this into a general ystdlib-cpp typing library. + * @tparam T + */ +template +concept NumericType = requires(T type) { + { + static_cast>(type) + } -> std::convertible_to; +}; + +class StreamInterface { +public: + // Constructor + StreamInterface() = default; + + // Delete copy constructor and assignment operator + StreamInterface(StreamInterface const&) = delete; + auto operator=(StreamInterface const&) -> StreamInterface& = delete; + + // Default move constructor and assignment operator + StreamInterface(StreamInterface&&) noexcept = default; + auto operator=(StreamInterface&&) noexcept -> StreamInterface& = default; + + // Destructor + virtual ~StreamInterface() = default; + + /** + * Seeks from the beginning to the given position. + * @param pos + * @return ystdlib::io_interface::ErrorCodeNum::OutOfBounds + */ + [[nodiscard]] virtual auto seek_from_begin(size_t pos) -> ystdlib::error_handling::Result + = 0; + + /** + * Seeks from the current position to the next position by the given offset amount. + * @param offset + * @return ystdlib::io_interface::ErrorCodeNum::OutOfBounds + */ + [[nodiscard]] virtual auto seek_from_current(off_t offset) + -> ystdlib::error_handling::Result + = 0; + + /** + * @return The current position of the file stream pointer. + */ + [[nodiscard]] virtual auto get_pos() const -> size_t = 0; +}; +} // namespace ystdlib::io_interface + +#endif // YSTDLIB_IO_INTERFACE_STREAMINTERFACE_HPP diff --git a/src/ystdlib/io_interface/WriterInterface.hpp b/src/ystdlib/io_interface/WriterInterface.hpp new file mode 100644 index 00000000..fdb0b26d --- /dev/null +++ b/src/ystdlib/io_interface/WriterInterface.hpp @@ -0,0 +1,80 @@ +#ifndef YSTDLIB_IO_INTERFACE_WRITERINTERFACE_HPP +#define YSTDLIB_IO_INTERFACE_WRITERINTERFACE_HPP + +#include +#include + +#include + +#include "StreamInterface.hpp" + +namespace ystdlib::io_interface { +class WriterInterface : public StreamInterface { +public: + // Constructor + WriterInterface() = default; + + // Delete copy constructor and assignment operator + WriterInterface(WriterInterface const&) = delete; + auto operator=(WriterInterface const&) -> WriterInterface& = delete; + + // Default move constructor and assignment operator + WriterInterface(WriterInterface&&) noexcept = default; + auto operator=(WriterInterface&&) noexcept -> WriterInterface& = default; + + // Destructor + ~WriterInterface() override = default; + + // Methods + /** + * Writes the given data to the underlying medium. + * @param data_buffer + * @return ystdlib::io_interface::ErrorCode::BadParam + * @return ystdlib::io_interface::ErrorCode::BufferFull + * @return ystdlib::io_interface::ErrorCode::Corrupt + */ + [[nodiscard]] virtual auto write(std::span output_buffer) + -> ystdlib::error_handling::Result + = 0; + + /** + * Forces any buffered output data to be available at the underlying medium. + * @return ystdlib::io_interface::ErrorCode::Corrupt + */ + [[nodiscard]] virtual auto flush() -> ystdlib::error_handling::Result = 0; + + /** + * Writes a numeric value to the underlying medium. + * @param val Value to write + * @return Same as WriterInterface::write. + */ + template + [[nodiscard]] auto write_numeric_value(T value) -> ystdlib::error_handling::Result; + + /** + * Writes the given character to the underlying medium. + * @param c + * @return Same as WriterInterface::write. + */ + [[nodiscard]] virtual auto write_char(char c) -> ystdlib::error_handling::Result { + return write({&c, 1}); + } + + /** + * Writes the given string to the underlying medium. + * @param str + * @return Same as WriterInterface::write. + */ + [[nodiscard]] virtual auto write_string(std::string const& str) + -> ystdlib::error_handling::Result { + return write({str.c_str(), str.length()}); + } +}; + +template +auto WriterInterface::write_numeric_value(T value) -> ystdlib::error_handling::Result { + return write({static_cast(&value), sizeof(T)}); +} +} // namespace ystdlib::io_interface + +#endif // YSTDLIB_IO_INTERFACE_WRITERINTERFACE_HPP diff --git a/src/ystdlib/io_interface/test/test_ReaderInterface.cpp b/src/ystdlib/io_interface/test/test_ReaderInterface.cpp new file mode 100644 index 00000000..e69de29b diff --git a/src/ystdlib/io_interface/test/test_WriterInterface.cpp b/src/ystdlib/io_interface/test/test_WriterInterface.cpp new file mode 100644 index 00000000..e69de29b diff --git a/taskfile.yaml b/taskfile.yaml index 4b76fb71..11155725 100644 --- a/taskfile.yaml +++ b/taskfile.yaml @@ -20,6 +20,7 @@ vars: - "all" - "containers" - "error_handling" + - "io_interface" tasks: clean: