diff --git a/docs/concepts/processors.md b/docs/concepts/processors.md index 32e29586..a0b8cff2 100644 --- a/docs/concepts/processors.md +++ b/docs/concepts/processors.md @@ -35,6 +35,7 @@ reflect-cpp currently supports the following processors: - `rfl::AddStructName` - `rfl::AddTagsToVariants` +- `rfl::AddNamespacedTagsToVariants` - `rfl::AllowRawPtrs` - `rfl::DefaultIfMissing` - `rfl::NoExtraFields` @@ -117,6 +118,89 @@ struct key_pressed_t { Note that there are other ways to address problems like this, for instance `rfl::TaggedUnion`. Please refer to the relevant sections of the documentation. +### `rfl::AddNamespacedTagsToVariants` + +This processor is similar to `rfl::AddTagsToVariants`, but instead of using just the struct name as the tag, it uses the full namespaced type name. This is particularly useful when you have: + +1. Structs with the same name in different namespaces +2. Structs with `rfl::Generic` fields that would otherwise create naming conflicts + +#### Use-case: Structs with the same name in different namespaces + +Consider this example where `rfl::AddTagsToVariants` would fail: + +```cpp +namespace Result { + struct Message { + std::string result; + }; +} + +namespace Error { + struct Message { + std::string error; + int error_id; + }; +} + +using Messages = std::variant; + +const auto msgs = std::vector{ + Result::Message{.result = "success"}, + Error::Message{.error = "failure", .error_id = 404} +}; + +// This would cause problems with rfl::AddTagsToVariants because both +// structs have the same name "Message" + +// But this works perfectly: +const auto json_string = rfl::json::write(msgs); +const auto msgs2 = rfl::json::read, rfl::AddNamespacedTagsToVariants>(json_string); +``` + +The resulting JSON includes the full namespace path: + +```json +[ + {"Result::Message": {"result": "success"}}, + {"Error::Message": {"error": "failure", "error_id": 404}} +] +``` + +#### Use-case: Structs with `rfl::Generic` fields that would otherwise create naming conflicts + +Another use case is with `rfl::Generic` fields, where multiple structs contain generic fields that would otherwise create naming conflicts: + +```cpp +struct APIResult { + rfl::Generic result; // Could be string, number, object, etc. +}; + +struct APIError { + rfl::Generic error; // Could be string, number, object, etc. +}; + +using APIResponse = std::variant; + +const auto response = APIResult{.result = std::string("200")}; + +// Without namespaces, both Generic fields would have the same tag name. +// With namespaces, they get unique identifiers: +const auto json_string = rfl::json::write(response); +``` + +This generates: + +```json +{"APIResult": {"result": {"std::string": "200"}}} +``` + +#### When to use `rfl::AddNamespacedTagsToVariants` vs `rfl::AddTagsToVariants` + +- Use `rfl::AddTagsToVariants` when struct names are unique and you want shorter, cleaner tags +- Use `rfl::AddNamespacedTagsToVariants` when you have naming conflicts or need to distinguish between types in different namespaces +- Custom tags (using `Tag = rfl::Literal<"custom_name">`) work with both processors and are not affected by the namespace handling + ### `rfl::AllowRawPtrs` By default, reflect-cpp does not allow *reading into* raw pointers, `std::string_view` or `std::span`. diff --git a/docs/variants_and_tagged_unions.md b/docs/variants_and_tagged_unions.md index a55c3ef3..3b39522e 100644 --- a/docs/variants_and_tagged_unions.md +++ b/docs/variants_and_tagged_unions.md @@ -57,6 +57,44 @@ const auto r2 = rfl::json::read(json_string); Please refer to the section on processors in this documentation for more information. +## Automatic tags with full namespaces + +In some cases, you might have struct name conflicts between different namespaces, or types that would generate identical tags when namespaces are removed. For these situations, you can use `rfl::AddNamespacedTagsToVariants` instead: + +```cpp +namespace Result { + struct Message { + std::string result; + }; +} + +namespace Error { + struct Message { + std::string error; + int error_id; + }; +} + +using Messages = std::variant; + +const Messages msg = Error::Message{.error = "Something went wrong", .error_id = 404}; + +const auto json_string = rfl::json::write(msg); +``` + +This generates JSON with fully qualified type names: + +```json +{"Error::Message":{"error":"Something went wrong","error_id":404}} +``` + +The key differences between `rfl::AddTagsToVariants` and `rfl::AddNamespacedTagsToVariants`: + +- `rfl::AddTagsToVariants` uses just the struct name (e.g., `"Message"`) +- `rfl::AddNamespacedTagsToVariants` uses the full namespaced name (e.g., `"Error::Message"`) +- Both respect custom tags defined with `using Tag = rfl::Literal<"custom_name">` +- Use the namespaced version when you are using `rfl::Generic` or need to distinguish between types in different namespaces and don't want to manually tag them as above + ## `rfl::TaggedUnion` (internally tagged) Another way to solve this problem is to add a tag inside the class. That is why we have provided a helper class for these purposes: `rfl::TaggedUnion`. diff --git a/include/rfl/AddTagsToVariants.hpp b/include/rfl/AddTagsToVariants.hpp index 09b9d867..1a68130e 100644 --- a/include/rfl/AddTagsToVariants.hpp +++ b/include/rfl/AddTagsToVariants.hpp @@ -14,6 +14,17 @@ struct AddTagsToVariants { } }; +/// This is a "fake" processor - it doesn't do much in itself, but its +/// inclusion instructs the parsers to automatically add tags to the variants +/// they might encounter. +struct AddNamespacedTagsToVariants { + public: + template + static auto process(auto&& _named_tuple) { + return _named_tuple; + } +}; + } // namespace rfl #endif diff --git a/include/rfl/Processors.hpp b/include/rfl/Processors.hpp index 2284eb96..696e9f8e 100644 --- a/include/rfl/Processors.hpp +++ b/include/rfl/Processors.hpp @@ -19,6 +19,7 @@ struct Processors; template <> struct Processors<> { static constexpr bool add_tags_to_variants_ = false; + static constexpr bool add_namespaced_tags_to_variants_ = false; static constexpr bool allow_raw_ptrs_ = false; static constexpr bool all_required_ = false; static constexpr bool default_if_missing_ = false; @@ -38,6 +39,10 @@ struct Processors { std::disjunction_v, internal::is_add_tags_to_variants...>; + static constexpr bool add_namespaced_tags_to_variants_ = + std::disjunction_v, + internal::is_add_namespaced_tags_to_variants...>; + static constexpr bool allow_raw_ptrs_ = std::disjunction_v, internal::is_allow_raw_ptrs...>; @@ -64,6 +69,9 @@ struct Processors { template static auto process(NamedTupleType&& _named_tuple) { + static_assert(!add_tags_to_variants_ || !add_namespaced_tags_to_variants_, + "You cannot add both rfl::AddTagsToVariants and " + "rfl::AddNamespacedTagsToVariants."); return Processors::template process( Head::template process(std::move(_named_tuple))); } diff --git a/include/rfl/internal/is_add_tags_to_variants_v.hpp b/include/rfl/internal/is_add_tags_to_variants_v.hpp index a756eb47..59fac0d5 100644 --- a/include/rfl/internal/is_add_tags_to_variants_v.hpp +++ b/include/rfl/internal/is_add_tags_to_variants_v.hpp @@ -22,6 +22,20 @@ template constexpr bool is_add_tags_to_variants_v = is_add_tags_to_variants< std::remove_cvref_t>>::value; +template +class is_add_namespaced_tags_to_variants; + +template +class is_add_namespaced_tags_to_variants : public std::false_type {}; + +template <> +class is_add_namespaced_tags_to_variants : public std::true_type {}; + +template +constexpr bool is_add_namespaced_tags_to_variants_v = is_add_namespaced_tags_to_variants< + std::remove_cvref_t>>::value; + + } // namespace rfl::internal #endif diff --git a/include/rfl/parsing/Parser_rfl_variant.hpp b/include/rfl/parsing/Parser_rfl_variant.hpp index 3308e87a..1e7dc7d3 100644 --- a/include/rfl/parsing/Parser_rfl_variant.hpp +++ b/include/rfl/parsing/Parser_rfl_variant.hpp @@ -53,9 +53,11 @@ class Parser, ProcessorsType> { return _r.template read_union, V>(_u); }); - } else if constexpr (ProcessorsType::add_tags_to_variants_) { + } else if constexpr (ProcessorsType::add_tags_to_variants_ || + ProcessorsType::add_namespaced_tags_to_variants_) { + constexpr bool remove_namespaces = ProcessorsType::add_tags_to_variants_; using FieldVariantType = - rfl::Variant...>; + rfl::Variant...>; const auto from_field_variant = [](auto&& _field) -> rfl::Variant { return std::move(_field.value()); @@ -119,12 +121,14 @@ class Parser, ProcessorsType> { }, _variant); - } else if constexpr (ProcessorsType::add_tags_to_variants_) { + } else if constexpr (ProcessorsType::add_tags_to_variants_ || + ProcessorsType::add_namespaced_tags_to_variants_) { + constexpr bool remove_namespaces = ProcessorsType::add_tags_to_variants_; using FieldVariantType = - rfl::Variant...>; + rfl::Variant...>; const auto to_field_variant = [](const T& _t) -> FieldVariantType { - return VariantAlternativeWrapper(&_t); + return VariantAlternativeWrapper(&_t); }; Parser::write( _w, _variant.visit(to_field_variant), _parent); @@ -144,9 +148,11 @@ class Parser, ProcessorsType> { return FieldVariantParser::to_schema(_definitions); - } else if constexpr (ProcessorsType::add_tags_to_variants_) { + } else if constexpr (ProcessorsType::add_tags_to_variants_ || + ProcessorsType::add_namespaced_tags_to_variants_) { + constexpr bool remove_namespaces = ProcessorsType::add_tags_to_variants_; using FieldVariantType = - rfl::Variant...>; + rfl::Variant...>; return Parser::to_schema( _definitions); diff --git a/include/rfl/parsing/Parser_variant.hpp b/include/rfl/parsing/Parser_variant.hpp index 41218df1..d424ee61 100644 --- a/include/rfl/parsing/Parser_variant.hpp +++ b/include/rfl/parsing/Parser_variant.hpp @@ -70,9 +70,11 @@ class Parser, ProcessorsType> { return _r.template read_union, V>(_u); }); - } else if constexpr (ProcessorsType::add_tags_to_variants_) { + } else if constexpr (ProcessorsType::add_tags_to_variants_ || + ProcessorsType::add_namespaced_tags_to_variants_) { + constexpr bool remove_namespaces = ProcessorsType::add_tags_to_variants_; using FieldVariantType = - rfl::Variant...>; + rfl::Variant...>; const auto from_field_variant = [](auto&& _field) -> std::variant { return std::move(_field.value()); @@ -148,12 +150,14 @@ class Parser, ProcessorsType> { }, _variant); - } else if constexpr (ProcessorsType::add_tags_to_variants_) { + } else if constexpr (ProcessorsType::add_tags_to_variants_ || + ProcessorsType::add_namespaced_tags_to_variants_) { + constexpr bool remove_namespaces = ProcessorsType::add_tags_to_variants_; using FieldVariantType = - rfl::Variant...>; + rfl::Variant...>; const auto to_field_variant = [](const T& _t) -> FieldVariantType { - return VariantAlternativeWrapper(&_t); + return VariantAlternativeWrapper(&_t); }; Parser::write( _w, std::visit(to_field_variant, _variant), _parent); @@ -173,9 +177,11 @@ class Parser, ProcessorsType> { return FieldVariantParser::to_schema(_definitions); - } else if constexpr (ProcessorsType::add_tags_to_variants_) { + } else if constexpr (ProcessorsType::add_tags_to_variants_ || + ProcessorsType::add_namespaced_tags_to_variants_) { + constexpr bool remove_namespaces = ProcessorsType::add_tags_to_variants_; using FieldVariantType = - rfl::Variant...>; + rfl::Variant...>; return Parser::to_schema( _definitions); diff --git a/include/rfl/parsing/VariantAlternativeWrapper.hpp b/include/rfl/parsing/VariantAlternativeWrapper.hpp index 924b711f..fe6215bf 100644 --- a/include/rfl/parsing/VariantAlternativeWrapper.hpp +++ b/include/rfl/parsing/VariantAlternativeWrapper.hpp @@ -22,24 +22,28 @@ struct GetName> { constexpr static internal::StringLiteral name_ = _name; }; -template +template consteval auto make_tag() { if constexpr (internal::has_tag_v) { return typename T::Tag(); - } else { + } else if constexpr (std::is_same_v, std::string>) { + return Literal<"std::string">(); + } else if constexpr (_remove_namespaces) { return Literal< internal::remove_namespaces()>()>(); + } else { + return Literal()>(); } } -template +template using tag_t = std::invoke_result_t< - decltype(make_tag>>)>; + decltype(make_tag>, _remove_namespaces>)>; } // namespace vaw -template -using VariantAlternativeWrapper = Field>::name_, T>; +template +using VariantAlternativeWrapper = Field>::name_, T>; } // namespace rfl::parsing diff --git a/include/rfl/parsing/tabular/ArrowReader.hpp b/include/rfl/parsing/tabular/ArrowReader.hpp index 5956f468..d749ae74 100644 --- a/include/rfl/parsing/tabular/ArrowReader.hpp +++ b/include/rfl/parsing/tabular/ArrowReader.hpp @@ -27,6 +27,8 @@ template class ArrowReader { static_assert(!Processors::add_tags_to_variants_, "rfl::AddTagsToVariants cannot be used for tabular data."); + static_assert(!Processors::add_namespaced_tags_to_variants_, + "rfl::AddNamespacedTagsToVariants cannot be used for tabular data."); static_assert(!Processors::all_required_, "rfl::NoOptionals cannot be used for tabular data."); static_assert(!Processors::default_if_missing_, diff --git a/include/rfl/parsing/tabular/ArrowWriter.hpp b/include/rfl/parsing/tabular/ArrowWriter.hpp index 43c274e0..476f5f48 100644 --- a/include/rfl/parsing/tabular/ArrowWriter.hpp +++ b/include/rfl/parsing/tabular/ArrowWriter.hpp @@ -24,6 +24,8 @@ template class ArrowWriter { static_assert(!Processors::add_tags_to_variants_, "rfl::AddTagsToVariants cannot be used for tabular data."); + static_assert(!Processors::add_namespaced_tags_to_variants_, + "rfl::AddNamespacedTagsToVariants cannot be used for tabular data."); static_assert(!Processors::all_required_, "rfl::NoOptionals cannot be used for tabular data."); static_assert(!Processors::default_if_missing_, diff --git a/tests/cbor/test_integers.cpp b/tests/cbor/test_integers.cpp index f35f6611..c09cefaa 100644 --- a/tests/cbor/test_integers.cpp +++ b/tests/cbor/test_integers.cpp @@ -1,6 +1,7 @@ #include #include +#include "write_and_read.hpp" namespace test_integers { @@ -16,26 +17,27 @@ TEST(cbor, test_integers_signedness) { int64_t i64; }; - std::vector unsigned_buffer = rfl::cbor::write(Unsigned{BIG_INT}); - std::vector unsigned_expected = { - 0xBF, 0x63, 0x75, 0x36, 0x34, - 0x1B, // Per RFC 8949, Initial byte '0x1B' indicates "unsigned integer (eight-byte uint64_t follows)" - // See: https://www.rfc-editor.org/rfc/rfc8949.html#section-appendix.b - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF - }; - - EXPECT_EQ(std::vector(unsigned_expected.begin(), unsigned_expected.end()), unsigned_buffer); - - std::vector signed_buffer = rfl::cbor::write(Signed{static_cast(BIG_INT)}); - std::vector signed_expected = { - 0xBF, 0x63, 0x69, 0x36, 0x34, - 0x20, // Per RFC 8949, Initial byte '0x20' indicates "negative integer -1-0x00..-1-0x17 (-1..-24)" - // See: https://www.rfc-editor.org/rfc/rfc8949.html#section-appendix.b - 0xFF - }; - - EXPECT_EQ(std::vector(signed_expected.begin(), signed_expected.end()), signed_buffer); + write_and_read( + Unsigned{BIG_INT}, + // From https://cbor.me/?diag={_%20%22u64%22:%200xffffffffffffffff} + R"( +BF # map(*) + 63 # text(3) + 753634 # "u64" + 1B FFFFFFFFFFFFFFFF # unsigned(18446744073709551615) + FF # primitive(*) + )"); + + write_and_read( + Signed{static_cast(BIG_INT)}, + // From https://cbor.me/?diag={_%20%22i64%22:%20-1} + R"( +BF # map(*) + 63 # text(3) + 693634 # "i64" + 20 # negative(0) + FF # primitive(*) + )"); } } // namespace test_integers diff --git a/tests/cbor/test_variant.cpp b/tests/cbor/test_variant.cpp index 36448976..78a37761 100644 --- a/tests/cbor/test_variant.cpp +++ b/tests/cbor/test_variant.cpp @@ -21,11 +21,23 @@ struct Square { double width; }; -using Shapes = std::variant>; +using Shape = std::variant>; TEST(cbor, test_variant) { - const Shapes r = Rectangle{.height = 10, .width = 5}; - - write_and_read(r); + write_and_read( + // Note: Non-integral values force double-precision encoding on cbor.me, for convenience + Shape{Rectangle{.height = 10.00000001, .width = 5.00000001}}, + // Diagnostic notation: {_"height": 10.00000001, "width": 5.00000001} + // From: https://cbor.me/?diag={_%22height%22:%2010.00000001,%20%22width%22:%205.00000001} + R"( +BF # map(*) + 66 # text(6) + 686569676874 # "height" + FB 402400000055E63C # primitive(4621819117594601020) + 65 # text(5) + 7769647468 # "width" + FB 4014000000ABCC77 # primitive(4617315517972860023) + FF # primitive(*) + )"); } } // namespace test_variant diff --git a/tests/cbor/test_variant_auto_tagging.cpp b/tests/cbor/test_variant_auto_tagging.cpp new file mode 100644 index 00000000..0f7b79e2 --- /dev/null +++ b/tests/cbor/test_variant_auto_tagging.cpp @@ -0,0 +1,185 @@ +#include +#include +#include +#include +#include + +#include "write_and_read.hpp" + +// NOTE TO MAINTAINERS: +// These tests are very similar to the others in test_add_*tags_to_*variants.cpp, so please keep them +// in sync. +namespace test_variant_auto_tagging { + +// test 1 -> normal behaviour + +struct button_pressed_t {}; + +// Test that we can still use structs that must be moved +struct button_released_t { + rfl::Box button; +}; + +struct key_pressed_t { + using Tag = rfl::Literal<"key_pressed">; + char key; +}; + +using my_event_type_t = + rfl::Variant; + +TEST(cbor, test_add_tags_to_rfl_variants) { + std::vector vec; + vec.emplace_back(button_pressed_t{}); + vec.emplace_back(button_released_t{rfl::make_box(4)}); + vec.emplace_back(key_pressed_t{'c'}); + vec.emplace_back(3); + write_and_read( + vec, + // Diagnostic notation: [{_ "button_pressed_t": {_ }}, {_ "button_released_t": {_ "button": 4}}, {_ "key_pressed": {_ "key": 99}}, {_ "int": 3}] + // See: https://cbor.me/?diag=[{_%20%22button_pressed_t%22:%20{_%20}},%20{_%20%22button_released_t%22:%20{_%20%22button%22:%204}},%20{_%20%22key_pressed%22:%20{_%20%22key%22:%2099}},%20{_%20%22int%22:%203}] + R"( +84 # array(4) + BF # map(*) + 70 # text(16) + 627574746F6E5F707265737365645F74 # "button_pressed_t" + BF # map(*) + FF # primitive(*) + FF # primitive(*) + BF # map(*) + 71 # text(17) + 627574746F6E5F72656C65617365645F74 # "button_released_t" + BF # map(*) + 66 # text(6) + 627574746F6E # "button" + 04 # unsigned(4) + FF # primitive(*) + FF # primitive(*) + BF # map(*) + 6B # text(11) + 6B65795F70726573736564 # "key_pressed" + BF # map(*) + 63 # text(3) + 6B6579 # "key" + 18 63 # unsigned(99) + FF # primitive(*) + FF # primitive(*) + BF # map(*) + 63 # text(3) + 696E74 # "int" + 03 # unsigned(3) + FF # primitive(*) + )"); +} + +// test 2 -> 'Generic' within a struct like this can be read/written with +// rfl::AddNamespacedTagsToVariants so that the underlying `std::variant` holds two 'Generic' fields +// with different namespaces. See the similar test in test_add_tags_to_variants.cpp. +// A hypothetical alternative would be adding an optional manual tag to rfl::Generic + +struct APIResult { + rfl::Generic result; +}; +struct APIError { + rfl::Generic error; +}; + +using APICallOutput = std::variant; + +TEST(cbor, test_add_namespaced_tags_to_variants_with_generic) { + std::vector output = { + APIResult{"200"}, + APIError{"an error"} + }; + write_and_read( + output, + // Diagnostic notation: [{_ "test_variant_auto_tagging::APIResult": {_ "result": {_ "std::string": "200"}}}, {_ "test_variant_auto_tagging::APIError": {_ "error": {_ "std::string": "an error"}}}] + // See: https://cbor.me/?diag=[{_%20%22test_variant_auto_tagging::APIResult%22:%20{_%20%22result%22:%20{_%20%22std::string%22:%20%22200%22}}},%20{_%20%22test_variant_auto_tagging::APIError%22:%20{_%20%22error%22:%20{_%20%22std::string%22:%20%22an%20error%22}}}] + R"( +82 # array(2) + BF # map(*) + 78 24 # text(36) + 746573745F76617269616E745F6175746F5F74616767696E673A3A415049526573756C74 # "test_variant_auto_tagging::APIResult" + BF # map(*) + 66 # text(6) + 726573756C74 # "result" + BF # map(*) + 6B # text(11) + 7374643A3A737472696E67 # "std::string" + 63 # text(3) + 323030 # "200" + FF # primitive(*) + FF # primitive(*) + FF # primitive(*) + BF # map(*) + 78 23 # text(35) + 746573745F76617269616E745F6175746F5F74616767696E673A3A4150494572726F72 # "test_variant_auto_tagging::APIError" + BF # map(*) + 65 # text(5) + 6572726F72 # "error" + BF # map(*) + 6B # text(11) + 7374643A3A737472696E67 # "std::string" + 68 # text(8) + 616E206572726F72 # "an error" + FF # primitive(*) + FF # primitive(*) + FF # primitive(*) + )"); +} + +// test 3 -> two structs with the same name in different namespaces should still +// be serializable because we're using rfl::AddNamespacedTagsToVariants + +namespace Result { +struct Message { + std::string result; +}; +} // namespace Result + +namespace Error { +struct Message { + std::string error; + int error_id; +}; +}; // namespace Error + +using Messages = std::variant; + +TEST(json, test_add_namespaced_tags_to_variants_different_namespaces) { + std::vector msgs{ + Result::Message{.result="a result"}, + Error::Message{.error="an error", .error_id=2}, + }; + write_and_read(msgs, + // Diagnostic notation: [{_ "test_variant_auto_tagging::Result::Message":{_ "result":"a result"}},{_ "test_variant_auto_tagging::Error::Message":{_ "error":"an error", "error_id":2}}] + // See: https://cbor.me/?diag=[{_%20%22test_variant_auto_tagging::Result::Message%22:{_%20%22result%22:%22a%20result%22}},{_%20%22test_variant_auto_tagging::Error::Message%22:{_%20%22error%22:%22an%20error%22,%20%22error_id%22:2}}] + R"( +82 # array(2) + BF # map(*) + 78 2A # text(42) + 746573745F76617269616E745F6175746F5F74616767696E673A3A526573756C743A3A4D657373616765 # "test_variant_auto_tagging::Result::Message" + BF # map(*) + 66 # text(6) + 726573756C74 # "result" + 68 # text(8) + 6120726573756C74 # "a result" + FF # primitive(*) + FF # primitive(*) + BF # map(*) + 78 29 # text(41) + 746573745F76617269616E745F6175746F5F74616767696E673A3A4572726F723A3A4D657373616765 # "test_variant_auto_tagging::Error::Message" + BF # map(*) + 65 # text(5) + 6572726F72 # "error" + 68 # text(8) + 616E206572726F72 # "an error" + 68 # text(8) + 6572726F725F6964 # "error_id" + 02 # unsigned(2) + FF # primitive(*) + FF # primitive(*) + )"); +} + +} // namespace test_variant_auto_tagging diff --git a/tests/cbor/write_and_read.hpp b/tests/cbor/write_and_read.hpp index 5a4cd180..ea011ea4 100644 --- a/tests/cbor/write_and_read.hpp +++ b/tests/cbor/write_and_read.hpp @@ -7,14 +7,85 @@ #include #include +// Helper function to parse hex bytes from CBOR diagnostic output +template +std::vector parse_cbor_diagnostic_hex(const std::string& cbor_diagnostic_hex) { + std::vector result; + std::istringstream iss{cbor_diagnostic_hex}; + std::string token; + + while (iss >> token) { + // Skip comments (lines starting with #) + if (token.starts_with("#")) { + // Skip the rest of the line + std::string line; + std::getline(iss, line); + continue; + } + + // Check if token is a valid hex string (even number of hex digits) + if (token.length() >= 2 && token.length() % 2 == 0 && + std::all_of(token.begin(), token.end(), [](char c) { + return std::isxdigit(c); + })) { + + // Process hex string in pairs of characters + for (size_t i = 0; i < token.length(); i += 2) { + std::string hex_byte = token.substr(i, 2); + CharT byte = static_cast( + std::stoul(hex_byte, nullptr, 16) + ); + result.push_back(byte); + } + } + } + + return result; +} + +// TODO: Ideally, we'd find a clean way to bundle the https://github.com/BlockchainCommons/bc-dcbor-cli +// utility and use it to diff the annotated hex output, but this will do for now. +template +std::string print_cbor_me_link( const std::vector& buffer ) +{ + std::ostringstream oss; + oss << "decode " << buffer.size() << " CBOR bytes at:\n https://cbor.me/?bytes="; + oss << std::setfill('0') << std::hex; + for ( auto b: buffer ) { + oss << std::setw(2) << (int32_t)(uint8_t) b; + } + oss << "\n"; + return oss.str(); +} + template -void write_and_read(const auto& _struct) { - using T = std::remove_cvref_t; - const auto serialized1 = rfl::cbor::write(_struct); +void write_and_read(const auto& object) { + using T = std::remove_cvref_t; + const auto serialized1 = rfl::cbor::write(object); const auto res = rfl::cbor::read(serialized1); EXPECT_TRUE(res && true) << "Test failed on read. Error: " << res.error().what(); const auto serialized2 = rfl::cbor::write(res.value()); - EXPECT_EQ(serialized1, serialized2); + EXPECT_EQ(serialized1, serialized2) + << "\nTest failed on write. For first pass, " << print_cbor_me_link(serialized1) + << "\nFor second pass, " << print_cbor_me_link(serialized2); } + +template +void write_and_read(const auto& object, const std::string& expected_diagnostic_hex) { + using T = std::remove_cvref_t; + const auto serialized1 = rfl::cbor::write(object); + const std::vector expected_cbor = parse_cbor_diagnostic_hex(expected_diagnostic_hex); + EXPECT_EQ(serialized1, expected_cbor) + << "\nTest failed on write. For expected, " << print_cbor_me_link(expected_cbor) + << "\nFor actual, " << print_cbor_me_link(serialized1); + const auto res = rfl::cbor::read(serialized1); + EXPECT_TRUE(res && true) << "Test failed on read. Error: " + << res.error().what(); + const auto serialized2 = rfl::cbor::write(res.value()); + EXPECT_EQ(serialized1, serialized2) + << "\nTest failed on write. For first pass, " << print_cbor_me_link(serialized1) + << "\nFor second pass, " << print_cbor_me_link(serialized2); +} + #endif diff --git a/tests/json/test_add_namespaced_tags_to_rfl_variants.cpp b/tests/json/test_add_namespaced_tags_to_rfl_variants.cpp new file mode 100644 index 00000000..ad6dc2c9 --- /dev/null +++ b/tests/json/test_add_namespaced_tags_to_rfl_variants.cpp @@ -0,0 +1,90 @@ +#include +#include +#include +#include +#include + +#include "write_and_read.hpp" + +// NOTE TO MAINTAINERS: +// These tests are very similar to the others in test_add_*tags_to_*variants.cpp, so please keep them +// in sync. +namespace test_add_namespaced_tags_to_rfl_variants { + +// test 1 -> normal behaviour + +struct button_pressed_t {}; + +// Test that we can still use structs that must be moved +struct button_released_t { + rfl::Box button; +}; + +struct key_pressed_t { + // Manually specified tags don't get the namespace prefixed + using Tag = rfl::Literal<"key_pressed">; + char key; +}; + +using my_event_type_t = + rfl::Variant; + +TEST(json, test_add_namespaced_tags_to_rfl_variants) { + std::vector vec; + vec.emplace_back(button_pressed_t{}); + vec.emplace_back(button_released_t{rfl::make_box(4)}); + vec.emplace_back(key_pressed_t{'c'}); + vec.emplace_back(3); + + write_and_read( + vec, + R"([{"test_add_namespaced_tags_to_rfl_variants::button_pressed_t":{}},{"test_add_namespaced_tags_to_rfl_variants::button_released_t":{"button":4}},{"key_pressed":{"key":99}},{"int":3}])"); +} + +// test 2 -> 'Generic' within a struct like this can be read/written with +// rfl::AddNamespacedTagsToVariants so that the underlying `std::variant` holds two 'Generic' fields +// with different namespaces. See the similar test in test_add_tags_to_variants.cpp. +// A hypothetical alternative would be adding an optional manual tag to rfl::Generic + +struct APIResult { + rfl::Generic result; +}; +struct APIError { + rfl::Generic error; +}; + +using APICallOutput = rfl::Variant; + +TEST(json, test_add_namespaced_tags_to_rfl_variants_with_generic) { + APICallOutput output = APIResult{"200"}; + write_and_read(output, + R"({"test_add_namespaced_tags_to_rfl_variants::APIResult":{"result":{"std::string":"200"}}})"); +} + +// test 3 -> two structs with the same name in different namespaces should still +// be serializable because we're using rfl::AddNamespacedTagsToVariants + +namespace Result { +struct Message { + std::string result; +}; +} // namespace Result + +namespace Error { +struct Message { + std::string error; + int error_id; +}; +}; // namespace Error + +using Messages = rfl::Variant; + +TEST(json, test_add_namespaced_tags_to_rfl_variants_different_namespaces) { + std::vector msgs{ + Result::Message{.result="a result"}, + Error::Message{.error="an error", .error_id=2}, + }; + write_and_read(msgs, + R"([{"test_add_namespaced_tags_to_rfl_variants::Result::Message":{"result":"a result"}},{"test_add_namespaced_tags_to_rfl_variants::Error::Message":{"error":"an error","error_id":2}}])"); +} +} // namespace test_add_namespaced_tags_to_rfl_variants diff --git a/tests/json/test_add_namespaced_tags_to_variants.cpp b/tests/json/test_add_namespaced_tags_to_variants.cpp new file mode 100644 index 00000000..338d0712 --- /dev/null +++ b/tests/json/test_add_namespaced_tags_to_variants.cpp @@ -0,0 +1,91 @@ +#include +#include +#include +#include +#include +#include + +#include "write_and_read.hpp" + +// NOTE TO MAINTAINERS: +// These tests are very similar to the others in test_add_*tags_to_*variants.cpp, so please keep them +// in sync. +namespace test_add_namespaced_tags_to_variants { + +// test 1 -> normal behaviour + +struct button_pressed_t {}; + +// Test that we can still use structs that must be moved +struct button_released_t { + rfl::Box button; +}; + +struct key_pressed_t { + // Manually specified tags don't get the namespace prefixed + using Tag = rfl::Literal<"key_pressed">; + char key; +}; + +using my_event_type_t = + std::variant; + +TEST(json, test_add_namespaced_tags_to_variants) { + std::vector vec; + vec.emplace_back(button_pressed_t{}); + vec.emplace_back(button_released_t{rfl::make_box(4)}); + vec.emplace_back(key_pressed_t{'c'}); + vec.emplace_back(3); + + write_and_read( + vec, + R"([{"test_add_namespaced_tags_to_variants::button_pressed_t":{}},{"test_add_namespaced_tags_to_variants::button_released_t":{"button":4}},{"key_pressed":{"key":99}},{"int":3}])"); +} + +// test 2 -> 'Generic' within a struct like this can be read/written with +// rfl::AddNamespacedTagsToVariants so that the underlying `std::variant` holds two 'Generic' fields +// with different namespaces. See the similar test in test_add_tags_to_variants.cpp. +// A hypothetical alternative would be adding an optional manual tag to rfl::Generic + +struct APIResult { + rfl::Generic result; +}; +struct APIError { + rfl::Generic error; +}; + +using APICallOutput = std::variant; + +TEST(json, test_add_namespaced_tags_to_variants_with_generic) { + APICallOutput output = APIResult{"200"}; + write_and_read(output, + R"({"test_add_namespaced_tags_to_variants::APIResult":{"result":{"std::string":"200"}}})"); +} + +// test 3 -> two structs with the same name in different namespaces should still +// be serializable because we're using rfl::AddNamespacedTagsToVariants + +namespace Result { +struct Message { + std::string result; +}; +} // namespace Result + +namespace Error { +struct Message { + std::string error; + int error_id; +}; +}; // namespace Error + +using Messages = std::variant; + +TEST(json, test_add_namespaced_tags_to_variants_different_namespaces) { + std::vector msgs{ + Result::Message{.result="a result"}, + Error::Message{.error="an error", .error_id=2}, + }; + write_and_read(msgs, + R"([{"test_add_namespaced_tags_to_variants::Result::Message":{"result":"a result"}},{"test_add_namespaced_tags_to_variants::Error::Message":{"error":"an error","error_id":2}}])"); +} +} // namespace test_add_namespaced_tags_to_variants diff --git a/tests/json/test_add_tag_to_rfl_variant.cpp b/tests/json/test_add_tag_to_rfl_variant.cpp deleted file mode 100644 index 265f72bb..00000000 --- a/tests/json/test_add_tag_to_rfl_variant.cpp +++ /dev/null @@ -1,39 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - -#include "write_and_read.hpp" - -namespace test_add_tag_to_rfl_variant { - -struct button_pressed_t {}; - -// Test that we can still use structs that must be moved -struct button_released_t { - rfl::Box button; -}; - -struct key_pressed_t { - using Tag = rfl::Literal<"key_pressed">; - char key; -}; - -using my_event_type_t = - rfl::Variant; - -TEST(json, test_add_tag_to_rfl_variant) { - std::vector vec; - vec.emplace_back(button_pressed_t{}); - vec.emplace_back(button_released_t{rfl::make_box(4)}); - vec.emplace_back(key_pressed_t{'c'}); - vec.emplace_back(3); - - write_and_read( - vec, - R"([{"button_pressed_t":{}},{"button_released_t":{"button":4}},{"key_pressed":{"key":99}},{"int":3}])"); -} -} // namespace test_add_tag_to_rfl_variant diff --git a/tests/json/test_add_tag_to_variant.cpp b/tests/json/test_add_tag_to_variant.cpp deleted file mode 100644 index 5ce9a953..00000000 --- a/tests/json/test_add_tag_to_variant.cpp +++ /dev/null @@ -1,33 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - -#include "write_and_read.hpp" - -namespace test_add_tag_to_variant { - -struct button_pressed_t {}; - -struct button_released_t {}; - -struct key_pressed_t { - using Tag = rfl::Literal<"key_pressed">; - char key; -}; - -using my_event_type_t = - std::variant; - -TEST(json, test_add_tag_to_variant) { - const auto vec = std::vector( - {button_pressed_t{}, button_released_t{}, key_pressed_t{'c'}, 3}); - - write_and_read( - vec, - R"([{"button_pressed_t":{}},{"button_released_t":{}},{"key_pressed":{"key":99}},{"int":3}])"); -} -} // namespace test_add_tag_to_variant diff --git a/tests/json/test_add_tags_to_rfl_variants.cpp b/tests/json/test_add_tags_to_rfl_variants.cpp new file mode 100644 index 00000000..5afbc4c1 --- /dev/null +++ b/tests/json/test_add_tags_to_rfl_variants.cpp @@ -0,0 +1,93 @@ +#include +#include +#include +#include +#include + +#include "write_and_read.hpp" + +// NOTE TO MAINTAINERS: +// These tests are very similar to the others in test_add_*tags_to_*variants.cpp, so please keep them +// in sync. +namespace test_add_tags_to_rfl_variants { + +// test 1 -> normal behaviour + +struct button_pressed_t {}; + +// Test that we can still use structs that must be moved +struct button_released_t { + rfl::Box button; +}; + +struct key_pressed_t { + using Tag = rfl::Literal<"key_pressed">; + char key; +}; + +using my_event_type_t = + rfl::Variant; + +TEST(json, test_add_tags_to_rfl_variants) { + std::vector vec; + vec.emplace_back(button_pressed_t{}); + vec.emplace_back(button_released_t{rfl::make_box(4)}); + vec.emplace_back(key_pressed_t{'c'}); + vec.emplace_back(3); + + write_and_read( + vec, + R"([{"button_pressed_t":{}},{"button_released_t":{"button":4}},{"key_pressed":{"key":99}},{"int":3}])"); +} + +// test 2 -> 'Generic' within a struct like this cannot be read/written without using +// rfl::AddNamespacedTagsToVariants due to the underlying `rfl::Variant` holding two fields with +// name 'Generic'. See the similar test in test_add_namespaced_tags_to_rfl_variant.cpp +// A hypothetical solution would be adding an optional manual tag to rfl::Generic + +// struct APIResult { +// rfl::Generic result; +// }; +// struct APIError { +// rfl::Generic error; +// }; + +// using APICallOutput = rfl::Variant; + +// TEST(json, test_add_tags_to_rfl_variants_with_generic) { +// APICallOutput output = APIResult{"200"}; +// write_and_read(output, +// R"({"APIResult":{"result":{"std::string":"200"}}})"); +// } + +// test 3 -> two structs with the same name in different namespaces should still +// be serializable without rfl::AddNamespacedTagsToVariants, but only with manual tags + +namespace Result { +struct Message { + // Avoid duplicate typenames from different namespaces using manual tag + using Tag = rfl::Literal<"result_msg">; + std::string result; +}; +} // namespace Result + +namespace Error { +struct Message { + // Avoid duplicate typenames from different namespaces using manual tag + using Tag = rfl::Literal<"error_msg">; + std::string error; + int error_id; +}; +}; // namespace Error + +using Messages = rfl::Variant; + +TEST(json, test_add_tags_to_rfl_variants_different_namespaces) { + std::vector msgs{ + Result::Message{.result="a result"}, + Error::Message{.error="an error", .error_id=2}, + }; + write_and_read(msgs, + R"([{"result_msg":{"result":"a result"}},{"error_msg":{"error":"an error","error_id":2}}])"); +} +} // namespace test_add_tags_to_rfl_variants diff --git a/tests/json/test_add_tags_to_variants.cpp b/tests/json/test_add_tags_to_variants.cpp new file mode 100644 index 00000000..af7595bc --- /dev/null +++ b/tests/json/test_add_tags_to_variants.cpp @@ -0,0 +1,94 @@ +#include +#include +#include +#include +#include +#include + +#include "write_and_read.hpp" + +// NOTE TO MAINTAINERS: +// These tests are very similar to the others in test_add_*tags_to_*variants.cpp, so please keep them +// in sync. +namespace test_add_tags_to_variants { + +// test 1 -> normal behaviour + +struct button_pressed_t {}; + +// Test that we can still use structs that must be moved +struct button_released_t { + rfl::Box button; +}; + +struct key_pressed_t { + using Tag = rfl::Literal<"key_pressed">; + char key; +}; + +using my_event_type_t = + std::variant; + +TEST(json, test_add_tags_to_variants) { + std::vector vec; + vec.emplace_back(button_pressed_t{}); + vec.emplace_back(button_released_t{rfl::make_box(4)}); + vec.emplace_back(key_pressed_t{'c'}); + vec.emplace_back(3); + + write_and_read( + vec, + R"([{"button_pressed_t":{}},{"button_released_t":{"button":4}},{"key_pressed":{"key":99}},{"int":3}])"); +} + +// test 2 -> 'Generic' within a struct like this cannot be read/written without using +// rfl::AddNamespacedTagsToVariants due to the underlying `std::variant` holding two fields with +// name 'Generic'. See the similar test in test_add_namespaced_tags_to_variants.cpp. +// A hypothetical solution would be adding an optional manual tag to rfl::Generic + +// struct APIResult { +// rfl::Generic result; +// }; +// struct APIError { +// rfl::Generic error; +// }; + +// using APICallOutput = std::variant; + +// TEST(json, test_add_tags_to_variants_with_generic) { +// APICallOutput output = APIResult{"200"}; +// write_and_read(output, +// R"({"APIResult":{"result":{"std::string":"200"}}})"); +// } + +// test 3 -> two structs with the same name in different namespaces should still +// be serializable without rfl::AddNamespacedTagsToVariants, but only with manual tags + +namespace Result { +struct Message { + // Avoid duplicate typenames from different namespaces using manual tag + using Tag = rfl::Literal<"result_msg">; + std::string result; +}; +} // namespace Result + +namespace Error { +struct Message { + // Avoid duplicate typenames from different namespaces using manual tag + using Tag = rfl::Literal<"error_msg">; + std::string error; + int error_id; +}; +}; // namespace Error + +using Messages = std::variant; + +TEST(json, test_add_tags_to_variants_different_namespaces) { + std::vector msgs{ + Result::Message{.result="a result"}, + Error::Message{.error="an error", .error_id=2}, + }; + write_and_read(msgs, + R"([{"result_msg":{"result":"a result"}},{"error_msg":{"error":"an error","error_id":2}}])"); +} +} // namespace test_add_tags_to_variants