diff --git a/.github/workflows/linux.yaml b/.github/workflows/linux.yaml index a367e8f6a..9633ab354 100644 --- a/.github/workflows/linux.yaml +++ b/.github/workflows/linux.yaml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: false matrix: - format: ["JSON", "AVRO", "CAPNPROTO", "CBOR", "FLEXBUFFERS", "MSGPACK", "PARQUET", "TOML", "UBJSON", "XML", "YAML", "benchmarks", "headers"] + format: ["JSON", "AVRO", "CAPNPROTO", "CBOR", "CEREAL", "FLEXBUFFERS", "MSGPACK", "PARQUET", "TOML", "UBJSON", "XML", "YAML", "benchmarks", "headers"] compiler: [llvm, gcc] compiler-version: [11, 12, 13, 14, 16, 17, 18] cxx: [20, 23] diff --git a/.github/workflows/windows.yaml b/.github/workflows/windows.yaml index cc191e6d7..6aa4e3a03 100644 --- a/.github/workflows/windows.yaml +++ b/.github/workflows/windows.yaml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: false matrix: - format: ["JSON", "AVRO", "CAPNPROTO", "CBOR", "FLEXBUFFERS", "MSGPACK", "PARQUET", "TOML", "UBJSON", "XML", "YAML", "benchmarks"] + format: ["JSON", "AVRO", "CAPNPROTO", "CBOR", "CEREAL", "FLEXBUFFERS", "MSGPACK", "PARQUET", "TOML", "UBJSON", "XML", "YAML", "benchmarks"] name: "windows-msvc (${{ matrix.format }})" concurrency: group: "windows-${{ github.ref }}-${{ github.job }}-${{ matrix.format }}" diff --git a/.gitignore b/.gitignore index 43aa36d0f..b4e879274 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ *.bson *.capnproto *.cbor +*.cereal *.csv *.json *.fb diff --git a/CMakeLists.txt b/CMakeLists.txt index a18a82ef2..6b8b2408a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,7 @@ option(REFLECTCPP_AVRO "Enable AVRO support" ${REFLECTCPP_ALL_FORMATS}) option(REFLECTCPP_BSON "Enable BSON support" ${REFLECTCPP_ALL_FORMATS}) option(REFLECTCPP_CAPNPROTO "Enable Cap’n Proto support" ${REFLECTCPP_ALL_FORMATS}) option(REFLECTCPP_CBOR "Enable CBOR support" ${REFLECTCPP_ALL_FORMATS}) +option(REFLECTCPP_CEREAL "Enable Cereal support" ${REFLECTCPP_ALL_FORMATS}) option(REFLECTCPP_CSV "Enable CSV support" ${REFLECTCPP_ALL_FORMATS}) option(REFLECTCPP_FLEXBUFFERS "Enable flexbuffers support" ${REFLECTCPP_ALL_FORMATS}) option(REFLECTCPP_MSGPACK "Enable msgpack support" ${REFLECTCPP_ALL_FORMATS}) @@ -49,6 +50,7 @@ if(REFLECTCPP_BUILD_BENCHMARKS) set(REFLECTCPP_BSON ON CACHE BOOL "" FORCE) set(REFLECTCPP_CAPNPROTO ON CACHE BOOL "" FORCE) set(REFLECTCPP_CBOR ON CACHE BOOL "" FORCE) + set(REFLECTCPP_CEREAL ON CACHE BOOL "" FORCE) set(REFLECTCPP_FLEXBUFFERS ON CACHE BOOL "" FORCE) set(REFLECTCPP_MSGPACK ON CACHE BOOL "" FORCE) set(REFLECTCPP_XML ON CACHE BOOL "" FORCE) @@ -57,11 +59,25 @@ if(REFLECTCPP_BUILD_BENCHMARKS) set(REFLECTCPP_YAML ON CACHE BOOL "" FORCE) endif() -if (REFLECTCPP_BUILD_TESTS OR REFLECTCPP_BUILD_BENCHMARKS OR REFLECTCPP_CHECK_HEADERS OR - (REFLECTCPP_JSON AND NOT REFLECTCPP_USE_BUNDLED_DEPENDENCIES) OR REFLECTCPP_AVRO OR - REFLECTCPP_BSON OR REFLECTCPP_CAPNPROTO OR REFLECTCPP_CBOR OR REFLECTCPP_CSV OR - REFLECTCPP_FLEXBUFFERS OR REFLECTCPP_MSGPACK OR REFLECTCPP_PARQUET OR REFLECTCPP_XML OR - REFLECTCPP_TOML OR REFLECTCPP_UBJSON OR REFLECTCPP_YAML) +if ( + REFLECTCPP_BUILD_TESTS OR + REFLECTCPP_BUILD_BENCHMARKS OR + REFLECTCPP_CHECK_HEADERS OR + (REFLECTCPP_JSON AND NOT REFLECTCPP_USE_BUNDLED_DEPENDENCIES) OR + REFLECTCPP_AVRO OR + REFLECTCPP_BSON OR + REFLECTCPP_CAPNPROTO OR + REFLECTCPP_CBOR OR + REFLECTCPP_CEREAL OR + REFLECTCPP_CSV OR + REFLECTCPP_FLEXBUFFERS OR + REFLECTCPP_MSGPACK OR + REFLECTCPP_PARQUET OR + REFLECTCPP_XML OR + REFLECTCPP_TOML OR + REFLECTCPP_UBJSON OR + REFLECTCPP_YAML +) # enable vcpkg per default if features other than JSON are required set(REFLECTCPP_USE_VCPKG_DEFAULT ON) endif() @@ -98,7 +114,11 @@ if (REFLECTCPP_USE_VCPKG) if (REFLECTCPP_CBOR OR REFLECTCPP_CHECK_HEADERS) list(APPEND VCPKG_MANIFEST_FEATURES "cbor") endif() - + + if (REFLECTCPP_CEREAL OR REFLECTCPP_CHECK_HEADERS) + list(APPEND VCPKG_MANIFEST_FEATURES "cereal") + endif() + if (REFLECTCPP_CSV OR REFLECTCPP_CHECK_HEADERS) list(APPEND VCPKG_MANIFEST_FEATURES "csv") endif() @@ -172,10 +192,6 @@ else() target_compile_options(reflectcpp PRIVATE $<$: -Wall -Wextra -Wpedantic -Wshadow -Wconversion - #-Wnull-dereference -Wold-style-cast - #-g3 -fno-omit-frame-pointer - #-fsanitize=address,undefined - #-fno-sanitize-recover=undefined > ) endif() @@ -312,6 +328,16 @@ if (REFLECTCPP_CBOR OR REFLECTCPP_CHECK_HEADERS) include_directories(PUBLIC ${jsoncons_INCLUDE_DIRS}) endif () +if (REFLECTCPP_CEREAL OR REFLECTCPP_CHECK_HEADERS) + list(APPEND REFLECT_CPP_SOURCES + src/reflectcpp_cereal.cpp + ) + if (NOT TARGET cereal::cereal) + find_package(cereal CONFIG REQUIRED) + endif () + target_link_libraries(reflectcpp PUBLIC cereal::cereal) +endif() + if (REFLECTCPP_CSV OR REFLECTCPP_CHECK_HEADERS) if (NOT TARGET Arrow) find_package(Arrow CONFIG REQUIRED) diff --git a/MR_DESCRIPTION.md b/MR_DESCRIPTION.md deleted file mode 100644 index 08c3ee6d2..000000000 --- a/MR_DESCRIPTION.md +++ /dev/null @@ -1,385 +0,0 @@ -# Fix 30 bugs found during code review - -## Summary - -This MR fixes 30 bugs across core types, JSON, XML, TOML, MsgPack, BSON, Avro, Cap'n Proto, and CLI modules. Each bug has a regression test that fails before the fix and passes after. - -**Stats:** 39 files changed, 1197 insertions, 52 deletions. - ---- - -## Discussion: Breaking API Changes - -The following 3 fixes change public API behavior. They are correct from a safety standpoint, but may break downstream code. Please review whether a deprecation period or alternative approach is preferred. - -### 1. `Validator::get()`/`value()` return mutable reference - -> **Breaking change:** Removes non-const overloads of `get()`, `value()`, `operator*()`, `operator()()`. -> Code like `validator.get() = new_value` will no longer compile. -> -> **Conflicts with API convention:** PR #613 established that wrapper types should provide `operator*`, `.value()`, `.get()`, and `operator()`. This fix removes the non-const variants to prevent mutation that bypasses validation. An alternative would be to keep non-const overloads but route them through re-validation (i.e. a validating setter). - -**File:** `include/rfl/Validator.hpp:98-99, 114-115` - -**Problem:** Non-const `get()`, `value()`, `operator*()`, `operator()()` return `T&`, allowing direct write that bypasses validation. - -**Test:** `tests/generic/test_validator.cpp` — `validator_get_does_not_allow_invalid_mutation`. Uses `std::is_assignable_v` to verify `get()` returns `const T&`. - -**Fix:** Removed all non-const overloads (`get()`, `value()`, `operator*()`, `operator()()`). - ---- - -### 2. `Literal(const std::string&)` is implicit - -> **Breaking change:** Adds `explicit` to the string constructor. -> Code like `rfl::Literal<"a","b"> lit = my_string;` will no longer compile; must use `rfl::Literal<"a","b">(my_string)` instead. - -**File:** `include/rfl/Literal.hpp:46` - -**Problem:** Non-explicit constructor allows implicit `std::string` → `Literal` conversion, which throws on invalid input. Surprising for users. - -**Test:** `tests/generic/test_literal.cpp` — `literal_constructor_is_explicit`. Uses `std::is_convertible_v` to verify implicit conversion is disabled. - -**Fix:** Added `explicit` keyword. - ---- - -### 3. `TaggedUnion` — typo `discrimininator_` - -> **Breaking change:** Renames the public static member `discrimininator_` → `discriminator_`. -> Code referencing `TaggedUnion<...>::discrimininator_` will no longer compile. -> A `[[deprecated]]` alias could be added to ease migration if desired. - -**File:** `include/rfl/TaggedUnion.hpp:15` - -**Problem:** Extra 'n' — `discrimininator_` instead of `discriminator_`. - -**Test:** `tests/generic/test_tagged_union.cpp` — `tagged_union_discriminator_spelling`. Uses concepts to check member name existence. - -**Fix:** Renamed to `discriminator_`. Also fixed the typo in the error message string literal `"discrimininator"` → `"discriminator"` in `Parser_tagged_union.hpp:151`. - ---- - -## Bugs Fixed - -### 4. `Tuple::operator<=>` always returns `equivalent` - -**File:** `include/rfl/Tuple.hpp:107` - -**Problem:** The condition `_ordering != equivalent` is checked on first iteration where `_ordering` is initialized to `equivalent` — the body never executes. The operator returns `equivalent` for any two tuples. - -**Test:** `tests/generic/test_tuple.cpp` — `tuple_spaceship_not_always_equivalent`, `tuple_spaceship_less`. Verifies `Tuple(1) <=> Tuple(2)` returns `less`, not `equivalent`. - -**Fix:** Changed `!=` to `==` so the comparison body executes when ordering is still undetermined. - ---- - -### 5. `Result::operator=(const Result&)` does not update `success_` - -**File:** `include/rfl/Result.hpp:210-216` - -**Problem:** The converting assignment copies `t_or_err_` but never updates `success_`. After assigning an error `Result` into a value `Result`, `success_` remains `true` — `.value()` reads garbage. - -**Test:** `tests/generic/test_result.cpp` — `result_cross_type_assign_error_clears_success`. Assigns error `Result` to value `Result`, checks the target is falsy. - -**Fix:** Replaced raw union copy with proper `destroy()` + `success_` update + `move_from_other()`. - ---- - -### 6. `Validator::operator=(U&&)` is `noexcept` but throws - -**File:** `include/rfl/Validator.hpp:77` - -**Problem:** The operator calls `.value()` which throws `std::runtime_error` on validation failure. Marked `noexcept` — any validation error calls `std::terminate`. - -**Test:** `tests/generic/test_validator.cpp` — `validator_assign_rvalue_operator_is_noexcept_but_can_throw`. Uses `std::is_nothrow_assignable_v` to verify the operator is no longer `noexcept`. - -**Fix:** Removed `noexcept` specifier. - ---- - -### 7. `Generic::to_int()` silently truncates `int64_t` to `int` - -**File:** `include/rfl/Generic.hpp:175-188` - -**Problem:** `static_cast(_v)` with no range check. Values outside `[INT_MIN, INT_MAX]` are silently truncated. - -**Test:** `tests/generic/test_generic.cpp` — `generic_to_int_rejects_overflow`, `generic_to_int_rejects_underflow`. Passes `INT_MAX+1` and `INT_MIN-1`, expects failure. - -**Fix:** Added range check before the cast, returning an error for out-of-range values. - ---- - -### 8. `Box`/`Ref` `operator<=>` compares pointer addresses - -**Files:** `include/rfl/Box.hpp:130`, `include/rfl/Ref.hpp:118` - -**Problem:** `_b1.ptr() <=> _b2.ptr()` compares memory addresses instead of pointed-to values. Two `Box(42)` with different allocations compare as unequal. - -**Test:** `tests/generic/test_box.cpp` — `box_spaceship_compares_values_not_pointers`. `tests/generic/test_ref.cpp` — `ref_spaceship_compares_values_not_pointers`. - -**Fix:** Changed to `*_b1 <=> *_b2` (dereference before compare). - ---- - -### 9. `Field`/`Flatten`/`Skip` cross-type move constructor copies instead of moving - -**Files:** `include/rfl/Field.hpp:37`, `include/rfl/Flatten.hpp:32`, `include/rfl/internal/Skip.hpp:43` - -**Problem:** `value_(_field.get())` — `get()` returns lvalue reference, so the copy constructor is called instead of move. - -**Test:** `tests/generic/test_field.cpp` — `field_cross_type_move_does_not_copy`. `tests/generic/test_flatten.cpp` — `flatten_cross_type_move_does_not_copy`. `tests/generic/test_skip.cpp` — `skip_cross_type_move_does_not_copy`. Use `MoveTracker` types that count copies vs moves. - -**Fix:** Changed to `value_(std::move(_field.get()))` in all three types. - ---- - -### 10. `Result::value_or(T&&)` returns dangling rvalue reference - -**File:** `include/rfl/Result.hpp:295` - -**Problem:** Returns `T&&` instead of `T`. If the default argument is a temporary, the caller gets a dangling reference. - -**Test:** `tests/generic/test_result.cpp` — `result_value_or_returns_value_not_rvalue_ref`. Checks return type is `T`, not `T&&`. - -**Fix:** Changed return type from `T&&` to `T`. - ---- - -### 11. `Result::error() &&` returns `Error&` instead of `Error&&` - -**File:** `include/rfl/Result.hpp:335` - -**Problem:** The rvalue-qualified overload returns `Error&` but `get_err() &&` returns `Error&&`. Prevents move semantics on rvalue Results. - -**Test:** `tests/generic/test_result.cpp` — `result_error_rvalue_qualified_returns_rvalue_ref`. Checks return type is `Error&&`. - -**Fix:** Changed return type from `Error&` to `Error&&`. - ---- - -### 12. `Object::difference_type` is unsigned - -**File:** `include/rfl/Object.hpp:29` - -**Problem:** `using difference_type = typename DataType::size_type` — `size_type` is unsigned, but the C++ standard requires `difference_type` to be signed. - -**Test:** `tests/generic/test_object.cpp` — `object_difference_type_is_signed`. Uses `std::is_signed_v`. - -**Fix:** Changed to `typename DataType::difference_type`. - ---- - -### 13. `transform_camel_case` prepends `_` to names starting with uppercase - -**File:** `include/rfl/internal/transform_case.hpp:60-73` - -**Problem:** When `_i == 0` and the first character is uppercase, the function unconditionally prepends `'_'`. `"MyField"` becomes `"_my_field"` instead of `"my_field"`. - -**Test:** `tests/json/test_regression_bugs.cpp` — `camel_case_to_snake_case_no_leading_underscore`. Serializes a struct with `CamelCaseToSnakeCase` and checks output. - -**Fix:** Added `sizeof...(chars) > 0` guard so `'_'` is only inserted between characters, not at the beginning. - ---- - -### 14. `Timestamp::to_time_t()` double-subtracts timezone offset - -**File:** `include/rfl/Timestamp.hpp:120` - -**Problem:** `timegm(&tm) - tm_.tm_gmtoff` — `timegm()` already returns UTC. Subtracting `tm_gmtoff` applies the timezone correction twice. Non-Windows only. - -**Test:** `tests/json/test_regression_bugs.cpp` — `timestamp_to_time_t_no_double_timezone_correction`. Sets `tm_gmtoff = 3600` and verifies `to_time_t()` returns the correct UTC epoch. - -**Fix:** Removed `- tm_.tm_gmtoff`. - ---- - -### 15. `FieldVariantParser` — wrong error message - -**File:** `include/rfl/parsing/FieldVariantParser.hpp:47-50` - -**Problem:** When zero matching fields are found, the error says "found more than one" instead of "found none". - -**Test:** `tests/json/test_regression_bugs.cpp` — `field_variant_empty_object_error_message`. Parses `"{}"` and checks the error message does not contain "more than one". - -**Fix:** Changed message to "found none". - ---- - -### 16. `json::Writer` reads past `string_view` bounds - -**File:** `src/rfl/json/Writer.cpp:68, :91, :113` - -**Problem:** `yyjson_mut_strcpy(_name.data())` reads until `\0`, but `string_view::data()` is not null-terminated. Reads past buffer for substrings. - -**Test:** `tests/json/test_regression_bugs.cpp` — `json_writer_handles_non_null_terminated_field_names`. Creates a `string_view` substring and verifies the JSON key matches the view's length. - -**Fix:** Changed all three call sites from `yyjson_mut_strcpy` to `yyjson_mut_strncpy` with explicit size. - ---- - -### 17. `cli::Reader` — `stoull` accepts negative numbers for unsigned types - -**File:** `include/rfl/cli/Reader.hpp:77` - -**Problem:** `std::stoull("-1")` wraps to `ULLONG_MAX` without error. `static_cast` wraps again to 65535. - -**Test:** `tests/cli/test_regression_bugs.cpp` — `cli_rejects_negative_for_unsigned`. Passes `--port=-1` and expects failure. - -**Fix:** Added explicit check for leading `-` character, returning an error for negative input. - ---- - -### 18. `cli::Reader` — no range check on unsigned narrowing cast - -**File:** `include/rfl/cli/Reader.hpp:77` - -**Problem:** `static_cast(stoull(str))` silently truncates. `stoull("99999")` for `uint16_t` produces 34463. - -**Test:** `tests/cli/test_regression_bugs.cpp` — `cli_rejects_out_of_range_for_narrow_type`. Passes `--port=99999` for `uint16_t` and expects failure. - -**Fix:** Added `std::numeric_limits::max()` bounds check before the cast. - ---- - -### 19. `cli::Reader` — no range check on signed narrowing cast - -**File:** `include/rfl/cli/Reader.hpp:101-102` - -**Problem:** `static_cast(stoll(str))` silently truncates for `int8_t`/`int16_t`/`int32_t`. `stoll("200")` cast to `int8_t` wraps to -56. - -**Test:** `tests/cli/test_regression_bugs.cpp` — `cli_rejects_out_of_range_for_signed_narrow_type`, `cli_rejects_large_negative_for_signed_narrow_type`. Passes `--level=200` and `--level=-200` for `int8_t` and expects failure. - -**Fix:** Added `std::numeric_limits::min()`/`max()` bounds check before the cast. - ---- - -### 20. `cli::Reader` — `std::stod` is locale-dependent - -**File:** `include/rfl/cli/Reader.hpp:64` - -**Problem:** `std::stod` uses the C locale. In locales with comma decimal separator (e.g. `de_DE`), `"3.14"` parses as `3.0`. - -**Test:** `tests/cli/test_regression_bugs.cpp` — `cli_float_parsing_ignores_locale`. Sets `de_DE` locale, parses `--rate=3.14`, checks result is `3.14`. - -**Fix:** Replaced `std::stod` with `std::from_chars` which is locale-independent. - ---- - -### 21. `capnproto::read(istream, schema)` — infinite recursion - -**File:** `include/rfl/capnproto/read.hpp:79-82` - -**Problem:** The overload `read(std::istream&, const Schema&)` calls `read(_stream, _schema)` — resolves to itself. Stack overflow. - -**Test:** `tests/capnproto/compile_fail/capnproto_read_istream_schema.cpp` — instantiates the overload. Before the fix, GCC rejects it as "use of auto before deduction" (infinite recursion detected at compile time). After the fix, the file compiles successfully, verifying the overload resolves correctly. - -**Fix:** Read bytes from stream into a vector, then call `read(bytes.data(), bytes.size(), _schema)`. - ---- - -### 22. `avro::SchemaImpl` move-assignment — double-decrement / use-after-free - -**File:** `src/rfl/avro/SchemaImpl.cpp:34-42` - -**Problem:** Move-assignment neither releases the old `iface_` (leak) nor nullifies `_other.iface_` (double-decrement when `_other`'s destructor runs). - -**Test:** `tests/avro/test_regression_bugs.cpp` — `schema_impl_move_assignment_no_double_free`. Move-assigns two `SchemaImpl` objects, verifies source `iface_` is null after move. - -**Fix:** Added `avro_value_iface_decref`/`avro_schema_decref` for the old iface before reassignment, and `_other.iface_ = nullptr` after. - ---- - -### 23. `xml::Reader` uses `stoi` for all integer types - -**File:** `include/rfl/xml/Reader.hpp:99-105` - -**Problem:** `std::stoi` returns `int` (32-bit). For `int64_t`/`uint64_t` fields, values beyond `INT_MAX` throw `out_of_range` or are silently truncated. Additionally, `static_cast` after `stoll`/`stoull` silently truncates for narrow types like `int16_t`/`uint16_t`. - -**Test:** `tests/xml/test_regression_bugs.cpp` — `read_int64_above_int32_max`, `read_int64_large_negative`, `read_uint64_large_value`, `read_rejects_out_of_range_for_int16`, `read_rejects_negative_for_uint16`, `read_rejects_large_negative_for_int16`. - -**Fix:** Replaced with `std::from_chars` which parses directly into the target type, handling overflow, negative-for-unsigned, and narrowing without manual checks. - ---- - -### 24. `toml::Writer` — `at_path()` dereferences as path, crashes on dotted keys - -**File:** `src/rfl/toml/Writer.cpp:26, :40` - -**Problem:** After `emplace("a.b", ...)`, `at_path("a.b")` interprets the dot as a path separator and looks for `table["a"]["b"]` which doesn't exist — returns invalid node — `as_table()`/`as_array()` returns nullptr. - -**Test:** `tests/toml/test_regression_bugs.cpp` — `field_name_with_dot_does_not_crash`, `array_field_name_with_dot_does_not_crash`. Use `rfl::Rename<"a.b", ...>` to create dotted field names. - -**Fix:** Replaced `at_path(_name)` with `operator[](_name)` which does literal key lookup. - ---- - -### 25. `msgpack::read` ignores `msgpack_unpack` return value - -**File:** `include/rfl/msgpack/read.hpp:35-36` - -**Problem:** If `msgpack_unpack` fails, the return value is not checked. `deserialized` remains uninitialized — garbage is parsed, producing misleading downstream errors like "Could not cast to a map". - -**Test:** `tests/msgpack/test_regression_bugs.cpp` — `read_single_invalid_byte_returns_meaningful_error`, `read_truncated_data_returns_meaningful_error`. Pass invalid/truncated data and verify the error doesn't mention "cast". - -**Fix:** Check return value; return `"Failed to unpack msgpack data."` error on failure. Accept both `MSGPACK_UNPACK_SUCCESS` and `MSGPACK_UNPACK_EXTRA_BYTES`. - ---- - -### 26. `msgpack::Writer` stores `uint64_t` as signed `int64` - -**File:** `include/rfl/msgpack/Writer.hpp:133-134` - -**Problem:** All integers are packed via `msgpack_pack_int64(static_cast(...))`. For `uint64_t` values above `INT64_MAX`, the cast wraps to negative — msgpack stores them as `NEGATIVE_INTEGER`. - -**Test:** `tests/msgpack/test_regression_bugs.cpp` — `uint64_above_int64_max_stored_as_positive`, `uint64_max_stored_as_positive`. Verify raw msgpack wire type is `POSITIVE_INTEGER`. - -**Fix:** Added a separate `std::is_unsigned` branch that calls `msgpack_pack_uint64`. - ---- - -### 27. `bson::Reader` rejects integer BSON values for float fields - -**File:** `include/rfl/bson/Reader.hpp:127-132` - -**Problem:** The float branch checks `btype != BSON_TYPE_DOUBLE` and rejects everything else — even though the error message claims `int32`/`int64`/`date_time` are valid. - -**Test:** `tests/bson/test_regression_bugs.cpp` — `read_int64_into_double_field`, `read_large_int64_into_double_field`. Write an int struct, read it back as a double struct. - -**Fix:** Changed to a `switch` accepting `BSON_TYPE_DOUBLE`, `INT32`, `INT64`, and `DATE_TIME`. - ---- - -### 28. `delete`/`delete[]` mismatch in test - -**File:** `tests/json/test_pointer_fields.cpp` - -**Problem:** A raw pointer field allocated with `new[]` was freed with `delete` instead of `delete[]`. - -**Test:** Pre-existing test; fix prevents undefined behavior under sanitizers. - -**Fix:** Changed `delete` to `delete[]`. - ---- - -### 29. `Flatten(U&&)` universal reference constructor copies instead of forwarding - -**File:** `include/rfl/Flatten.hpp:40` - -**Problem:** `value_(_value)` — `_value` is a named parameter (lvalue), so the copy constructor is always called even when an rvalue is passed. Should be `value_(std::forward(_value))`. - -**Test:** `tests/generic/test_flatten.cpp` — `flatten_universal_ref_ctor_forwards_rvalue`. Constructs `Flatten` from `FlatDerived&&` and verifies rvalue conversion is used. - -**Fix:** Changed to `value_(std::forward(_value))`. - ---- - -### 30. `Flatten::operator=(Flatten&&)` forwards `Flatten` instead of `U` - -**File:** `include/rfl/Flatten.hpp:103-105` - -**Problem:** `std::forward(_f)` forwards the `Flatten` object, not `U`. Assignment `value_ = Flatten` looks for implicit conversion which uses `get()` (lvalue ref) — copy instead of move. Causes hard compile error for types without `Flatten` → `Type` conversion. - -**Test:** `tests/generic/compile_fail/flatten_cross_type_move_assign.cpp` — compile test that instantiates the cross-type move assignment. Verifies compilation succeeds after the fix. - -**Fix:** Changed to `value_ = std::move(_f.get())`. diff --git a/README.md b/README.md index 9ac863e02..1232851a8 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ The following table lists the serialization formats currently supported by refle | BSON | [libbson](https://github.com/mongodb/mongo-c-driver) | >= 1.25.1 | Apache 2.0 | JSON-like binary format | | Cap'n Proto | [capnproto](https://capnproto.org) | >= 1.0.2 | MIT | Schemaful binary format | | CBOR | [jsoncons](https://github.com/danielaparker/jsoncons)| >= 0.176.0 | BSL 1.0 | JSON-like binary format | +| Cereal | [Cereal](https://uscilab.github.io/cereal/) | >= 1.3.2 | BSD | C++ serialization library with multiple formats | | CSV | [Apache Arrow](https://arrow.apache.org/) | >= 21.0.0 | Apache 2.0 | Tabular textual format | | flexbuffers | [flatbuffers](https://github.com/google/flatbuffers) | >= 23.5.26 | Apache 2.0 | Schema-less version of flatbuffers, binary format | | msgpack | [msgpack-c](https://github.com/msgpack/msgpack-c) | >= 6.0.0 | BSL 1.0 | JSON-like binary format | @@ -155,6 +156,7 @@ rfl::avro::write(homer); rfl::bson::write(homer); rfl::capnproto::write(homer); rfl::cbor::write(homer); +rfl::cereal::write(homer); rfl::flexbuf::write(homer); rfl::msgpack::write(homer); rfl::toml::write(homer); @@ -165,6 +167,7 @@ rfl::avro::read(avro_bytes); rfl::bson::read(bson_bytes); rfl::capnproto::read(capnproto_bytes); rfl::cbor::read(cbor_bytes); +rfl::cereal::read(cereal_bytes); rfl::flexbuf::read(flexbuf_bytes); rfl::msgpack::read(msgpack_bytes); rfl::toml::read(toml_string); diff --git a/benchmarks/all/canada_read.cpp b/benchmarks/all/canada_read.cpp index 9449a74b4..e848ffea4 100644 --- a/benchmarks/all/canada_read.cpp +++ b/benchmarks/all/canada_read.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -104,6 +105,17 @@ static void BM_canada_read_reflect_cpp_cbor_without_field_names( } BENCHMARK(BM_canada_read_reflect_cpp_cbor_without_field_names); +static void BM_canada_read_reflect_cpp_cereal(benchmark::State &state) { + const auto data = rfl::cereal::write(load_data()); + for (auto _ : state) { + const auto res = rfl::cereal::read(data); + if (!res) { + std::cout << res.error().what() << std::endl; + } + } +} +BENCHMARK(BM_canada_read_reflect_cpp_cereal); + static void BM_canada_read_reflect_cpp_flexbuf(benchmark::State &state) { const auto data = rfl::flexbuf::write(load_data()); for (auto _ : state) { diff --git a/benchmarks/all/canada_write.cpp b/benchmarks/all/canada_write.cpp index 80a817b07..3b7005c0f 100644 --- a/benchmarks/all/canada_write.cpp +++ b/benchmarks/all/canada_write.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -103,6 +104,17 @@ static void BM_canada_write_reflect_cpp_cbor_without_field_names( } BENCHMARK(BM_canada_write_reflect_cpp_cbor_without_field_names); +static void BM_canada_write_reflect_cpp_cereal(benchmark::State &state) { + const auto data = load_data(); + for (auto _ : state) { + const auto output = rfl::cereal::write(data); + if (output.size() == 0) { + std::cout << "No output" << std::endl; + } + } +} +BENCHMARK(BM_canada_write_reflect_cpp_cereal); + static void BM_canada_write_reflect_cpp_flexbuf_without_field_names( benchmark::State &state) { const auto data = load_data(); diff --git a/benchmarks/all/licenses_read.cpp b/benchmarks/all/licenses_read.cpp index 2bbb3a7a1..289582520 100644 --- a/benchmarks/all/licenses_read.cpp +++ b/benchmarks/all/licenses_read.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -120,6 +121,17 @@ static void BM_licenses_read_reflect_cpp_cbor_without_field_names( } BENCHMARK(BM_licenses_read_reflect_cpp_cbor_without_field_names); +static void BM_licenses_read_reflect_cpp_cereal(benchmark::State &state) { + const auto data = rfl::cereal::write(load_data()); + for (auto _ : state) { + const auto res = rfl::cereal::read(data); + if (!res) { + std::cout << res.error().what() << std::endl; + } + } +} +BENCHMARK(BM_licenses_read_reflect_cpp_cereal); + static void BM_licenses_read_reflect_cpp_flexbuf(benchmark::State &state) { const auto data = rfl::flexbuf::write(load_data()); for (auto _ : state) { diff --git a/benchmarks/all/licenses_write.cpp b/benchmarks/all/licenses_write.cpp index db9f9a87a..74e74746e 100644 --- a/benchmarks/all/licenses_write.cpp +++ b/benchmarks/all/licenses_write.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -120,6 +121,17 @@ static void BM_licenses_write_reflect_cpp_cbor_without_field_names( } BENCHMARK(BM_licenses_write_reflect_cpp_cbor_without_field_names); +static void BM_licenses_write_reflect_cpp_cereal(benchmark::State &state) { + const auto data = load_data(); + for (auto _ : state) { + const auto output = rfl::cereal::write(data); + if (output.size() == 0) { + std::cout << "No output" << std::endl; + } + } +} +BENCHMARK(BM_licenses_write_reflect_cpp_cereal); + static void BM_licenses_write_reflect_cpp_flexbuf(benchmark::State &state) { const auto data = load_data(); for (auto _ : state) { diff --git a/benchmarks/all/person_read.cpp b/benchmarks/all/person_read.cpp index 9db2e769b..80e79d263 100644 --- a/benchmarks/all/person_read.cpp +++ b/benchmarks/all/person_read.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -98,6 +99,17 @@ static void BM_person_read_reflect_cpp_cbor_without_field_names( } BENCHMARK(BM_person_read_reflect_cpp_cbor_without_field_names); +static void BM_person_read_reflect_cpp_cereal(benchmark::State &state) { + const auto data = rfl::cereal::write(load_data()); + for (auto _ : state) { + const auto res = rfl::cereal::read(data); + if (!res) { + std::cout << res.error().what() << std::endl; + } + } +} +BENCHMARK(BM_person_read_reflect_cpp_cereal); + static void BM_person_read_reflect_cpp_flexbuf(benchmark::State &state) { const auto data = rfl::flexbuf::write(load_data()); for (auto _ : state) { diff --git a/benchmarks/all/person_write.cpp b/benchmarks/all/person_write.cpp index 02fafda8d..3e2f51311 100644 --- a/benchmarks/all/person_write.cpp +++ b/benchmarks/all/person_write.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -99,6 +100,17 @@ static void BM_person_write_reflect_cpp_cbor_without_field_names( } BENCHMARK(BM_person_write_reflect_cpp_cbor_without_field_names); +static void BM_person_write_reflect_cpp_cereal(benchmark::State &state) { + const auto data = load_data(); + for (auto _ : state) { + const auto output = rfl::cereal::write(data); + if (output.size() == 0) { + std::cout << "No output" << std::endl; + } + } +} +BENCHMARK(BM_person_write_reflect_cpp_cereal); + static void BM_person_write_reflect_cpp_flexbuf(benchmark::State &state) { const auto data = load_data(); for (auto _ : state) { diff --git a/conanfile.py b/conanfile.py index a085b7b64..ed8660d7f 100644 --- a/conanfile.py +++ b/conanfile.py @@ -40,6 +40,7 @@ class ReflectCppConan(ConanFile): "fPIC": [True, False], "with_capnproto": [True, False], "with_cbor": [True, False], + "with_cereal": [True, False], "with_csv": [True, False], "with_flatbuffers": [True, False], "with_msgpack": [True, False], @@ -54,6 +55,7 @@ class ReflectCppConan(ConanFile): "fPIC": True, "with_capnproto": False, "with_cbor": False, + "with_cereal": False, "with_csv": False, "with_flatbuffers": False, "with_msgpack": False, @@ -79,6 +81,8 @@ def requirements(self): self.requires("capnproto/1.1.0", transitive_headers=True) if self.options.with_cbor or self.options.with_ubjson: self.requires("jsoncons/0.176.0", transitive_headers=True) + if self.options.with_cereal: + self.requires("cereal/1.3.2", transitive_headers=True) if self.options.with_csv or self.options.with_parquet: self.requires("arrow/21.0.0", transitive_headers=True) if self.options.with_flatbuffers: @@ -116,6 +120,7 @@ def generate(self): tc.cache_variables["REFLECTCPP_USE_VCPKG"] = False tc.cache_variables["REFLECTCPP_CAPNPROTO"] = self.options.with_capnproto tc.cache_variables["REFLECTCPP_CBOR"] = self.options.with_cbor + tc.cache_variables["REFLECTCPP_CEREAL"] = self.options.with_cereal tc.cache_variables["REFLECTCPP_CSV"] = self.options.with_csv tc.cache_variables["REFLECTCPP_FLEXBUFFERS"] = self.options.with_flatbuffers tc.cache_variables["REFLECTCPP_MSGPACK"] = self.options.with_msgpack diff --git a/docs/supported_formats/cereal.md b/docs/supported_formats/cereal.md new file mode 100644 index 000000000..e45d21fc9 --- /dev/null +++ b/docs/supported_formats/cereal.md @@ -0,0 +1,101 @@ +# Cereal + +For Cereal support, you must also include the header `` and link to the [Cereal](https://uscilab.github.io/cereal/) library. +Furthermore, when compiling reflect-cpp, you need to pass `-DREFLECTCPP_CEREAL=ON` to cmake. + +Cereal is a C++ serialization library that provides multiple archive formats including binary, portable binary, JSON, and XML formats, even though we use the portable binary format. + +## Reading and writing + +Suppose you have a struct like this: + +```cpp +struct Person { + std::string first_name; + std::string last_name; + rfl::Timestamp<"%Y-%m-%d"> birthday; + std::vector children; +}; +``` + +A `Person` struct can be serialized to a bytes vector like this: + +```cpp +const auto person = Person{...}; +const std::vector bytes = rfl::cereal::write(person); +``` + +You can parse bytes like this: + +```cpp +const rfl::Result result = rfl::cereal::read(bytes); +``` + +## Loading and saving + +You can also load and save to disc using a very similar syntax: + +```cpp +const rfl::Result result = rfl::cereal::load("/path/to/file.cereal"); + +const auto person = Person{...}; +rfl::cereal::save("/path/to/file.cereal", person); +``` + +## Reading from and writing into streams + +You can also read from and write into any `std::istream` and `std::ostream` respectively. + +```cpp +const rfl::Result result = rfl::cereal::read(my_istream); + +const auto person = Person{...}; +rfl::cereal::write(person, my_ostream); +``` + +Note that `std::cout` is also an ostream, so this works as well: + +```cpp +rfl::cereal::write(person, std::cout) << std::endl; +``` + +(Since Cereal binary format is a binary format, the readability of this will be limited, but it might be useful for debugging). + +## Custom constructors + +One of the great things about C++ is that it gives you control over +when and how you code is compiled. + +For large and complex systems of structs, it is often a good idea to split up +your code into smaller compilation units. You can do so using custom constructors. + +For the Cereal format, these must be a static function on your struct or class called +`from_cereal_obj` that takes a `rfl::cereal::Reader::InputVarType` as input and return +the class or the class wrapped in `rfl::Result`. + +In your header file you can write something like this: + +```cpp +struct Person { + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name; + rfl::Timestamp<"%Y-%m-%d"> birthday; + + using InputVarType = rfl::cereal::Reader::InputVarType; + static rfl::Result from_cereal_obj(const InputVarType& _obj); +}; +``` + +And in your source file, you implement `from_cereal_obj` as follows: + +```cpp +rfl::Result Person::from_cereal_obj(const InputVarType& _obj) { + const auto from_nt = [](auto&& _nt) { + return rfl::from_named_tuple(std::move(_nt)); + }; + return rfl::cereal::read>(*_obj.archive_) + .transform(from_nt); +} +``` + +This will force the compiler to only compile the Cereal parsing when the source file is compiled. diff --git a/include/rfl/cereal.hpp b/include/rfl/cereal.hpp new file mode 100644 index 000000000..f4c3bc62e --- /dev/null +++ b/include/rfl/cereal.hpp @@ -0,0 +1,13 @@ +#ifndef RFL_CEREAL_HPP_ +#define RFL_CEREAL_HPP_ + +#include "../rfl.hpp" +#include "cereal/Parser.hpp" +#include "cereal/Reader.hpp" +#include "cereal/Writer.hpp" +#include "cereal/load.hpp" +#include "cereal/read.hpp" +#include "cereal/save.hpp" +#include "cereal/write.hpp" + +#endif diff --git a/include/rfl/cereal/Parser.hpp b/include/rfl/cereal/Parser.hpp new file mode 100644 index 000000000..9b7cd0a24 --- /dev/null +++ b/include/rfl/cereal/Parser.hpp @@ -0,0 +1,54 @@ +#ifndef RFL_CEREAL_PARSER_HPP_ +#define RFL_CEREAL_PARSER_HPP_ + +#include "../parsing/Parser.hpp" +#include "Reader.hpp" +#include "Writer.hpp" + +namespace rfl { +namespace parsing { + +/// Cereal requires us to explicitly set the number of fields in advance. +/// Because of that, we require all of the fields and then set them to nullptr, +/// if necessary. +template + requires AreReaderAndWriter> +struct Parser, + ProcessorsType> + : public NamedTupleParser< + cereal::Reader, cereal::Writer, + /*_ignore_empty_containers=*/false, + /*_all_required=*/true, + /*_no_field_names=*/ProcessorsType::no_field_names_, ProcessorsType, + FieldTypes...> {}; + +template + requires AreReaderAndWriter> +struct Parser, ProcessorsType> + : public TupleParser> {}; + +template + requires AreReaderAndWriter> +struct Parser, ProcessorsType> + : public TupleParser> {}; + +} // namespace parsing +} // namespace rfl + +namespace rfl { +namespace cereal { + +template +using Parser = parsing::Parser; + +} +} // namespace rfl + +#endif diff --git a/include/rfl/cereal/Reader.hpp b/include/rfl/cereal/Reader.hpp new file mode 100644 index 000000000..dd8420a3a --- /dev/null +++ b/include/rfl/cereal/Reader.hpp @@ -0,0 +1,167 @@ +#ifndef RFL_CEREAL_READER_HPP_ +#define RFL_CEREAL_READER_HPP_ + +#include +#include +#include +#include +#include +#include +#include + +#include "../Bytestring.hpp" +#include "../Result.hpp" +#include "../Vectorstring.hpp" +#include "../always_false.hpp" +#include "../internal/is_literal.hpp" +#include "../parsing/schemaful/IsSchemafulReader.hpp" + +namespace rfl::cereal { + +struct Reader { + using CerealArchive = ::cereal::PortableBinaryInputArchive; + + struct CerealInputVar { + CerealArchive* archive_; + }; + + struct InputVarType { + CerealArchive* archive_; + }; + + struct InputArrayType { + CerealArchive* archive_; + }; + + struct InputObjectType { + CerealArchive* archive_; + }; + + struct InputMapType { + CerealArchive* archive_; + }; + + struct InputUnionType { + CerealArchive* archive_; + }; + + template + static constexpr bool has_custom_constructor = + (requires(InputVarType var) { T::from_cereal_obj(var); }); + + bool is_empty(const InputVarType& _var) const noexcept; + + template + rfl::Result to_basic_type(const InputVarType& _var) const noexcept { + try { + if constexpr (internal::is_literal_v) { + std::string str; + (*_var.archive_)(str); + return std::remove_cvref_t::from_string(str); + + } else if constexpr (std::is_same, + rfl::Bytestring>() || + std::is_same, + rfl::Vectorstring>()) { + size_t size; + (*_var.archive_)(::cereal::make_size_tag(size)); + std::remove_cvref_t result(size); + (*_var.archive_)(::cereal::binary_data(result.data(), size)); + return result; + + } else { + T value; + (*_var.archive_)(value); + return value; + } + } catch (std::exception& e) { + return error(std::string("Cereal read error: ") + e.what()); + } + } + + rfl::Result to_array( + const InputVarType& _var) const noexcept; + + rfl::Result to_object( + const InputVarType& _var) const noexcept; + + rfl::Result to_map(const InputVarType& _var) const noexcept; + + rfl::Result to_union( + const InputVarType& _var) const noexcept; + + template + std::optional read_array(const ArrayReader& _array_reader, + const InputArrayType& _arr) const noexcept { + try { + size_t size; + (*_arr.archive_)(::cereal::make_size_tag(size)); + for (size_t i = 0; i < size; ++i) { + const auto err = _array_reader.read(InputVarType{_arr.archive_}); + if (err) { + return err; + } + } + return std::nullopt; + } catch (std::exception& e) { + return Error(std::string("Cereal array read error: ") + e.what()); + } + } + + template + std::optional read_map(const MapReader& _map_reader, + const InputMapType& _map) const noexcept { + try { + size_t size; + (*_map.archive_)(::cereal::make_size_tag(size)); + for (size_t i = 0; i < size; ++i) { + std::string key; + (*_map.archive_)(key); + _map_reader.read(std::string_view(key), InputVarType{_map.archive_}); + } + return std::nullopt; + } catch (std::exception& e) { + return Error(std::string("Cereal map read error: ") + e.what()); + } + } + + template + std::optional read_object(const ObjectReader& _object_reader, + const InputObjectType& _obj) const noexcept { + try { + for (size_t i = 0; i < ObjectReader::size(); ++i) { + _object_reader.read(i, InputVarType{_obj.archive_}); + } + return std::nullopt; + } catch (std::exception& e) { + return Error(std::string("Cereal object read error: ") + e.what()); + } + } + + template + rfl::Result read_union(const InputUnionType& _union) const noexcept { + try { + size_t index; + (*_union.archive_)(index); + return UnionReader::read(*this, index, InputVarType{_union.archive_}); + } catch (std::exception& e) { + return error(std::string("Cereal union read error: ") + e.what()); + } + } + + template + rfl::Result use_custom_constructor( + const InputVarType& _var) const noexcept { + try { + return T::from_cereal_obj(_var); + } catch (std::exception& e) { + return error(e.what()); + } + } +}; + +static_assert(parsing::schemaful::IsSchemafulReader); + +} // namespace rfl::cereal + +#endif diff --git a/include/rfl/cereal/Writer.hpp b/include/rfl/cereal/Writer.hpp new file mode 100644 index 000000000..84c3d4a0a --- /dev/null +++ b/include/rfl/cereal/Writer.hpp @@ -0,0 +1,187 @@ +#ifndef RFL_CEREAL_WRITER_HPP_ +#define RFL_CEREAL_WRITER_HPP_ + +#include +#include +#include +#include +#include +#include +#include + +#include "../Bytestring.hpp" +#include "../Vectorstring.hpp" +#include "../always_false.hpp" +#include "../common.hpp" +#include "../internal/is_literal.hpp" +#include "../parsing/schemaful/IsSchemafulWriter.hpp" + +namespace rfl::cereal { + +class Writer { + public: + using CerealArchive = ::cereal::PortableBinaryOutputArchive; + + struct CerealOutputArray {}; + + struct CerealOutputMap {}; + + struct CerealOutputObject {}; + + struct CerealOutputUnion {}; + + struct CerealOutputVar {}; + + using OutputArrayType = CerealOutputArray; + using OutputObjectType = CerealOutputObject; + using OutputMapType = CerealOutputMap; + using OutputUnionType = CerealOutputUnion; + using OutputVarType = CerealOutputVar; + + Writer(CerealArchive* _archive); + + ~Writer() = default; + + OutputArrayType array_as_root(const size_t _size) const; + + OutputMapType map_as_root(const size_t _size) const; + + OutputObjectType object_as_root(const size_t _size) const; + + OutputUnionType union_as_root() const; + + OutputVarType null_as_root() const; + + template + OutputVarType value_as_root(const T& _var) const { + (*archive_)(_var); + return OutputVarType{}; + } + + OutputArrayType add_array_to_array(const size_t _size, + OutputArrayType* _parent) const; + + OutputArrayType add_array_to_map(const std::string_view& _name, + const size_t _size, + OutputMapType* _parent) const; + + OutputArrayType add_array_to_object(const std::string_view& _name, + const size_t _size, + OutputObjectType* _parent) const; + + OutputArrayType add_array_to_union(const size_t _index, const size_t _size, + OutputUnionType* _parent) const; + + OutputMapType add_map_to_array(const size_t _size, + OutputArrayType* _parent) const; + + OutputMapType add_map_to_map(const std::string_view& _name, + const size_t _size, + OutputMapType* _parent) const; + + OutputMapType add_map_to_object(const std::string_view& _name, + const size_t _size, + OutputObjectType* _parent) const; + + OutputMapType add_map_to_union(const size_t _index, const size_t _size, + OutputUnionType* _parent) const; + + OutputObjectType add_object_to_array(const size_t _size, + OutputArrayType* _parent) const; + + OutputObjectType add_object_to_map(const std::string_view& _name, + const size_t _size, + OutputMapType* _parent) const; + + OutputObjectType add_object_to_object(const std::string_view& _name, + const size_t _size, + OutputObjectType* _parent) const; + + OutputObjectType add_object_to_union(const size_t _index, const size_t _size, + OutputUnionType* _parent) const; + + OutputUnionType add_union_to_array(OutputArrayType* _parent) const; + + OutputUnionType add_union_to_map(const std::string_view& _name, + OutputMapType* _parent) const; + + OutputUnionType add_union_to_object(const std::string_view& _name, + OutputObjectType* _parent) const; + + OutputUnionType add_union_to_union(const size_t _index, + OutputUnionType* _parent) const; + + template + OutputVarType add_value_to_array(const T& _var, OutputArrayType*) const { + add_value(_var); + return OutputVarType{}; + } + + template + OutputVarType add_value_to_map(const std::string_view& _name, const T& _var, + OutputMapType* _parent) const { + add_string_view(_name); + add_value(_var); + return OutputVarType{}; + } + + template + OutputVarType add_value_to_object(const std::string_view& _name, + const T& _var, OutputObjectType*) const { + add_value(_var); + return OutputVarType{}; + } + + template + OutputVarType add_value_to_union(const size_t _index, const T& _var, + OutputUnionType* _parent) const { + (*archive_)(_index); + add_value(_var); + return OutputVarType{}; + } + + OutputVarType add_null_to_array(OutputArrayType* _parent) const; + + OutputVarType add_null_to_map(const std::string_view& _name, + OutputMapType* _parent) const; + + OutputVarType add_null_to_object(const std::string_view& _name, + OutputObjectType* _parent) const; + + OutputVarType add_null_to_union(const size_t _index, + OutputUnionType* _parent) const; + + void end_array(OutputArrayType* _arr) const noexcept; + + void end_map(OutputMapType* _map) const noexcept; + + void end_object(OutputObjectType* _obj) const noexcept; + + private: + void add_string_view(const std::string_view& _str) const; + + template + void add_value(const T& _var) const noexcept { + using Type = std::remove_cvref_t; + if constexpr (internal::is_literal_v) { + add_value(_var.str()); + + } else if constexpr (std::is_same() || + std::is_same()) { + (*archive_)(::cereal::make_size_tag(_var.size())); + (*archive_)(::cereal::binary_data(_var.data(), _var.size())); + + } else { + (*archive_)(_var); + } + } + + private: + CerealArchive* archive_; +}; + +static_assert(parsing::schemaful::IsSchemafulWriter); + +} // namespace rfl::cereal + +#endif diff --git a/include/rfl/cereal/load.hpp b/include/rfl/cereal/load.hpp new file mode 100644 index 000000000..c026375ec --- /dev/null +++ b/include/rfl/cereal/load.hpp @@ -0,0 +1,22 @@ +#ifndef RFL_CEREAL_LOAD_HPP_ +#define RFL_CEREAL_LOAD_HPP_ + +#include "../Result.hpp" +#include "../io/load_bytes.hpp" +#include "read.hpp" + +namespace rfl { +namespace cereal { + +template +Result load(const std::string& _fname) { + const auto read_bytes = [](const auto& _bytes) { + return read(_bytes); + }; + return rfl::io::load_bytes(_fname).and_then(read_bytes); +} + +} // namespace cereal +} // namespace rfl + +#endif diff --git a/include/rfl/cereal/read.hpp b/include/rfl/cereal/read.hpp new file mode 100644 index 000000000..ceca8406e --- /dev/null +++ b/include/rfl/cereal/read.hpp @@ -0,0 +1,61 @@ +#ifndef RFL_CEREAL_READ_HPP_ +#define RFL_CEREAL_READ_HPP_ + +#include +#include +#include +#include + +#include "../Processors.hpp" +#include "../concepts.hpp" +#include "../internal/wrap_in_rfl_array_t.hpp" +#include "Parser.hpp" +#include "Reader.hpp" + +namespace rfl::cereal { + +using InputVarType = Reader::InputVarType; + +/// Parses an object from a Cereal InputArchive. +template +auto read(Reader::CerealArchive& _archive) { + const auto r = Reader(); + auto var = InputVarType{&_archive}; + return Parser>::read(r, var); +} + +/// Parses an object from Cereal binary format using reflection. +template +Result> read( + const concepts::ByteLike auto* _bytes, const size_t _size) { + try { + std::stringstream ss( + std::string(reinterpret_cast(_bytes), _size)); + ::cereal::PortableBinaryInputArchive archive(ss); + return read(archive); + } catch (std::exception& e) { + return error(std::string("Cereal read error: ") + e.what()); + } +} + +/// Parses an object from Cereal binary format using reflection. +template +auto read(const concepts::ContiguousByteContainer auto& _bytes) { + return read(_bytes.data(), _bytes.size()); +} + +/// Parses an object from a stream using Cereal binary format. +template +auto read(std::istream& _stream) { + try { + ::cereal::PortableBinaryInputArchive archive(_stream); + return read(archive); + } catch (std::exception& e) { + return Result>( + error(std::string("Cereal read error: ") + e.what())); + } +} + +} // namespace rfl::cereal + +#endif diff --git a/include/rfl/cereal/save.hpp b/include/rfl/cereal/save.hpp new file mode 100644 index 000000000..09a9f6628 --- /dev/null +++ b/include/rfl/cereal/save.hpp @@ -0,0 +1,24 @@ +#ifndef RFL_CEREAL_SAVE_HPP_ +#define RFL_CEREAL_SAVE_HPP_ + +#include + +#include "../Result.hpp" +#include "../io/save_bytes.hpp" +#include "write.hpp" + +namespace rfl { +namespace cereal { + +template +Result save(const std::string& _fname, const auto& _obj) { + const auto write_func = [](const auto& _obj, auto& _stream) -> auto& { + return write(_obj, _stream); + }; + return rfl::io::save_bytes(_fname, _obj, write_func); +} + +} // namespace cereal +} // namespace rfl + +#endif diff --git a/include/rfl/cereal/write.hpp b/include/rfl/cereal/write.hpp new file mode 100644 index 000000000..d219031ee --- /dev/null +++ b/include/rfl/cereal/write.hpp @@ -0,0 +1,45 @@ +#ifndef RFL_CEREAL_WRITE_HPP_ +#define RFL_CEREAL_WRITE_HPP_ + +#include +#include + +#include "../Processors.hpp" +#include "../Result.hpp" +#include "../parsing/Parent.hpp" +#include "Parser.hpp" +#include "Writer.hpp" + +namespace rfl::cereal { + +/// Writes an object to a Cereal OutputArchive. +template +void write(const T& _obj, Writer::CerealArchive& _archive) { + using ParentType = parsing::Parent; + auto w = Writer(&_archive); + Parser>::write(w, _obj, typename ParentType::Root{}); +} + +/// Returns Cereal binary bytes. +template +std::vector write(const auto& _obj) { + std::stringstream ss; + { + ::cereal::PortableBinaryOutputArchive archive(ss); + write, Ps...>(_obj, archive); + } + auto str = ss.str(); + return std::vector(str.begin(), str.end()); +} + +/// Writes Cereal binary format into an ostream. +template +std::ostream& write(const auto& _obj, std::ostream& _stream) { + ::cereal::PortableBinaryOutputArchive archive(_stream); + write, Ps...>(_obj, archive); + return _stream; +} + +} // namespace rfl::cereal + +#endif diff --git a/include/rfl/internal/bind_to_tuple.hpp b/include/rfl/internal/bind_to_tuple.hpp index e2b28b27e..2eab5b357 100644 --- a/include/rfl/internal/bind_to_tuple.hpp +++ b/include/rfl/internal/bind_to_tuple.hpp @@ -7,7 +7,6 @@ #include "../Tuple.hpp" #include "../always_false.hpp" -// #include "is_named_tuple.hpp" //Not here #include "num_fields.hpp" namespace rfl::internal { diff --git a/include/rfl/parsing/MapParser.hpp b/include/rfl/parsing/MapParser.hpp index 03432c9a6..33f21b464 100644 --- a/include/rfl/parsing/MapParser.hpp +++ b/include/rfl/parsing/MapParser.hpp @@ -61,7 +61,7 @@ struct MapParser { private: static Result make_map(const R& _r, const auto& _obj_or_map) { MapType map; - std::vector errors; + std::vector errors; const auto map_reader = MapReader(&_r, &map, &errors); if constexpr (schemaful::IsSchemafulReader) { diff --git a/include/rfl/parsing/MapReader.hpp b/include/rfl/parsing/MapReader.hpp index 6e932a613..3301066a4 100644 --- a/include/rfl/parsing/MapReader.hpp +++ b/include/rfl/parsing/MapReader.hpp @@ -23,7 +23,7 @@ class MapReader { std::remove_cvref_t; public: - MapReader(const R* _r, MapType* _map, std::vector* _errors) + MapReader(const R* _r, MapType* _map, std::vector* _errors) : r_(_r), map_(_map), errors_(_errors) {} ~MapReader() = default; @@ -34,8 +34,8 @@ class MapReader { if (res) { map_->emplace(std::move(*res)); } else { - errors_->push_back(Error("Failed to parse field '" + std::string(_name) + - "': " + res.error().what())); + errors_->push_back("Failed to parse field '" + std::string(_name) + + "': " + res.error().what()); } } @@ -109,7 +109,7 @@ class MapReader { MapType* map_; /// Collects any errors we may have come across. - std::vector* errors_; + std::vector* errors_; }; } // namespace rfl::parsing diff --git a/include/rfl/parsing/NamedTupleParser.hpp b/include/rfl/parsing/NamedTupleParser.hpp index 7f423e04e..d31d39414 100644 --- a/include/rfl/parsing/NamedTupleParser.hpp +++ b/include/rfl/parsing/NamedTupleParser.hpp @@ -245,10 +245,10 @@ struct NamedTupleParser { /// Generates error messages for when fields are missing. template - static void handle_one_missing_field(const std::array& _found, - const NamedTupleType& _view, - std::array* _set, - std::vector* _errors) noexcept { + static void handle_one_missing_field( + const std::array& _found, const NamedTupleType& _view, + std::array* _set, + std::vector* _errors) noexcept { using FieldType = internal::nth_element_t<_i, FieldTypes...>; using ValueType = std::remove_reference_t< std::remove_pointer_t>; @@ -265,7 +265,7 @@ struct NamedTupleParser { std::stringstream stream; stream << "Field named '" << std::string(current_name) << "' not found."; - _errors->emplace_back(Error(stream.str())); + _errors->emplace_back(stream.str()); } else if constexpr (!internal::has_default_val_v) { if constexpr (!std::is_const_v) { @@ -283,7 +283,7 @@ struct NamedTupleParser { template static void handle_missing_fields( const std::array& _found, const NamedTupleType& _view, - std::array* _set, std::vector* _errors, + std::array* _set, std::vector* _errors, std::integer_sequence) noexcept { (handle_one_missing_field<_is>(_found, _view, _set, _errors), ...); } @@ -305,7 +305,8 @@ struct NamedTupleParser { found.fill(false); auto set = std::array(); set.fill(false); - std::vector errors; + std::vector errors; + errors.reserve(size_); const auto reader = ViewReaderType(&_r, _view, &found, &set, &errors); if constexpr (_no_field_names) { const auto err = _r.read_array(reader, _obj_or_arr); @@ -329,7 +330,8 @@ struct NamedTupleParser { static std::optional read_object_or_array_with_default( const R& _r, const InputObjectOrArrayType& _obj_or_arr, NamedTupleType* _view) noexcept { - std::vector errors; + std::vector errors; + errors.reserve(size_); const auto reader = ViewReaderWithDefaultType(&_r, _view, &errors); if constexpr (_no_field_names) { const auto err = _r.read_array(reader, _obj_or_arr); diff --git a/include/rfl/parsing/Parser_rfl_variant.hpp b/include/rfl/parsing/Parser_rfl_variant.hpp index 8c5dd2f5b..63f598d55 100644 --- a/include/rfl/parsing/Parser_rfl_variant.hpp +++ b/include/rfl/parsing/Parser_rfl_variant.hpp @@ -71,7 +71,7 @@ class Parser, ProcessorsType> { } else { std::optional> result; - std::vector errors; + std::vector errors; errors.reserve(sizeof...(AlternativeTypes)); read_variant( _r, _var, &result, &errors, @@ -189,7 +189,7 @@ class Parser, ProcessorsType> { static void read_one_alternative( const R& _r, const InputVarType& _var, std::optional>* _result, - std::vector* _errors) noexcept { + std::vector* _errors) noexcept { if (!*_result) { using AltType = std::remove_cvref_t>; @@ -197,7 +197,7 @@ class Parser, ProcessorsType> { if (res) { *_result = std::move(*res); } else { - _errors->emplace_back(std::move(res.error())); + _errors->emplace_back(std::move(res.error().what())); } } } @@ -206,7 +206,7 @@ class Parser, ProcessorsType> { static void read_variant( const R& _r, const InputVarType& _var, std::optional>* _result, - std::vector* _errors, + std::vector* _errors, std::integer_sequence) noexcept { (read_one_alternative<_is>(_r, _var, _result, _errors), ...); } diff --git a/include/rfl/parsing/Parser_variant.hpp b/include/rfl/parsing/Parser_variant.hpp index 55b1da69f..0820858b1 100644 --- a/include/rfl/parsing/Parser_variant.hpp +++ b/include/rfl/parsing/Parser_variant.hpp @@ -87,7 +87,7 @@ class Parser, ProcessorsType> { } else { std::optional> result; - std::vector errors; + std::vector errors; errors.reserve(sizeof...(AlternativeTypes)); read_variant( _r, _var, &result, &errors, @@ -216,7 +216,7 @@ class Parser, ProcessorsType> { static void read_one_alternative( const R& _r, const InputVarType& _var, std::optional>* _result, - std::vector* _errors) noexcept { + std::vector* _errors) noexcept { if (!*_result) { using AltType = std::remove_cvref_t>; @@ -224,7 +224,7 @@ class Parser, ProcessorsType> { if (res) { _result->emplace(std::move(*res)); } else { - _errors->emplace_back(res.error()); + _errors->emplace_back(res.error().what()); } } } @@ -233,7 +233,7 @@ class Parser, ProcessorsType> { static void read_variant( const R& _r, const InputVarType& _var, std::optional>* _result, - std::vector* _errors, + std::vector* _errors, std::integer_sequence) noexcept { (read_one_alternative<_is>(_r, _var, _result, _errors), ...); } diff --git a/include/rfl/parsing/ViewReader.hpp b/include/rfl/parsing/ViewReader.hpp index 8bdbfbdd4..990fa8a22 100644 --- a/include/rfl/parsing/ViewReader.hpp +++ b/include/rfl/parsing/ViewReader.hpp @@ -24,7 +24,7 @@ class ViewReader { public: ViewReader(const R* _r, ViewType* _view, std::array* _found, - std::array* _set, std::vector* _errors) + std::array* _set, std::vector* _errors) : r_(_r), view_(_view), found_(_found), set_(_set), errors_(_errors) {} ~ViewReader() = default; @@ -44,6 +44,8 @@ class ViewReader { std::make_integer_sequence()); } + static constexpr size_t size() { return size_; } + private: template static bool is_matching(const int _current_index) { @@ -65,17 +67,17 @@ class ViewReader { using OriginalType = typename FieldType::Type; using T = std::remove_cvref_t>; - constexpr auto name = FieldType::name(); if (!(*_already_assigned) && !std::get(*_found) && is_matching(_current_name_or_index)) { std::get(*_found) = true; *_already_assigned = true; auto res = Parser::read(_r, _var); if (!res) { + constexpr auto name = FieldType::name(); std::stringstream stream; - stream << "Failed to parse field '" << std::string(name) + stream << "Failed to parse field '" << name << "': " << res.error().what(); - _errors->emplace_back(Error(stream.str())); + _errors->emplace_back(stream.str()); return; } if constexpr (std::is_pointer_v) { @@ -107,7 +109,7 @@ class ViewReader { std::stringstream stream; stream << "Failed to parse field '" << _current_name << "': " << res.error().what(); - _errors->emplace_back(Error(stream.str())); + _errors->emplace_back(stream.str()); return; } extra_fields->emplace(std::string(_current_name), std::move(*res)); @@ -141,9 +143,8 @@ class ViewReader { "sense, because schemaful formats cannot have extra fields."); if (!already_assigned) { std::stringstream stream; - stream << "Value named '" << _current_name_or_index - << "' not used."; - _errors->emplace_back(Error(stream.str())); + stream << "Value named '" << _current_name_or_index << "' not used."; + _errors->emplace_back(stream.str()); } } } @@ -183,7 +184,7 @@ class ViewReader { std::array* set_; /// Collects any errors we may have come across. - std::vector* errors_; + std::vector* errors_; }; } // namespace rfl::parsing diff --git a/include/rfl/parsing/ViewReaderWithDefault.hpp b/include/rfl/parsing/ViewReaderWithDefault.hpp index 8ee2d79ed..fa7d10021 100644 --- a/include/rfl/parsing/ViewReaderWithDefault.hpp +++ b/include/rfl/parsing/ViewReaderWithDefault.hpp @@ -24,7 +24,7 @@ class ViewReaderWithDefault { public: ViewReaderWithDefault(const R* _r, ViewType* _view, - std::vector* _errors) + std::vector* _errors) : r_(_r), view_(_view), errors_(_errors) { found_->fill(false); } @@ -40,6 +40,8 @@ class ViewReaderWithDefault { std::make_integer_sequence()); } + static constexpr size_t size() { return size_; } + private: template static void assign_if_field_matches(const R& _r, @@ -61,7 +63,7 @@ class ViewReaderWithDefault { std::stringstream stream; stream << "Failed to parse field '" << std::string(name) << "': " << res.error().what(); - _errors->emplace_back(Error(stream.str())); + _errors->emplace_back(stream.str()); return; } if constexpr (std::is_pointer_v) { @@ -87,7 +89,7 @@ class ViewReaderWithDefault { std::stringstream stream; stream << "Failed to parse field '" << _current_name << "': " << res.error().what(); - _errors->emplace_back(Error(stream.str())); + _errors->emplace_back(stream.str()); return; } extra_fields->emplace(std::string(_current_name), std::move(*res)); @@ -116,7 +118,7 @@ class ViewReaderWithDefault { stream << "Value named '" << std::string(_current_name) << "' not used. Remove the rfl::NoExtraFields processor or add " "rfl::ExtraFields to avoid this error message."; - _errors->emplace_back(Error(stream.str())); + _errors->emplace_back(stream.str()); } } } @@ -152,7 +154,7 @@ class ViewReaderWithDefault { rfl::Ref> found_; /// Collects any errors we may have come across. - std::vector* errors_; + std::vector* errors_; }; } // namespace rfl::parsing diff --git a/include/rfl/parsing/ViewReaderWithDefaultAndStrippedFieldNames.hpp b/include/rfl/parsing/ViewReaderWithDefaultAndStrippedFieldNames.hpp index 7b12a2d8e..329f57d43 100644 --- a/include/rfl/parsing/ViewReaderWithDefaultAndStrippedFieldNames.hpp +++ b/include/rfl/parsing/ViewReaderWithDefaultAndStrippedFieldNames.hpp @@ -22,7 +22,7 @@ class ViewReaderWithDefaultAndStrippedFieldNames { public: ViewReaderWithDefaultAndStrippedFieldNames(const R* _r, ViewType* _view, - std::vector* _errors) + std::vector* _errors) : i_(0), r_(_r), view_(_view), errors_(_errors) {} ~ViewReaderWithDefaultAndStrippedFieldNames() = default; @@ -49,6 +49,8 @@ class ViewReaderWithDefaultAndStrippedFieldNames { return std::nullopt; } + static constexpr size_t size() { return size_; } + private: template static void assign_if_field_is_field_i(const R& _r, const auto& _var, @@ -64,7 +66,7 @@ class ViewReaderWithDefaultAndStrippedFieldNames { std::stringstream stream; stream << "Failed to parse field '" << std::string(name) << "': " << res.error().what(); - _errors->emplace_back(Error(stream.str())); + _errors->emplace_back(stream.str()); return; } if constexpr (std::is_pointer_v) { @@ -114,7 +116,7 @@ class ViewReaderWithDefaultAndStrippedFieldNames { ViewType* view_; /// Collects any errors we may have come across. - std::vector* errors_; + std::vector* errors_; }; } // namespace rfl::parsing diff --git a/include/rfl/parsing/ViewReaderWithStrippedFieldNames.hpp b/include/rfl/parsing/ViewReaderWithStrippedFieldNames.hpp index 5787a1e0e..2a1da3379 100644 --- a/include/rfl/parsing/ViewReaderWithStrippedFieldNames.hpp +++ b/include/rfl/parsing/ViewReaderWithStrippedFieldNames.hpp @@ -25,7 +25,7 @@ class ViewReaderWithStrippedFieldNames { ViewReaderWithStrippedFieldNames(const R* _r, ViewType* _view, std::array* _found, std::array* _set, - std::vector* _errors) + std::vector* _errors) : i_(0), r_(_r), view_(_view), @@ -50,6 +50,8 @@ class ViewReaderWithStrippedFieldNames { return std::nullopt; } + static constexpr size_t size() { return size_; } + private: template static void assign_if_field_is_field_i(const R& _r, const auto& _var, @@ -67,7 +69,7 @@ class ViewReaderWithStrippedFieldNames { std::stringstream stream; stream << "Failed to parse field '" << std::string(name) << "': " << res.error().what(); - _errors->emplace_back(Error(stream.str())); + _errors->emplace_back(stream.str()); return; } if constexpr (std::is_pointer_v) { @@ -126,7 +128,7 @@ class ViewReaderWithStrippedFieldNames { std::array* set_; /// Collects any errors we may have come across. - std::vector* errors_; + std::vector* errors_; }; } // namespace rfl::parsing diff --git a/include/rfl/parsing/to_single_error_message.hpp b/include/rfl/parsing/to_single_error_message.hpp index 1f0663510..4e7ec6a0f 100644 --- a/include/rfl/parsing/to_single_error_message.hpp +++ b/include/rfl/parsing/to_single_error_message.hpp @@ -13,11 +13,11 @@ namespace rfl::parsing { /// Combines a set of errors to a single, readable error message. inline std::string to_single_error_message( - std::vector _errors, + std::vector _errors, std::optional _msg_prefix = std::nullopt, size_t _err_limit = 10) { if (_errors.size() == 1) { - return _errors[0].what(); + return _errors[0]; } else { std::stringstream stream; stream << (_msg_prefix @@ -26,8 +26,7 @@ inline std::string to_single_error_message( for (size_t i = 0; i < _errors.size() && i < _err_limit; ++i) { stream << "\n" << i + 1 << ") " - << internal::strings::replace_all(_errors.at(i).what(), "\n", - "\n "); + << internal::strings::replace_all(_errors.at(i), "\n", "\n "); } if (_errors.size() > _err_limit) { stream << "\n...\nMore than " << _err_limit diff --git a/mkdocs.yaml b/mkdocs.yaml index 24e7e1fd8..cc55300fa 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -6,7 +6,7 @@ site_url: https://rfl.getml.com/ site_author: Code17 GmbH site_description: >- A C++20 library for fast serialization, deserialization and validation using reflection. - Supports JSON, AVRO, BSON, Cap'n Proto, CBOR, CSV, flexbuffers, msgpack, parquet, TOML, UBJSON, XML, YAML + Supports JSON, AVRO, BSON, Cap'n Proto, CBOR, Cereal, CSV, flexbuffers, msgpack, parquet, TOML, UBJSON, XML, YAML theme: name: "material" @@ -96,6 +96,7 @@ nav: - BSON: supported_formats/bson.md - Cap'n Proto: supported_formats/capnproto.md - CBOR: supported_formats/cbor.md + - Cereal: supported_formats/cereal.md - CSV: supported_formats/csv.md - FlexBuffers: supported_formats/flexbuffers.md - JSON: supported_formats/json.md diff --git a/reflectcpp-config.cmake.in b/reflectcpp-config.cmake.in index 128a89c70..d401db03c 100644 --- a/reflectcpp-config.cmake.in +++ b/reflectcpp-config.cmake.in @@ -4,6 +4,7 @@ set(REFLECTCPP_JSON @REFLECTCPP_JSON@) set(REFLECTCPP_BSON @REFLECTCPP_BSON@) set(REFLECTCPP_CAPNPROTO @REFLECTCPP_CAPNPROTO@) set(REFLECTCPP_CBOR @REFLECTCPP_CBOR@) +set(REFLECTCPP_CEREAL @REFLECTCPP_CEREAL@) set(REFLECTCPP_CSV @REFLECTCPP_CSV@) set(REFLECTCPP_FLEXBUFFERS @REFLECTCPP_FLEXBUFFERS@) set(REFLECTCPP_MSGPACK @REFLECTCPP_MSGPACK@) @@ -14,7 +15,7 @@ set(REFLECTCPP_XML @REFLECTCPP_XML@) set(REFLECTCPP_YAML @REFLECTCPP_YAML@) set(REFLECTCPP_USE_BUNDLED_DEPENDENCIES @REFLECTCPP_USE_BUNDLED_DEPENDENCIES@) -if(REFLECTCPP_BSON OR REFLECTCPP_CAPNPROTO OR REFLECTCPP_CBOR OR REFLECTCPP_FLEXBUFFERS OR REFLECTCPP_MSGPACK OR REFLECTCPP_TOML OR REFLECTCPP_UBJSON OR REFLECTCPP_XML OR REFLECTCPP_YAML OR (REFLECTCPP_JSON AND NOT REFLECTCPP_USE_BUNDLED_DEPENDENCIES)) +if(REFLECTCPP_BSON OR REFLECTCPP_CAPNPROTO OR REFLECTCPP_CBOR OR REFLECTCPP_CEREAL OR REFLECTCPP_FLEXBUFFERS OR REFLECTCPP_MSGPACK OR REFLECTCPP_TOML OR REFLECTCPP_UBJSON OR REFLECTCPP_XML OR REFLECTCPP_YAML OR (REFLECTCPP_JSON AND NOT REFLECTCPP_USE_BUNDLED_DEPENDENCIES)) include(CMakeFindDependencyMacro) endif() @@ -40,6 +41,10 @@ if (REFLECTCPP_CBOR OR REFLECTCPP_UBJSON) find_dependency(jsoncons) endif () +if (REFLECTCPP_CEREAL) + find_dependency(cereal) +endif () + if (REFLECTCPP_CSV) find_dependency(Arrow) endif() diff --git a/src/reflectcpp_cereal.cpp b/src/reflectcpp_cereal.cpp new file mode 100644 index 000000000..2d49d72e8 --- /dev/null +++ b/src/reflectcpp_cereal.cpp @@ -0,0 +1,33 @@ +/* + +MIT License + +Copyright (c) 2023-2024 Code17 GmbH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +// This file include all other source files, so that the user of the library +// don't need to add multiple source files into their build. +// Also, this speeds up compile time, compared to multiple separate .cpp files +// compilation. + +#include "rfl/cereal/Reader.cpp" +#include "rfl/cereal/Writer.cpp" diff --git a/src/rfl/cereal/Reader.cpp b/src/rfl/cereal/Reader.cpp new file mode 100644 index 000000000..e78d5c712 --- /dev/null +++ b/src/rfl/cereal/Reader.cpp @@ -0,0 +1,55 @@ +/* + +MIT License + +Copyright (c) 2023-2024 Code17 GmbH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#include "rfl/cereal/Reader.hpp" + +namespace rfl::cereal { + +bool Reader::is_empty(const InputVarType& _var) const noexcept { + return _var.archive_ == nullptr; +} + +rfl::Result Reader::to_array( + const InputVarType& _var) const noexcept { + return InputArrayType{_var.archive_}; +} + +rfl::Result Reader::to_object( + const InputVarType& _var) const noexcept { + return InputObjectType{_var.archive_}; +} + +rfl::Result Reader::to_map( + const InputVarType& _var) const noexcept { + return InputMapType{_var.archive_}; +} + +rfl::Result Reader::to_union( + const InputVarType& _var) const noexcept { + return InputUnionType{_var.archive_}; +} + +} // namespace rfl::cereal diff --git a/src/rfl/cereal/Writer.cpp b/src/rfl/cereal/Writer.cpp new file mode 100644 index 000000000..7518c650b --- /dev/null +++ b/src/rfl/cereal/Writer.cpp @@ -0,0 +1,189 @@ +/* + +MIT License + +Copyright (c) 2023-2024 Code17 GmbH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#include "rfl/cereal/Writer.hpp" + +namespace rfl::cereal { + +Writer::Writer(CerealArchive* _archive) : archive_(_archive) {} + +Writer::OutputArrayType Writer::array_as_root(const size_t _size) const { + (*archive_)(::cereal::make_size_tag(_size)); + return OutputArrayType{}; +} + +Writer::OutputMapType Writer::map_as_root(const size_t _size) const { + (*archive_)(::cereal::make_size_tag(_size)); + return OutputMapType{}; +} + +Writer::OutputObjectType Writer::object_as_root(const size_t _size) const { + return OutputObjectType{}; +} + +Writer::OutputUnionType Writer::union_as_root() const { + return OutputUnionType{}; +} + +Writer::OutputVarType Writer::null_as_root() const { return OutputVarType{}; } + +Writer::OutputArrayType Writer::add_array_to_array( + const size_t _size, OutputArrayType* _parent) const { + (*archive_)(::cereal::make_size_tag(_size)); + return OutputArrayType{}; +} + +Writer::OutputArrayType Writer::add_array_to_map(const std::string_view& _name, + const size_t _size, + OutputMapType* _parent) const { + add_string_view(_name); + (*archive_)(::cereal::make_size_tag(_size)); + return OutputArrayType{}; +} + +Writer::OutputArrayType Writer::add_array_to_object( + const std::string_view& _name, const size_t _size, + OutputObjectType* _parent) const { + (*archive_)(::cereal::make_size_tag(_size)); + return OutputArrayType{}; +} + +Writer::OutputArrayType Writer::add_array_to_union( + const size_t _index, const size_t _size, OutputUnionType* _parent) const { + (*archive_)(_index); + (*archive_)(::cereal::make_size_tag(_size)); + return OutputArrayType{}; +} + +Writer::OutputMapType Writer::add_map_to_array(const size_t _size, + OutputArrayType* _parent) const { + (*archive_)(::cereal::make_size_tag(_size)); + return OutputMapType{}; +} + +Writer::OutputMapType Writer::add_map_to_map(const std::string_view& _name, + const size_t _size, + OutputMapType* _parent) const { + add_string_view(_name); + (*archive_)(::cereal::make_size_tag(_size)); + return OutputMapType{}; +} + +Writer::OutputMapType Writer::add_map_to_object( + const std::string_view& _name, const size_t _size, + OutputObjectType* _parent) const { + (*archive_)(::cereal::make_size_tag(_size)); + return OutputMapType{}; +} + +Writer::OutputMapType Writer::add_map_to_union(const size_t _index, + const size_t _size, + OutputUnionType* _parent) const { + (*archive_)(_index); + (*archive_)(::cereal::make_size_tag(_size)); + return OutputMapType{}; +} + +Writer::OutputObjectType Writer::add_object_to_array( + const size_t _size, OutputArrayType* _parent) const { + return OutputObjectType{}; +} + +Writer::OutputObjectType Writer::add_object_to_map( + const std::string_view& _name, const size_t _size, + OutputMapType* _parent) const { + add_string_view(_name); + return OutputObjectType{}; +} + +Writer::OutputObjectType Writer::add_object_to_object( + const std::string_view& _name, const size_t _size, + OutputObjectType* _parent) const { + return OutputObjectType{}; +} + +Writer::OutputObjectType Writer::add_object_to_union( + const size_t _index, const size_t _size, OutputUnionType* _parent) const { + (*archive_)(_index); + return OutputObjectType{}; +} + +Writer::OutputUnionType Writer::add_union_to_array( + OutputArrayType* _parent) const { + return OutputUnionType{}; +} + +Writer::OutputUnionType Writer::add_union_to_map(const std::string_view& _name, + OutputMapType* _parent) const { + add_string_view(_name); + return OutputUnionType{}; +} + +Writer::OutputUnionType Writer::add_union_to_object( + const std::string_view& _name, OutputObjectType* _parent) const { + return OutputUnionType{}; +} + +Writer::OutputUnionType Writer::add_union_to_union( + const size_t _index, OutputUnionType* _parent) const { + (*archive_)(_index); + return OutputUnionType{}; +} + +Writer::OutputVarType Writer::add_null_to_array( + OutputArrayType* _parent) const { + return OutputVarType{}; +} + +Writer::OutputVarType Writer::add_null_to_map(const std::string_view& _name, + OutputMapType* _parent) const { + add_string_view(_name); + return OutputVarType{}; +} + +Writer::OutputVarType Writer::add_null_to_object( + const std::string_view& _name, OutputObjectType* _parent) const { + return OutputVarType{}; +} + +Writer::OutputVarType Writer::add_null_to_union( + const size_t _index, OutputUnionType* _parent) const { + (*archive_)(_index); + return OutputVarType{}; +} + +void Writer::end_array(OutputArrayType* _arr) const noexcept {} + +void Writer::end_map(OutputMapType* _map) const noexcept {} + +void Writer::end_object(OutputObjectType* _obj) const noexcept {} + +void Writer::add_string_view(const std::string_view& _str) const { + (*archive_)(::cereal::make_size_tag(_str.size())); + (*archive_)(::cereal::binary_data(_str.data(), _str.size())); +} + +} // namespace rfl::cereal diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9f3c2a908..6401a7392 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,15 +1,15 @@ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -O2") +# Note: Adding -Wno-stringop-overflow is necessary, because of false positive warnings, see https://gcc.gnu.org/bugzilla/show_bug.cgi?id=110498 if (MSVC) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std:c++20") else() - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3 -Wall -Werror -ggdb -ftemplate-backtrace-limit=0") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3 -Wall -Werror -ggdb -ftemplate-backtrace-limit=0") endif() if (REFLECTCPP_JSON) add_subdirectory(generic) add_subdirectory(json) - # add_subdirectory(alloc) # Disabled: tests detect known leaks in Parser_string_view/Parser_span, not yet fixed add_subdirectory(json_c_arrays_and_inheritance) add_subdirectory(cli) endif () @@ -30,6 +30,10 @@ if (REFLECTCPP_CBOR) add_subdirectory(cbor) endif() +if (REFLECTCPP_CEREAL) + add_subdirectory(cereal) +endif() + if (REFLECTCPP_CSV) add_subdirectory(csv) endif() diff --git a/tests/cereal/CMakeLists.txt b/tests/cereal/CMakeLists.txt new file mode 100644 index 000000000..e3b915fb2 --- /dev/null +++ b/tests/cereal/CMakeLists.txt @@ -0,0 +1,19 @@ +project(reflect-cpp-cereal-tests) + +file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "*.cpp") + +add_executable( + reflect-cpp-cereal-tests + ${SOURCES} +) +target_precompile_headers(reflect-cpp-cereal-tests PRIVATE [["rfl.hpp"]] ) + +target_link_libraries(reflect-cpp-cereal-tests PRIVATE reflectcpp_tests_crt) + +add_custom_command(TARGET reflect-cpp-cereal-tests POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy -t $ $ + COMMAND_EXPAND_LISTS +) + +find_package(GTest) +gtest_discover_tests(reflect-cpp-cereal-tests) diff --git a/tests/cereal/test_add_struct_name.cpp b/tests/cereal/test_add_struct_name.cpp new file mode 100644 index 000000000..1f8e5ab13 --- /dev/null +++ b/tests/cereal/test_add_struct_name.cpp @@ -0,0 +1,45 @@ +#include +#include + +#include "write_and_read.hpp" + +namespace test_add_struct_name { + +using Age = rfl::Validator, rfl::Maximum<130>>; + +struct Person { + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name = "Simpson"; + std::string town = "Springfield"; + rfl::Timestamp<"%Y-%m-%d"> birthday; + Age age; + rfl::Email email; + std::vector children; +}; + +TEST(cereal, test_add_struct_name) { + const auto bart = Person{.first_name = "Bart", + .birthday = "1987-04-19", + .age = 10, + .email = "bart@simpson.com"}; + + const auto lisa = Person{.first_name = "Lisa", + .birthday = "1987-04-19", + .age = 8, + .email = "lisa@simpson.com"}; + + const auto maggie = Person{.first_name = "Maggie", + .birthday = "1987-04-19", + .age = 0, + .email = "maggie@simpson.com"}; + + const auto homer = + Person{.first_name = "Homer", + .birthday = "1987-04-19", + .age = 45, + .email = "homer@simpson.com", + .children = std::vector({bart, lisa, maggie})}; + + write_and_read>(homer); +} +} // namespace test_add_struct_name diff --git a/tests/cereal/test_array.cpp b/tests/cereal/test_array.cpp new file mode 100644 index 000000000..81412d435 --- /dev/null +++ b/tests/cereal/test_array.cpp @@ -0,0 +1,32 @@ +#include +#include +#include +#include + +#include + +#include "write_and_read.hpp" + +namespace test_array { + +struct Person { + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name = "Simpson"; + std::unique_ptr> children = nullptr; +}; + +TEST(cereal, test_array) { + auto bart = Person{.first_name = "Bart"}; + + auto lisa = Person{.first_name = "Lisa"}; + + auto maggie = Person{.first_name = "Maggie"}; + + const auto homer = Person{ + .first_name = "Homer", + .children = std::make_unique>(std::array{ + std::move(bart), std::move(lisa), std::move(maggie)})}; + + write_and_read(homer); +} +} // namespace test_array diff --git a/tests/cereal/test_box.cpp b/tests/cereal/test_box.cpp new file mode 100644 index 000000000..06c44cb22 --- /dev/null +++ b/tests/cereal/test_box.cpp @@ -0,0 +1,43 @@ +#include +#include +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_box { + +struct DecisionTree { + struct Leaf { + using Tag = rfl::Literal<"Leaf">; + double value; + }; + + struct Node { + using Tag = rfl::Literal<"Node">; + rfl::Rename<"criticalValue", double> critical_value; + rfl::Box lesser; + rfl::Box greater; + }; + + using LeafOrNode = rfl::TaggedUnion<"type", Leaf, Node>; + + rfl::Field<"leafOrNode", LeafOrNode> leaf_or_node; +}; + +TEST(cereal, test_box) { + auto leaf1 = DecisionTree::Leaf{.value = 3.0}; + + auto leaf2 = DecisionTree::Leaf{.value = 5.0}; + + auto node = DecisionTree::Node{ + .critical_value = 10.0, + .lesser = rfl::make_box(DecisionTree::LeafOrNode(leaf1)), + .greater = rfl::make_box(DecisionTree::LeafOrNode(leaf2))}; + + const DecisionTree tree = DecisionTree{.leaf_or_node = std::move(node)}; + + write_and_read(tree); +} +} // namespace test_box diff --git a/tests/cereal/test_bytestring.cpp b/tests/cereal/test_bytestring.cpp new file mode 100644 index 000000000..8a897ca12 --- /dev/null +++ b/tests/cereal/test_bytestring.cpp @@ -0,0 +1,18 @@ +#include + +#include "write_and_read.hpp" + +namespace test_bytestring { + +struct TestStruct { + rfl::Bytestring bytestring; +}; + +TEST(cereal, test_bytestring) { + const auto test = + TestStruct{.bytestring = rfl::Bytestring({std::byte{13}, std::byte{14}, + std::byte{15}, std::byte{16}})}; + + write_and_read(test); +} +} // namespace test_bytestring diff --git a/tests/cereal/test_custom_class1.cpp b/tests/cereal/test_custom_class1.cpp new file mode 100644 index 000000000..ec1c4aefe --- /dev/null +++ b/tests/cereal/test_custom_class1.cpp @@ -0,0 +1,35 @@ +#include +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_custom_class1 { + +struct Person { + struct PersonImpl { + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name = "Simpson"; + std::vector children; + }; + + using ReflectionType = PersonImpl; + + Person(const PersonImpl& _impl) : impl(_impl) {} + + Person(const std::string& _first_name) + : impl(PersonImpl{.first_name = _first_name}) {} + + const ReflectionType& reflection() const { return impl; }; + + private: + PersonImpl impl; +}; + +TEST(cereal, test_custom_class1) { + const auto bart = Person("Bart"); + + write_and_read(bart); +} +} // namespace test_custom_class1 diff --git a/tests/cereal/test_custom_class3.cpp b/tests/cereal/test_custom_class3.cpp new file mode 100644 index 000000000..1876297d3 --- /dev/null +++ b/tests/cereal/test_custom_class3.cpp @@ -0,0 +1,62 @@ +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_custom_class3 { + +struct Person { + Person(const std::string& _first_name, const std::string& _last_name, + const int _age) + : first_name_(_first_name), last_name_(_last_name), age_(_age) {} + + const auto& first_name() const { return first_name_; } + + const auto& last_name() const { return last_name_; } + + auto age() const { return age_; } + + private: + std::string first_name_; + std::string last_name_; + int age_; +}; + +struct PersonImpl { + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name; + int age; + + static PersonImpl from_class(const Person& _p) noexcept { + return PersonImpl{.first_name = _p.first_name(), + .last_name = _p.last_name(), + .age = _p.age()}; + } + + Person to_class() const { return Person(first_name(), last_name(), age); } +}; +} // namespace test_custom_class3 + +namespace rfl { +namespace parsing { + +template +struct Parser + : public CustomParser {}; + +} // namespace parsing +} // namespace rfl + +namespace test_custom_class3 { + +TEST(cereal, test_custom_class3) { + const auto bart = Person("Bart", "Simpson", 10); + + write_and_read(bart); +} + +} // namespace test_custom_class3 diff --git a/tests/cereal/test_custom_class4.cpp b/tests/cereal/test_custom_class4.cpp new file mode 100644 index 000000000..97ddd04d7 --- /dev/null +++ b/tests/cereal/test_custom_class4.cpp @@ -0,0 +1,68 @@ +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_custom_class4 { + +struct Person { + Person(const std::string& _first_name, + const rfl::Box& _last_name, int _age) + : first_name_(_first_name), + last_name_(rfl::make_box(*_last_name)), + age_(_age) {} + + const auto& first_name() const { return first_name_; } + + const auto& last_name() const { return last_name_; } + + auto age() const { return age_; } + + private: + std::string first_name_; + rfl::Box last_name_; + int age_; +}; + +struct PersonImpl { + rfl::Field<"firstName", std::string> first_name; + rfl::Field<"lastName", rfl::Box> last_name; + rfl::Field<"age", int> age; + + static PersonImpl from_class(const Person& _p) noexcept { + return PersonImpl{.first_name = _p.first_name(), + .last_name = rfl::make_box(*_p.last_name()), + .age = _p.age()}; + } + + Person to_class() const { + return Person(first_name.value(), std::move(last_name.value()), + age.value()); + } +}; + +} // namespace test_custom_class4 + +namespace rfl { +namespace parsing { + +template +struct Parser + : public CustomParser {}; + +} // namespace parsing +} // namespace rfl + +namespace test_custom_class4 { + +TEST(cereal, test_custom_class4) { + const auto bart = test_custom_class4::Person( + "Bart", rfl::make_box("Simpson"), 10); + + write_and_read(bart); +} +} // namespace test_custom_class4 diff --git a/tests/cereal/test_default_values.cpp b/tests/cereal/test_default_values.cpp new file mode 100644 index 000000000..3fb3c9160 --- /dev/null +++ b/tests/cereal/test_default_values.cpp @@ -0,0 +1,26 @@ +#include +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_default_values { + +struct Person { + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name = "Simpson"; + std::vector children; +}; + +TEST(cereal, test_default_values) { + const auto bart = Person{.first_name = "Bart"}; + const auto lisa = Person{.first_name = "Lisa"}; + const auto maggie = Person{.first_name = "Maggie"}; + const auto homer = + Person{.first_name = "Homer", + .children = std::vector({bart, lisa, maggie})}; + + write_and_read(homer); +} +} // namespace test_default_values diff --git a/tests/cereal/test_deque.cpp b/tests/cereal/test_deque.cpp new file mode 100644 index 000000000..a0bcc9474 --- /dev/null +++ b/tests/cereal/test_deque.cpp @@ -0,0 +1,26 @@ +#include +#include + +#include "write_and_read.hpp" + +namespace test_deque { + +struct Person { + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name = "Simpson"; + std::unique_ptr> children; +}; + +TEST(cereal, test_default_values) { + auto children = std::make_unique>(); + children->emplace_back(Person{.first_name = "Bart"}); + children->emplace_back(Person{.first_name = "Lisa"}); + children->emplace_back(Person{.first_name = "Maggie"}); + + const auto homer = + Person{.first_name = "Homer", .children = std::move(children)}; + + write_and_read(homer); + +} +} // namespace test_deque diff --git a/tests/cereal/test_enum.cpp b/tests/cereal/test_enum.cpp new file mode 100644 index 000000000..925200726 --- /dev/null +++ b/tests/cereal/test_enum.cpp @@ -0,0 +1,22 @@ +#include +#include +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_enum { + +enum class Color { red, green, blue, yellow }; + +struct Circle { + double radius; + Color color; +}; + +TEST(cereal, test_enum) { + const auto circle = Circle{.radius = 5.0, .color = Color::green}; + write_and_read(circle); +} +} // namespace test_enum diff --git a/tests/cereal/test_field_variant.cpp b/tests/cereal/test_field_variant.cpp new file mode 100644 index 000000000..5c0d3d9fd --- /dev/null +++ b/tests/cereal/test_field_variant.cpp @@ -0,0 +1,33 @@ +#include +#include + +#include "write_and_read.hpp" + +namespace test_field_variant { + +struct Circle { + double radius; +}; + +struct Rectangle { + double height; + double width; +}; + +struct Square { + double width; +}; + +struct Shapes { + rfl::Variant, rfl::Field<"rectangle", Rectangle>, + rfl::Field<"square", rfl::Box>> + root; +}; + +TEST(cereal, test_field_variant_std) { + const auto r = + Shapes{rfl::make_field<"rectangle">(Rectangle{.height = 10, .width = 5})}; + + write_and_read(r); +} +} // namespace test_field_variant diff --git a/tests/cereal/test_field_variant_std.cpp b/tests/cereal/test_field_variant_std.cpp new file mode 100644 index 000000000..4932cb24d --- /dev/null +++ b/tests/cereal/test_field_variant_std.cpp @@ -0,0 +1,33 @@ +#include +#include + +#include "write_and_read.hpp" + +namespace test_field_variant_std { + +struct Circle { + double radius; +}; + +struct Rectangle { + double height; + double width; +}; + +struct Square { + double width; +}; + +struct Shapes { + std::variant, rfl::Field<"rectangle", Rectangle>, + rfl::Field<"square", rfl::Box>> + root; +}; + +TEST(cereal, test_field_variant_std) { + const auto r = + Shapes{rfl::make_field<"rectangle">(Rectangle{.height = 10, .width = 5})}; + + write_and_read(r); +} +} // namespace test_field_variant_std diff --git a/tests/cereal/test_flag_enum.cpp b/tests/cereal/test_flag_enum.cpp new file mode 100644 index 000000000..aa7d13965 --- /dev/null +++ b/tests/cereal/test_flag_enum.cpp @@ -0,0 +1,32 @@ +#include +#include + +#include "write_and_read.hpp" + +namespace test_flag_enum { + +enum class Color { + red = 256, + green = 512, + blue = 1024, + yellow = 2048, + orange = (256 | 2048) // red + yellow = orange +}; + +inline Color operator|(Color c1, Color c2) { + return static_cast(static_cast(c1) | static_cast(c2)); +} + +struct Circle { + float radius; + Color color; +}; + +TEST(cereal, test_flag_enum) { + const auto circle = + Circle{.radius = 2.0, .color = Color::blue | Color::orange}; + + write_and_read(circle); +} + +} // namespace test_flag_enum diff --git a/tests/cereal/test_flag_enum_with_int.cpp b/tests/cereal/test_flag_enum_with_int.cpp new file mode 100644 index 000000000..3605e8cbd --- /dev/null +++ b/tests/cereal/test_flag_enum_with_int.cpp @@ -0,0 +1,31 @@ +#include +#include + +#include "write_and_read.hpp" + +namespace test_flag_enum_with_int { + +enum class Color { + red = 256, + green = 512, + blue = 1024, + yellow = 2048, + orange = (256 | 2048) // red + yellow = orange +}; + +inline Color operator|(Color c1, Color c2) { + return static_cast(static_cast(c1) | static_cast(c2)); +} + +struct Circle { + float radius; + Color color; +}; + +TEST(cereal, test_flag_enum_with_int) { + const auto circle = Circle{.radius = 2.0, .color = static_cast(10000)}; + + write_and_read(circle); +} + +} // namespace test_flag_enum_with_int diff --git a/tests/cereal/test_flatten.cpp b/tests/cereal/test_flatten.cpp new file mode 100644 index 000000000..50323a912 --- /dev/null +++ b/tests/cereal/test_flatten.cpp @@ -0,0 +1,31 @@ +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_flatten { + +struct Person { + rfl::Field<"firstName", std::string> first_name; + rfl::Field<"lastName", rfl::Box> last_name; + rfl::Field<"age", int> age; +}; + +struct Employee { + rfl::Flatten person; + rfl::Field<"employer", rfl::Box> employer; + rfl::Field<"salary", float> salary; +}; + +TEST(cereal, test_flatten) { + const auto employee = Employee{ + .person = Person{.first_name = "Homer", + .last_name = rfl::make_box("Simpson"), + .age = 45}, + .employer = rfl::make_box("Mr. Burns"), + .salary = 60000.0}; + + write_and_read(employee); +} +} // namespace test_flatten diff --git a/tests/cereal/test_flatten_anonymous.cpp b/tests/cereal/test_flatten_anonymous.cpp new file mode 100644 index 000000000..ec8ea1182 --- /dev/null +++ b/tests/cereal/test_flatten_anonymous.cpp @@ -0,0 +1,32 @@ +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_flatten_anonymous { + +struct Person { + std::string first_name; + rfl::Box last_name; + int age; +}; + +struct Employee { + rfl::Flatten person; + rfl::Box employer; + float salary; +}; + +TEST(cereal, test_flatten_anonymous) { + const auto employee = Employee{ + .person = Person{.first_name = "Homer", + .last_name = rfl::make_box("Simpson"), + .age = 45}, + .employer = rfl::make_box("Mr. Burns"), + .salary = 60000.0}; + + write_and_read(employee); +} + +} // namespace test_flatten_anonymous diff --git a/tests/cereal/test_forward_list.cpp b/tests/cereal/test_forward_list.cpp new file mode 100644 index 000000000..5b1696cbb --- /dev/null +++ b/tests/cereal/test_forward_list.cpp @@ -0,0 +1,26 @@ +#include +#include + +#include "write_and_read.hpp" + +namespace test_forward_list { + +struct Person { + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name = "Simpson"; + std::unique_ptr> children; +}; + +TEST(cereal, test_forward_list) { + auto children = std::make_unique>(); + children->emplace_front(Person{.first_name = "Maggie"}); + children->emplace_front(Person{.first_name = "Lisa"}); + children->emplace_front(Person{.first_name = "Bart"}); + + const auto homer = + Person{.first_name = "Homer", .children = std::move(children)}; + + write_and_read(homer); + +} +} // namespace test_forward_list diff --git a/tests/cereal/test_generic.cpp b/tests/cereal/test_generic.cpp new file mode 100644 index 000000000..1dfd789d1 --- /dev/null +++ b/tests/cereal/test_generic.cpp @@ -0,0 +1,33 @@ +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_generic { + +TEST(cereal, test_generic) { + auto bart = rfl::Generic::Object(); + bart["first_name"] = "Bart"; + bart["last_name"] = "Simpson"; + bart["age"] = 10; + + auto lisa = rfl::Generic::Object(); + lisa["first_name"] = "Lisa"; + lisa["last_name"] = "Simpson"; + lisa["age"] = 8; + + auto maggie = rfl::Generic::Object(); + maggie["first_name"] = "Lisa"; + maggie["last_name"] = "Simpson"; + maggie["age"] = 0; + + auto homer = rfl::Generic::Object(); + homer["first_name"] = "Lisa"; + homer["last_name"] = "Simpson"; + homer["age"] = 45; + homer["children"] = rfl::Generic::Array({bart, lisa, maggie}); + + write_and_read(homer); +} +} // namespace test_generic diff --git a/tests/cereal/test_literal.cpp b/tests/cereal/test_literal.cpp new file mode 100644 index 000000000..873bece8e --- /dev/null +++ b/tests/cereal/test_literal.cpp @@ -0,0 +1,24 @@ +#include +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_literal { + +using FirstName = rfl::Literal<"Homer", "Marge", "Bart", "Lisa", "Maggie">; +using LastName = rfl::Literal<"Simpson">; + +struct Person { + rfl::Rename<"firstName", FirstName> first_name; + rfl::Rename<"lastName", LastName> last_name; + std::vector children; +}; + +TEST(cereal, test_literal) { + const auto bart = Person{.first_name = FirstName::make<"Bart">()}; + + write_and_read(bart); +} +} // namespace test_literal diff --git a/tests/cereal/test_literal_map.cpp b/tests/cereal/test_literal_map.cpp new file mode 100644 index 000000000..f3baa931c --- /dev/null +++ b/tests/cereal/test_literal_map.cpp @@ -0,0 +1,24 @@ +#include +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_literal_map { + +using FieldName = rfl::Literal<"firstName", "lastName">; + +struct RootStruct { + std::map> root; +}; + +TEST(cereal, test_literal_map) { + RootStruct homer; + homer.root.insert(std::make_pair(FieldName::make<"firstName">(), + std::make_unique("Homer"))); + homer.root.insert(std::make_pair(FieldName::make<"lastName">(), + std::make_unique("Simpson"))); + write_and_read(homer); +} +} // namespace test_literal_map diff --git a/tests/cereal/test_map.cpp b/tests/cereal/test_map.cpp new file mode 100644 index 000000000..3b8aad9c1 --- /dev/null +++ b/tests/cereal/test_map.cpp @@ -0,0 +1,30 @@ +#include +#include +#include +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_map { + +struct Person { + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name = "Simpson"; + std::unique_ptr> children; +}; + +TEST(cereal, test_map) { + auto children = std::make_unique>(); + + children->insert(std::make_pair("Bart", Person{.first_name = "Bart"})); + children->insert(std::make_pair("Lisa", Person{.first_name = "Lisa"})); + children->insert(std::make_pair("Maggie", Person{.first_name = "Maggie"})); + + const auto homer = + Person{.first_name = "Homer", .children = std::move(children)}; + + write_and_read(homer); +} +} // namespace test_map diff --git a/tests/cereal/test_map2.cpp b/tests/cereal/test_map2.cpp new file mode 100644 index 000000000..dddc2b456 --- /dev/null +++ b/tests/cereal/test_map2.cpp @@ -0,0 +1,27 @@ +#include +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_map2 { + +struct Person { + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name = "Simpson"; + std::map children; +}; + +TEST(cereal, test_map2) { + auto children = std::map(); + children.insert(std::make_pair("Bart", Person{.first_name = "Bart"})); + children.insert(std::make_pair("Lisa", Person{.first_name = "Lisa"})); + children.insert(std::make_pair("Maggie", Person{.first_name = "Maggie"})); + + const auto homer = + Person{.first_name = "Homer", .children = std::move(children)}; + + write_and_read(homer); +} +} // namespace test_map2 diff --git a/tests/cereal/test_monster_example.cpp b/tests/cereal/test_monster_example.cpp new file mode 100644 index 000000000..3bf66f849 --- /dev/null +++ b/tests/cereal/test_monster_example.cpp @@ -0,0 +1,59 @@ +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_monster_example { + +using Color = rfl::Literal<"Red", "Green", "Blue">; + +struct Weapon { + std::string name; + short damage; +}; + +using Equipment = + rfl::Variant, rfl::Field<"None", int>>; + +struct Vec3 { + float x; + float y; + float z; +}; + +struct Monster { + Vec3 pos; + short mana = 150; + short hp = 100; + std::string name; + bool friendly = false; + std::vector inventory; + Color color = Color::make<"Blue">(); + std::vector weapons; + Equipment equipped; + std::vector path; +}; + +TEST(cereal, test_monster_example) { + const auto sword = Weapon{.name = "Sword", .damage = 3}; + const auto axe = Weapon{.name = "Axe", .damage = 5}; + + const auto weapons = std::vector({sword, axe}); + + const auto position = Vec3{1.0f, 2.0f, 3.0f}; + + const auto inventory = std::vector({0, 1, 2, 3, 4, 5, 6, 7, 8, 9}); + + const auto orc = Monster{.pos = position, + .mana = 150, + .hp = 80, + .name = "MyMonster", + .inventory = inventory, + .color = Color::make<"Red">(), + .weapons = weapons, + .equipped = rfl::make_field<"weapon">(axe)}; + + write_and_read(orc); +} +} // namespace test_monster_example diff --git a/tests/cereal/test_optional_fields.cpp b/tests/cereal/test_optional_fields.cpp new file mode 100644 index 000000000..c1ce79a23 --- /dev/null +++ b/tests/cereal/test_optional_fields.cpp @@ -0,0 +1,24 @@ +#include +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_optional_fields { + +struct Person { + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name; + std::optional town = std::nullopt; +}; + +TEST(cereal, test_optional_fields) { + const auto person1 = Person{.first_name = "Homer", .last_name = "Simpson"}; + write_and_read(person1); + + const auto person2 = Person{ + .first_name = "Homer", .last_name = "Simpson", .town = "Springfield"}; + write_and_read(person2); +} +} // namespace test_optional_fields diff --git a/tests/cereal/test_optionals_in_vectors.cpp b/tests/cereal/test_optionals_in_vectors.cpp new file mode 100644 index 000000000..70c952a33 --- /dev/null +++ b/tests/cereal/test_optionals_in_vectors.cpp @@ -0,0 +1,29 @@ +#include +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_optionals_in_vectors { + +struct Person { + std::string first_name; + std::string last_name = "Simpson"; + std::vector> children; +}; + +TEST(cereal, test_optionals_in_vectors) { + const std::optional bart = Person{.first_name = "Bart"}; + + const std::optional lisa = Person{.first_name = "Lisa"}; + + const std::optional maggie = Person{.first_name = "Maggie"}; + + const auto homer = Person{.first_name = "Homer", + .children = std::vector>( + {bart, lisa, maggie, std::nullopt})}; + + write_and_read(homer); +} +} // namespace test_optionals_in_vectors diff --git a/tests/cereal/test_person.cpp b/tests/cereal/test_person.cpp new file mode 100644 index 000000000..aa33cd539 --- /dev/null +++ b/tests/cereal/test_person.cpp @@ -0,0 +1,31 @@ +#include + +#include +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_person { + +struct Person { + std::string first_name; + std::string last_name = "Simpson"; + std::vector children; +}; + +TEST(cereal, test_person) { + const auto bart = Person{.first_name = "Bart"}; + + const auto lisa = Person{.first_name = "Lisa"}; + + const auto maggie = Person{.first_name = "Maggie"}; + + const auto homer = + Person{.first_name = "Homer", + .children = std::vector({bart, lisa, maggie})}; + + write_and_read(homer); +} +} // namespace test_person diff --git a/tests/cereal/test_readme_example.cpp b/tests/cereal/test_readme_example.cpp new file mode 100644 index 000000000..6cc2baaae --- /dev/null +++ b/tests/cereal/test_readme_example.cpp @@ -0,0 +1,45 @@ +#include +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_readme_example { + +using Age = rfl::Validator, rfl::Maximum<130>>; + +struct Person { + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name = "Simpson"; + std::string town = "Springfield"; + rfl::Timestamp<"%Y-%m-%d"> birthday; + Age age; + rfl::Email email; + std::vector child; +}; + +TEST(cereal, test_readme_example) { + const auto bart = Person{.first_name = "Bart", + .birthday = "1987-04-19", + .age = 10, + .email = "bart@simpson.com"}; + + const auto lisa = Person{.first_name = "Lisa", + .birthday = "1987-04-19", + .age = 8, + .email = "lisa@simpson.com"}; + + const auto maggie = Person{.first_name = "Maggie", + .birthday = "1987-04-19", + .age = 0, + .email = "maggie@simpson.com"}; + + const auto homer = Person{.first_name = "Homer", + .birthday = "1987-04-19", + .email = "homer@simpson.com", + .child = std::vector({bart, lisa, maggie})}; + + write_and_read(homer); +} +} // namespace test_readme_example diff --git a/tests/cereal/test_readme_example2.cpp b/tests/cereal/test_readme_example2.cpp new file mode 100644 index 000000000..b912b2be1 --- /dev/null +++ b/tests/cereal/test_readme_example2.cpp @@ -0,0 +1,20 @@ +#include +#include + +#include "write_and_read.hpp" + +namespace test_readme_example2 { + +struct Person { + std::string first_name; + std::string last_name; + int age; +}; + +TEST(cereal, test_readme_example2) { + const auto homer = + Person{.first_name = "Homer", .last_name = "Simpson", .age = 45}; + + write_and_read(homer); +} +} // namespace test_readme_example2 diff --git a/tests/cereal/test_readme_example3.cpp b/tests/cereal/test_readme_example3.cpp new file mode 100644 index 000000000..4c9873c22 --- /dev/null +++ b/tests/cereal/test_readme_example3.cpp @@ -0,0 +1,30 @@ +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_readme_example3 { + +struct Person { + std::string first_name; + std::string last_name; + rfl::Timestamp<"%Y-%m-%d"> birthday; + std::vector children; +}; + +TEST(cereal, test_readme_example3) { + const auto bart = Person{.first_name = "Bart", .birthday = "1987-04-19"}; + + const auto lisa = Person{.first_name = "Lisa", .birthday = "1987-04-19"}; + + const auto maggie = Person{.first_name = "Maggie", .birthday = "1987-04-19"}; + + const auto homer = + Person{.first_name = "Homer", + .birthday = "1987-04-19", + .children = std::vector({bart, lisa, maggie})}; + + write_and_read(homer); +} +} // namespace test_readme_example3 diff --git a/tests/cereal/test_ref.cpp b/tests/cereal/test_ref.cpp new file mode 100644 index 000000000..0e4a2f2ec --- /dev/null +++ b/tests/cereal/test_ref.cpp @@ -0,0 +1,41 @@ +#include +#include + +#include "write_and_read.hpp" + +namespace test_ref { + +struct DecisionTree { + struct Leaf { + using Tag = rfl::Literal<"Leaf">; + double value; + }; + + struct Node { + using Tag = rfl::Literal<"Node">; + rfl::Rename<"criticalValue", double> critical_value; + rfl::Ref lesser; + rfl::Ref greater; + }; + + using LeafOrNode = rfl::TaggedUnion<"type", Leaf, Node>; + + rfl::Field<"leafOrNode", LeafOrNode> leaf_or_node; +}; + +TEST(cereal, test_ref) { + const auto leaf1 = DecisionTree::Leaf{.value = 3.0}; + + const auto leaf2 = DecisionTree::Leaf{.value = 5.0}; + + auto node = DecisionTree::Node{ + .critical_value = 10.0, + .lesser = rfl::make_ref(DecisionTree{leaf1}), + .greater = rfl::make_ref(DecisionTree{leaf2})}; + + const DecisionTree tree{.leaf_or_node = std::move(node)}; + + write_and_read(tree); + +} +} // namespace test_ref diff --git a/tests/cereal/test_rfl_tuple.cpp b/tests/cereal/test_rfl_tuple.cpp new file mode 100644 index 000000000..5729a867e --- /dev/null +++ b/tests/cereal/test_rfl_tuple.cpp @@ -0,0 +1,32 @@ + +#include +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_rfl_tuple { + +struct Person { + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name = "Simpson"; + std::unique_ptr> children = nullptr; +}; + +TEST(cereal, test_rfl_tuple) { + auto bart = Person{.first_name = "Bart"}; + + auto lisa = Person{.first_name = "Lisa"}; + + auto maggie = Person{.first_name = "Maggie"}; + + const auto homer = + Person{.first_name = "Homer", + .children = std::make_unique>( + rfl::Tuple{ + std::move(bart), std::move(lisa), std::move(maggie)})}; + + write_and_read(homer); +} +} // namespace test_rfl_tuple diff --git a/tests/cereal/test_rfl_variant.cpp b/tests/cereal/test_rfl_variant.cpp new file mode 100644 index 000000000..4be257c94 --- /dev/null +++ b/tests/cereal/test_rfl_variant.cpp @@ -0,0 +1,30 @@ +#include +#include + +#include "write_and_read.hpp" + +namespace test_variant { + +struct Circle { + double radius; +}; + +struct Rectangle { + double height; + double width; +}; + +struct Square { + double width; +}; + +struct Shapes { + rfl::Variant> root; +}; + +TEST(cereal, test_rfl_variant) { + const auto r = Shapes{Rectangle{.height = 10, .width = 5}}; + + write_and_read(r); +} +} // namespace test_variant diff --git a/tests/cereal/test_save_load.cpp b/tests/cereal/test_save_load.cpp new file mode 100644 index 000000000..c719d434b --- /dev/null +++ b/tests/cereal/test_save_load.cpp @@ -0,0 +1,29 @@ +#include +#include +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_save_load { +struct Person { + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name; + rfl::Email email; + int age; +}; + +TEST(cereal, test_save_load) { + const auto homer = Person{.first_name = "Homer", + .last_name = "Simpson", + .email = "homer@simpson.com", + .age = 45}; + + rfl::cereal::save("homer.cereal", homer); + + const auto homer2 = rfl::cereal::load("homer.cereal").value(); + + write_and_read(homer2); +} +} // namespace test_save_load diff --git a/tests/cereal/test_set.cpp b/tests/cereal/test_set.cpp new file mode 100644 index 000000000..bf8aa128b --- /dev/null +++ b/tests/cereal/test_set.cpp @@ -0,0 +1,23 @@ +#include +#include + +#include "write_and_read.hpp" + +namespace test_set { + +struct Person { + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name = "Simpson"; + std::unique_ptr> children; +}; + +TEST(cereal, test_set) { + auto children = std::make_unique>( + std::set({"Bart", "Lisa", "Maggie"})); + + const auto homer = + Person{.first_name = "Homer", .children = std::move(children)}; + + write_and_read(homer); +} +} // namespace test_set diff --git a/tests/cereal/test_shared_ptr.cpp b/tests/cereal/test_shared_ptr.cpp new file mode 100644 index 000000000..f9a870a04 --- /dev/null +++ b/tests/cereal/test_shared_ptr.cpp @@ -0,0 +1,28 @@ +#include +#include +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_shared_ptr { + +struct Person { + std::string first_name; + std::string last_name = "Simpson"; + std::shared_ptr> children; +}; + +TEST(cereal, test_shared_ptr) { + auto children = std::make_shared>(); + children->emplace_back(Person{.first_name = "Bart"}); + children->emplace_back(Person{.first_name = "Lisa"}); + children->emplace_back(Person{.first_name = "Maggie"}); + + const auto homer = + Person{.first_name = "Homer", .children = std::move(children)}; + + write_and_read(homer); +} +} // namespace test_shared_ptr diff --git a/tests/cereal/test_size.cpp b/tests/cereal/test_size.cpp new file mode 100644 index 000000000..4d6741465 --- /dev/null +++ b/tests/cereal/test_size.cpp @@ -0,0 +1,36 @@ +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_size { + +struct Person { + rfl::Rename<"firstName", std::string> first_name; + rfl::Rename<"lastName", std::string> last_name; + rfl::Timestamp<"%Y-%m-%d"> birthday; + rfl::Validator, + rfl::Size, rfl::EqualTo<3>>>> + children; +}; + +TEST(cereal, test_size) { + const auto bart = Person{ + .first_name = "Bart", .last_name = "Simpson", .birthday = "1987-04-19"}; + + const auto lisa = Person{ + .first_name = "Lisa", .last_name = "Simpson", .birthday = "1987-04-19"}; + + const auto maggie = Person{ + .first_name = "Maggie", .last_name = "Simpson", .birthday = "1987-04-19"}; + + const auto homer = + Person{.first_name = "Homer", + .last_name = "Simpson", + .birthday = "1987-04-19", + .children = std::vector({bart, lisa, maggie})}; + + write_and_read(homer); +} +} // namespace test_size diff --git a/tests/cereal/test_string_map.cpp b/tests/cereal/test_string_map.cpp new file mode 100644 index 000000000..b0e16ec77 --- /dev/null +++ b/tests/cereal/test_string_map.cpp @@ -0,0 +1,24 @@ +#include +#include +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_string_map { + +struct RootStruct { + std::map> root; +}; + +TEST(cereal, test_string_map) { + RootStruct homer; + homer.root.insert( + std::make_pair("firstName", std::make_unique("Homer"))); + homer.root.insert( + std::make_pair("lastName", std::make_unique("Simpson"))); + + write_and_read(homer); +} +} // namespace test_string_map diff --git a/tests/cereal/test_tagged_union.cpp b/tests/cereal/test_tagged_union.cpp new file mode 100644 index 000000000..00eb5baf2 --- /dev/null +++ b/tests/cereal/test_tagged_union.cpp @@ -0,0 +1,34 @@ +#include +#include +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_tagged_union { + +struct Circle { + using Tag = rfl::Literal<"circle">; + double radius; +}; + +struct Rectangle { + using Tag = rfl::Literal<"rectangle">; + double height; + double width; +}; + +struct Square { + using Tag = rfl::Literal<"square">; + double width; +}; + +using Shapes = rfl::TaggedUnion<"shape", Circle, Rectangle, Square>; + +TEST(cereal, test_tagged_union) { + const Shapes r = Rectangle{.height = 10, .width = 5}; + write_and_read(r); +} + +} // namespace test_tagged_union diff --git a/tests/cereal/test_timestamp.cpp b/tests/cereal/test_timestamp.cpp new file mode 100644 index 000000000..42cbe3ade --- /dev/null +++ b/tests/cereal/test_timestamp.cpp @@ -0,0 +1,24 @@ +#include +#include +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_timestamp { + +struct Person { + std::string first_name; + std::string last_name; + rfl::Timestamp<"%Y-%m-%d"> birthday; +}; + +TEST(cereal, test_timestamp) { + const auto person = Person{.first_name = "Homer", + .last_name = "Simpson", + .birthday = "1987-04-19"}; + + write_and_read(person); +} +} // namespace test_timestamp diff --git a/tests/cereal/test_tuple.cpp b/tests/cereal/test_tuple.cpp new file mode 100644 index 000000000..c7dfcfe24 --- /dev/null +++ b/tests/cereal/test_tuple.cpp @@ -0,0 +1,17 @@ +#include +#include +#include +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_tuple { + +TEST(cereal, test_tuple) { + const auto my_tuple = std::make_tuple(42, std::string("Hello"), true, 3.14); + write_and_read(my_tuple); +} + +} // namespace test_tuple diff --git a/tests/cereal/test_unique_ptr.cpp b/tests/cereal/test_unique_ptr.cpp new file mode 100644 index 000000000..588643ccc --- /dev/null +++ b/tests/cereal/test_unique_ptr.cpp @@ -0,0 +1,44 @@ +#include +#include +#include +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_unique_ptr { + +struct DecisionTree { + struct Leaf { + using Tag = rfl::Literal<"Leaf">; + double value; + }; + + struct Node { + using Tag = rfl::Literal<"Node">; + rfl::Rename<"criticalValue", double> critical_value; + std::unique_ptr lesser; + std::unique_ptr greater; + }; + + using LeafOrNode = rfl::TaggedUnion<"type", Leaf, Node>; + + rfl::Field<"leafOrNode", LeafOrNode> leaf_or_node; +}; + +TEST(cereal, test_unique_ptr) { + auto leaf1 = DecisionTree::Leaf{.value = 3.0}; + + auto leaf2 = DecisionTree::Leaf{.value = 5.0}; + + auto node = DecisionTree::Node{ + .critical_value = 10.0, + .lesser = std::make_unique(DecisionTree::LeafOrNode(leaf1)), + .greater = std::make_unique(DecisionTree::LeafOrNode(leaf2))}; + + const DecisionTree tree = DecisionTree{.leaf_or_node = std::move(node)}; + + write_and_read(tree); +} +} // namespace test_unique_ptr diff --git a/tests/cereal/test_unique_ptr2.cpp b/tests/cereal/test_unique_ptr2.cpp new file mode 100644 index 000000000..31d5d72db --- /dev/null +++ b/tests/cereal/test_unique_ptr2.cpp @@ -0,0 +1,40 @@ +#include +#include + +#include "write_and_read.hpp" + +namespace test_unique_ptr2 { + +struct DecisionTree { + struct Leaf { + using Tag = rfl::Literal<"Leaf">; + double value; + }; + + struct Node { + using Tag = rfl::Literal<"Node">; + rfl::Rename<"criticalValue", double> critical_value; + std::unique_ptr lesser; + std::unique_ptr greater; + }; + + using LeafOrNode = rfl::TaggedUnion<"type", Leaf, Node>; + + rfl::Field<"leafOrNode", LeafOrNode> leaf_or_node; +}; + +TEST(cereal, test_unique_ptr2) { + auto leaf1 = DecisionTree::Leaf{.value = 3.0}; + + auto leaf2 = DecisionTree::Leaf{.value = 5.0}; + + auto node = DecisionTree::Node{ + .critical_value = 10.0, + .lesser = std::make_unique(DecisionTree{leaf1}), + .greater = std::make_unique(DecisionTree{leaf2})}; + + const DecisionTree tree{.leaf_or_node = std::move(node)}; + + write_and_read(tree); +} +} // namespace test_unique_ptr2 diff --git a/tests/cereal/test_variant.cpp b/tests/cereal/test_variant.cpp new file mode 100644 index 000000000..b2c7e3d13 --- /dev/null +++ b/tests/cereal/test_variant.cpp @@ -0,0 +1,32 @@ +#include +#include +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_variant { + +struct Circle { + double radius; +}; + +struct Rectangle { + double height; + double width; +}; + +struct Square { + double width; +}; + +using Shapes = std::variant; + +TEST(cereal, test_variant) { + const Shapes r = Rectangle{.height = 10, .width = 5}; + + write_and_read(r); +} + +} // namespace test_variant diff --git a/tests/cereal/test_variants_in_vectors.cpp b/tests/cereal/test_variants_in_vectors.cpp new file mode 100644 index 000000000..0f1a3d891 --- /dev/null +++ b/tests/cereal/test_variants_in_vectors.cpp @@ -0,0 +1,31 @@ +#include +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_variants_in_vectors { + +struct Person { + std::string first_name; + std::string last_name = "Simpson"; + std::vector> children; +}; + +TEST(cereal, test_variants_in_vectors) { + const std::variant bart = Person{.first_name = "Bart"}; + + const std::variant lisa = Person{.first_name = "Lisa"}; + + const std::variant maggie = + Person{.first_name = "Maggie"}; + + const auto homer = + Person{.first_name = "Homer", + .children = std::vector>( + {bart, lisa, maggie, "Unknown"})}; + + write_and_read(homer); +} +} // namespace test_variants_in_vectors diff --git a/tests/cereal/test_wstring.cpp b/tests/cereal/test_wstring.cpp new file mode 100644 index 000000000..aef3725bf --- /dev/null +++ b/tests/cereal/test_wstring.cpp @@ -0,0 +1,19 @@ +#include +#include +#include + +#include "write_and_read.hpp" + +struct TestStruct { + std::string theNormalString; + std::wstring theWiderString; +}; + +namespace test_wstring { +TEST(cereal, test_wstring) { + const auto test = TestStruct{.theNormalString = "The normal string", + .theWiderString = L"The wider string"}; + + write_and_read(test); +} +} // namespace test_wstring diff --git a/tests/cereal/write_and_read.hpp b/tests/cereal/write_and_read.hpp new file mode 100644 index 000000000..fc84a002a --- /dev/null +++ b/tests/cereal/write_and_read.hpp @@ -0,0 +1,18 @@ +#ifndef WRITE_AND_READ_ +#define WRITE_AND_READ_ + +#include + +#include + +template +void write_and_read(const auto& _struct) { + using T = std::remove_cvref_t; + const auto serialized1 = rfl::cereal::write(_struct); + const auto res = rfl::cereal::read(serialized1); + EXPECT_TRUE(res && true) << "Test failed on read. Error: " + << res.error().what(); + const auto serialized2 = rfl::cereal::write(res.value()); + EXPECT_EQ(serialized1, serialized2); +} +#endif diff --git a/vcpkg.json b/vcpkg.json index 577903832..644044443 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -70,6 +70,15 @@ } ] }, + "cereal": { + "description": "Enable Cereal support", + "dependencies": [ + { + "name": "cereal", + "version>=": "1.3.2#1" + } + ] + }, "csv": { "description": "Enable CSV support", "dependencies": [