From 59794baa1f5e47a94a741af52b467d99d8069a88 Mon Sep 17 00:00:00 2001 From: EJainDev Date: Sat, 26 Sep 2026 09:40:37 -0400 Subject: [PATCH 1/2] feat: support mocking unmodifiable classes --- README.md | 34 +++++++++++++++ examples/CMakeLists.txt | 3 ++ examples/mock_vector.cpp | 24 +++++++++++ include/serial_xml.cxx | 88 +++++++++++++++++++++++++++++++++++---- tests/test_serial_xml.cpp | 29 +++++++++++++ 5 files changed, 171 insertions(+), 7 deletions(-) create mode 100644 examples/mock_vector.cpp diff --git a/README.md b/README.md index 38fae94..1e3679b 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,39 @@ This is the complete list of annotations: - `[[=cdata]]` -- Emit the value inside `cdata` (``) tags. - `[[=exclude_on_empty]]` -- Do not emit any tags when the range is empty +### Mock classes for types you cannot annotate + +Specialize `serial_xml::mock_type` to associate a type with a mock class. The +mock's annotated, public, const functions describe the XML fields. SerialXML +calls functions with matching names and compatible return types on the actual +object; the mock functions only need declarations. This works for third-party +types and standard library types whose declarations cannot be modified. + +```cpp +struct vector { + [[=serial_xml::attribute, =serial_xml::name{"count"}]] std::size_t size() const; + [[=serial_xml::name{"is_empty"}]] bool empty() const; +}; + +template +struct serial_xml::mock_type> { + using type = vector; +}; + +struct document { std::vector values; }; +// false +serial_xml::to_xml(document{{1, 2, 3}}, false); +``` + +Only annotated mock members are serialized. Each must be a non-void, const +function callable without arguments. A matching target function must be public, +const, callable without arguments, and return a type convertible to the mock +function's return type. Missing or ambiguous matches produce a compile-time +diagnostic. Registration is by type through `mock_type`; sharing a class name +alone does not activate a mock. Explicit registration takes precedence over +automatic STL iteration for the registered type, including nested members. To +retain automatic iteration, leave the type unregistered. + ### Common Confusion Points 1. For *some* STL containers, the library automatically iterates through them. Therefore, your generated XML will not match the expectations. To avoid this, add the `[[=no_iter]]` annotation to object member. The current list of STL containers that are automatically iterated: @@ -193,6 +226,7 @@ Set `-D BUILD_EXAMPLES=OFF` when configuring CMake to omit them from your build. | [`format_specifiers.cpp`](examples/format_specifiers.cpp) | Passing format specifications through to `std::format`. | | [`escaping.cpp`](examples/escaping.cpp) | Escaping XML-special characters in children, attributes, and raw text. | | [`advanced_mixed.cpp`](examples/advanced_mixed.cpp) | A combined example using attributes, named tags, nesting, iteration, and escaping. | +| [`mock_vector.cpp`](examples/mock_vector.cpp) | Registering a mock class for `std::vector` and serializing its accessors. | ## Contributing diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index b94f85c..a07f860 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -33,3 +33,6 @@ target_link_libraries(advanced_mixed PRIVATE serial_xml) add_executable(optional_types optional_types.cpp) target_link_libraries(optional_types PRIVATE serial_xml) + +add_executable(mock_vector mock_vector.cpp) +target_link_libraries(mock_vector PRIVATE serial_xml) diff --git a/examples/mock_vector.cpp b/examples/mock_vector.cpp new file mode 100644 index 0000000..425f2ec --- /dev/null +++ b/examples/mock_vector.cpp @@ -0,0 +1,24 @@ +#include + +import std; + +import serial_xml; + +// This mock describes the XML view of std::vector without changing std::vector. +// The declarations need no definitions: SerialXML calls the matching functions +// on the actual std::vector instance. +struct vector { + [[ = serial_xml::attribute, = serial_xml::name{"count"} ]] std::size_t size() const; + [[= serial_xml::name{"is_empty"}]] bool empty() const; +}; + +template +struct serial_xml::mock_type> { + using type = vector; +}; + +struct document { + std::vector values; +}; + +int main() { std::cout << serial_xml::to_xml(document{{10, 20, 30}}) << '\n'; } diff --git a/include/serial_xml.cxx b/include/serial_xml.cxx index 87d82b1..f0642c4 100644 --- a/include/serial_xml.cxx +++ b/include/serial_xml.cxx @@ -45,6 +45,15 @@ export constexpr cdata_ cdata; struct exclude_on_empty_ {}; export constexpr exclude_on_empty_ exclude_on_empty; +// Specialize for an existing type to serialize it through an annotated mock class. +export template +struct mock_type { + using type = void; +}; + +template +inline constexpr bool has_mock_type = !std::is_void_v::type>; + constexpr bool is_alpha(const char c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); } constexpr bool is_num(const char c) { return (c >= '0' && c <= '9'); } @@ -171,10 +180,50 @@ inline decltype(auto) get_value(auto&& s) { return (s.[:m:]); } +template + requires(std::meta::is_function(mock_member)) +consteval auto find_mock_function() -> std::pair { + static constexpr auto actual_members = + std::define_static_array(std::meta::members_of(^^T, std::meta::access_context::current())); + std::meta::info match = ^^void; + std::size_t count = 0; + + template for (constexpr auto candidate : actual_members) { + if constexpr (std::meta::is_function(candidate) && !std::meta::is_constructor(candidate) && + !std::meta::is_destructor(candidate)) { + if constexpr (std::meta::has_identifier(candidate) && + std::meta::identifier_of(candidate) == std::meta::identifier_of(mock_member) && + std::meta::is_const(candidate) && std::meta::parameters_of(candidate).empty() && + std::is_convertible_v) { + match = candidate; + ++count; + } + } + } + return {match, count}; +} + template requires(std::meta::is_function(m)) inline decltype(auto) get_value(auto&& s) { - return s.[:m:](); + using T = std::remove_cvref_t; + if constexpr (has_mock_type) { + static constexpr auto match = find_mock_function(); + static_assert(match.second != 0, + "No compatible public const zero-argument function on the registered type for " + "mock function " + + std::string(std::meta::identifier_of(m))); + static_assert(match.second <= 1, + "Ambiguous public const zero-argument functions on the registered type for " + "mock function " + + std::string(std::meta::identifier_of(m))); + if constexpr (match.second == 1) { + return s.[:match.first:](); + } + } else { + return s.[:m:](); + } } template @@ -198,6 +247,9 @@ constexpr auto value_m_t = value_type::m_t; template consteval bool is_stl_handled() { + if constexpr (has_mock_type) { + return false; + } if constexpr (get_namespace() == ^^std) { static constexpr auto m_t = std::meta::template_of(std::meta::dealias(m)); @@ -222,8 +274,9 @@ consteval auto get_annotations() bool is_no_iter = !is_stl_handled>(); bool is_raw = false; bool is_skip = false; - bool is_unpack = std::meta::is_class_type(value_m_t) && !std::formattable, char> && - !is_stl_handled>(); + bool is_unpack = std::meta::is_class_type(value_m_t) && + (has_mock_type> || + (!std::formattable, char> && !is_stl_handled>())); bool has_custom_format_function = false; std::optional> iter_names; @@ -359,8 +412,16 @@ consteval auto is_invalid_function() { template consteval auto get_members() { - static constexpr auto members = std::define_static_array( - std::meta::members_of(container, std::meta::access_context::current())); + using T = typename[:container:]; + static constexpr auto source = [] { + if constexpr (has_mock_type) { + return ^^typename mock_type::type; + } else { + return container; + } + }(); + static constexpr auto members = + std::define_static_array(std::meta::members_of(source, std::meta::access_context::current())); std::vector< std::pair(); + if constexpr (has_mock_type) { + if constexpr (!std::meta::annotations_of(m).empty()) { + static_assert(std::meta::is_function(m) && !invalid_function, + "Annotated mock members must be const, non-void functions callable without " + "arguments"); + } + if constexpr (std::meta::annotations_of(m).empty()) { + continue; + } + } + if constexpr (!invalid_function) { static constexpr auto m_annotations = get_annotations(); @@ -628,7 +700,8 @@ void add_attribute(std::string& result, std::string& buffer, const auto& value) static constexpr auto prefix_result = get_attribute_prefix(); static constexpr auto prefix = std::get<0>(prefix_result); static constexpr auto prefix_size = std::get<1>(prefix_result); - if constexpr (formatter.is_empty() && std::is_arithmetic_v && sizeof(T) <= 64) { + if constexpr (formatter.is_empty() && std::is_arithmetic_v && !std::is_same_v && + sizeof(T) <= 64) { if constexpr (std::is_floating_point_v) { static constexpr auto format_resize = 311 + prefix_size + 1; @@ -730,7 +803,8 @@ void add_child(std::string& result, std::string& buffer, const auto& value) { const auto original_size = result.size(); - if constexpr (formatter.is_empty() && std::is_arithmetic_v && sizeof(T) <= 64) { + if constexpr (formatter.is_empty() && std::is_arithmetic_v && !std::is_same_v && + sizeof(T) <= 64) { if constexpr (std::is_floating_point_v) { static constexpr auto format_resize = 311 + combined_size; result.resize_and_overwrite(original_size + format_resize, diff --git a/tests/test_serial_xml.cpp b/tests/test_serial_xml.cpp index a637cbc..caa44de 100644 --- a/tests/test_serial_xml.cpp +++ b/tests/test_serial_xml.cpp @@ -6,6 +6,35 @@ import serial_xml; std::string clean_to_xml(auto obj) { return serial_xml::to_xml(obj, false); } +// The declaration can live outside the namespace of the unmodifiable type. +// Its member functions are never defined or called on a mock instance. +struct vector { + [[ = serial_xml::attribute, = serial_xml::name{"count"} ]] std::size_t size() const; + [[= serial_xml::name{"is_empty"}]] bool empty() const; + int ignored() const; +}; + +template <> +struct serial_xml::mock_type> { + using type = vector; +}; + +TEST(Mock, RegisteredVectorRoot) { + std::vector values{2, 4, 6}; + ASSERT_EQ(serial_xml::to_xml(values, false, "numbers"), + "false"); +} + +TEST(Mock, RegisteredVectorMemberOverridesBuiltInIteration) { + struct VectorEnvelope { + std::vector values; + }; + + ASSERT_EQ(clean_to_xml(VectorEnvelope{{2, 4, 6}}), + "false"); +} + TEST(Naming, EmptyWithName) { struct[[= serial_xml::name{"MyStruct"}]] EmptyName {}; EmptyName obj; From b2a5029fc3e7a33cdb62989779ea9b23949b9385 Mon Sep 17 00:00:00 2001 From: EJainDev Date: Sat, 26 Sep 2026 17:30:24 -0400 Subject: [PATCH 2/2] fix: select mock views through annotations --- README.md | 40 ++++++------ examples/mock_vector.cpp | 21 +++---- include/serial_xml.cxx | 125 +++++++++++++++++++------------------- tests/test_serial_xml.cpp | 37 ++++++----- 4 files changed, 115 insertions(+), 108 deletions(-) diff --git a/README.md b/README.md index 1e3679b..e44cbae 100644 --- a/README.md +++ b/README.md @@ -126,36 +126,36 @@ This is the complete list of annotations: ### Mock classes for types you cannot annotate -Specialize `serial_xml::mock_type` to associate a type with a mock class. The -mock's annotated, public, const functions describe the XML fields. SerialXML -calls functions with matching names and compatible return types on the actual -object; the mock functions only need declarations. This works for third-party -types and standard library types whose declarations cannot be modified. +Annotate a field with `serial_xml::mock` to select its XML view. In the +mock class, annotate each serialized function with `serial_xml::accessor<&T::function>` +to identify the real function to call. The mock functions only need declarations. +This works for third-party and standard library types whose declarations cannot +be modified. ```cpp -struct vector { - [[=serial_xml::attribute, =serial_xml::name{"count"}]] std::size_t size() const; - [[=serial_xml::name{"is_empty"}]] bool empty() const; +template +struct vector_mock { + [[=serial_xml::accessor<&T::size>{}, =serial_xml::attribute, + =serial_xml::name{"count"}]] std::size_t size() const; + [[=serial_xml::accessor<&T::empty>{}, =serial_xml::name{"is_empty"}]] bool empty() const; }; -template -struct serial_xml::mock_type> { - using type = vector; +struct document { + [[=serial_xml::mock>>{}]] std::vector values; }; -struct document { std::vector values; }; // false serial_xml::to_xml(document{{1, 2, 3}}, false); ``` Only annotated mock members are serialized. Each must be a non-void, const -function callable without arguments. A matching target function must be public, -const, callable without arguments, and return a type convertible to the mock -function's return type. Missing or ambiguous matches produce a compile-time -diagnostic. Registration is by type through `mock_type`; sharing a class name -alone does not activate a mock. Explicit registration takes precedence over -automatic STL iteration for the registered type, including nested members. To -retain automatic iteration, leave the type unregistered. +function callable without arguments, with an accessor annotation naming a +compatible callable on the actual object. The accessor resolves directly at +compile time, including overload selection through an explicit cast when needed. +The mock annotation takes precedence over automatic STL iteration for that +field. Other fields of the same type retain their usual serialization. To mock +the root value, pass the mock as a template argument to `to_xml`, such as +`serial_xml::to_xml>>(values)`. ### Common Confusion Points @@ -226,7 +226,7 @@ Set `-D BUILD_EXAMPLES=OFF` when configuring CMake to omit them from your build. | [`format_specifiers.cpp`](examples/format_specifiers.cpp) | Passing format specifications through to `std::format`. | | [`escaping.cpp`](examples/escaping.cpp) | Escaping XML-special characters in children, attributes, and raw text. | | [`advanced_mixed.cpp`](examples/advanced_mixed.cpp) | A combined example using attributes, named tags, nesting, iteration, and escaping. | -| [`mock_vector.cpp`](examples/mock_vector.cpp) | Registering a mock class for `std::vector` and serializing its accessors. | +| [`mock_vector.cpp`](examples/mock_vector.cpp) | Annotating a `std::vector` field with a mock that serializes its accessors. | ## Contributing diff --git a/examples/mock_vector.cpp b/examples/mock_vector.cpp index 425f2ec..d9ea686 100644 --- a/examples/mock_vector.cpp +++ b/examples/mock_vector.cpp @@ -4,21 +4,18 @@ import std; import serial_xml; -// This mock describes the XML view of std::vector without changing std::vector. -// The declarations need no definitions: SerialXML calls the matching functions -// on the actual std::vector instance. -struct vector { - [[ = serial_xml::attribute, = serial_xml::name{"count"} ]] std::size_t size() const; - [[= serial_xml::name{"is_empty"}]] bool empty() const; -}; - -template -struct serial_xml::mock_type> { - using type = vector; +// The mock describes the XML view and points to the actual accessors. +template +struct vector_mock { + [[ + = serial_xml::accessor<&T::size>{}, = serial_xml::attribute, + = serial_xml::name{"count"} + ]] std::size_t size() const; + [[ = serial_xml::accessor<&T::empty>{}, = serial_xml::name{"is_empty"} ]] bool empty() const; }; struct document { - std::vector values; + [[= serial_xml::mock>>{}]] std::vector values; }; int main() { std::cout << serial_xml::to_xml(document{{10, 20, 30}}) << '\n'; } diff --git a/include/serial_xml.cxx b/include/serial_xml.cxx index f0642c4..d9d73ea 100644 --- a/include/serial_xml.cxx +++ b/include/serial_xml.cxx @@ -45,14 +45,29 @@ export constexpr cdata_ cdata; struct exclude_on_empty_ {}; export constexpr exclude_on_empty_ exclude_on_empty; -// Specialize for an existing type to serialize it through an annotated mock class. export template -struct mock_type { - using type = void; +struct mock {}; + +export template +struct accessor { + static constexpr auto value = Member; }; template -inline constexpr bool has_mock_type = !std::is_void_v::type>; +concept mock_class = std::is_class_v; + +template +consteval std::meta::info mock_of() { + static constexpr auto annotations = std::define_static_array(std::meta::annotations_of(m)); + template for (constexpr auto a : annotations) { + static constexpr auto a_t = std::meta::type_of(a); + if constexpr (std::meta::has_template_arguments(a_t) && + std::meta::template_of(a_t) == ^^::serial_xml::mock) { + return std::meta::template_arguments_of(a_t)[0]; + } + } + return ^^void; +} constexpr bool is_alpha(const char c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); } @@ -175,51 +190,39 @@ consteval std::meta::info get_namespace() { return m; } -template +template inline decltype(auto) get_value(auto&& s) { return (s.[:m:]); } -template - requires(std::meta::is_function(mock_member)) -consteval auto find_mock_function() -> std::pair { - static constexpr auto actual_members = - std::define_static_array(std::meta::members_of(^^T, std::meta::access_context::current())); - std::meta::info match = ^^void; - std::size_t count = 0; - - template for (constexpr auto candidate : actual_members) { - if constexpr (std::meta::is_function(candidate) && !std::meta::is_constructor(candidate) && - !std::meta::is_destructor(candidate)) { - if constexpr (std::meta::has_identifier(candidate) && - std::meta::identifier_of(candidate) == std::meta::identifier_of(mock_member) && - std::meta::is_const(candidate) && std::meta::parameters_of(candidate).empty() && - std::is_convertible_v) { - match = candidate; - ++count; - } +template +consteval std::meta::info accessor_of() { + static constexpr auto annotations = std::define_static_array(std::meta::annotations_of(m)); + template for (constexpr auto a : annotations) { + static constexpr auto a_t = std::meta::type_of(a); + if constexpr (std::meta::has_template_arguments(a_t) && + std::meta::template_of(a_t) == ^^::serial_xml::accessor) { + return std::meta::template_arguments_of(a_t)[0]; } } - return {match, count}; + return ^^void; } -template +template requires(std::meta::is_function(m)) inline decltype(auto) get_value(auto&& s) { - using T = std::remove_cvref_t; - if constexpr (has_mock_type) { - static constexpr auto match = find_mock_function(); - static_assert(match.second != 0, - "No compatible public const zero-argument function on the registered type for " - "mock function " + - std::string(std::meta::identifier_of(m))); - static_assert(match.second <= 1, - "Ambiguous public const zero-argument functions on the registered type for " - "mock function " + - std::string(std::meta::identifier_of(m))); - if constexpr (match.second == 1) { - return s.[:match.first:](); + if constexpr (mock_class) { + static constexpr auto member = accessor_of(); + static_assert(member != ^^void, "Annotated mock functions require serial_xml::accessor"); + if constexpr (member != ^^void) { + static_assert(std::is_invocable_v, + "Mock accessor must be callable on the serialized object"); + if constexpr (std::is_invocable_v) { + static_assert(std::is_convertible_v, + typename[:std::meta::return_type_of(m):]>, + "Mock accessor result must be convertible to the declared return type"); + return std::invoke([:member:], s); + } } } else { return s.[:m:](); @@ -247,9 +250,6 @@ constexpr auto value_m_t = value_type::m_t; template consteval bool is_stl_handled() { - if constexpr (has_mock_type) { - return false; - } if constexpr (get_namespace() == ^^std) { static constexpr auto m_t = std::meta::template_of(std::meta::dealias(m)); @@ -275,7 +275,7 @@ consteval auto get_annotations() bool is_raw = false; bool is_skip = false; bool is_unpack = std::meta::is_class_type(value_m_t) && - (has_mock_type> || + (mock_of() != ^^void || (!std::formattable, char> && !is_stl_handled>())); bool has_custom_format_function = false; @@ -410,12 +410,11 @@ consteval auto is_invalid_function() { return false; } -template +template consteval auto get_members() { - using T = typename[:container:]; static constexpr auto source = [] { - if constexpr (has_mock_type) { - return ^^typename mock_type::type; + if constexpr (mock_class) { + return ^^Mock; } else { return container; } @@ -434,7 +433,7 @@ consteval auto get_members() { template for (constexpr auto m : members) { static constexpr auto invalid_function = is_invalid_function(); - if constexpr (has_mock_type) { + if constexpr (mock_class) { if constexpr (!std::meta::annotations_of(m).empty()) { static_assert(std::meta::is_function(m) && !invalid_function, "Annotated mock members must be const, non-void functions callable without " @@ -879,7 +878,7 @@ void add_child(std::string& result, std::string& buffer, const auto& value) { } } -template +template requires(std::is_class_v) void to_xml(const T& value, std::string& result, std::string& buffer, bool first, const std::string& fixed_name = ""); @@ -951,7 +950,7 @@ auto handle_stl(std::string& result, std::string& buffer, const auto& value) -> return false; } -template +template requires(std::is_class_v) void to_xml(const T& value, std::string& result, std::string& buffer, bool first, const std::string& fixed_name) { @@ -983,7 +982,7 @@ void to_xml(const T& value, std::string& result, std::string& buffer, bool first std::string name{buffer}; std::format_to(std::back_inserter(result), "<{}", name); - static constexpr auto members = get_members(); + static constexpr auto members = get_members(); static constexpr auto attribute_annotations = members.first; static constexpr auto child_annotations = members.second; @@ -1006,10 +1005,10 @@ void to_xml(const T& value, std::string& result, std::string& buffer, bool first std::string("Invalid XML name: '") + std::string(view_name) + "'"); if constexpr (is_std) { - handle_stl(result, buffer, - get_value(value)); + handle_stl( + result, buffer, get_value(value)); } else { - add_attribute(result, buffer, get_value(value)); + add_attribute(result, buffer, get_value(value)); } } @@ -1019,7 +1018,8 @@ void to_xml(const T& value, std::string& result, std::string& buffer, bool first result += '>'; template for (constexpr auto m_a : child_annotations) { - static constexpr auto is_std = is_stl_handled(); + static constexpr auto is_std = + is_stl_handled() && mock_of() == ^^void; static constexpr auto m = m_a.first; static constexpr auto m_annotations = m_a.second; @@ -1048,7 +1048,7 @@ void to_xml(const T& value, std::string& result, std::string& buffer, bool first if constexpr (iter_names.first == nullptr && is_std && !is_no_iter) { handle_stl(result, buffer, get_value(value)); + formatter>(result, buffer, get_value(value)); } else { if constexpr (iter_names.first != nullptr && std::meta::is_class_type(value_m_t) && std::ranges::range>) { @@ -1058,7 +1058,7 @@ void to_xml(const T& value, std::string& result, std::string& buffer, bool first result.push_back('>'); } - for (const auto& item : get_value(value)) { + for (const auto& item : get_value(value)) { if constexpr (is_unpack) { to_xml(item, result, buffer, false, iter_names.first); } else { @@ -1072,10 +1072,11 @@ void to_xml(const T& value, std::string& result, std::string& buffer, bool first result.push_back('>'); } } else if constexpr (is_unpack) { - to_xml(get_value(value), result, buffer, false, m_name); + to_xml():]>(get_value(value), result, buffer, false, + m_name); } else { if constexpr (is_raw) { - buffer = format_value(get_value(value)); + buffer = format_value(get_value(value)); auto padded_size = get_escape_bitmask(buffer); @@ -1089,7 +1090,7 @@ void to_xml(const T& value, std::string& result, std::string& buffer, bool first return original_size + copy_with_escapes(buf, buffer, padded_size); }); } else { - add_child(result, buffer, get_value(value)); + add_child(result, buffer, get_value(value)); } } } @@ -1107,7 +1108,7 @@ void to_xml(const T& value, std::string& result, std::string& buffer, bool first } } -export template +export template requires(std::is_class_v) auto to_xml(const T& value, bool first = true, const std::string& fixed_name = "") -> std::string { std::string result; @@ -1116,7 +1117,7 @@ auto to_xml(const T& value, bool first = true, const std::string& fixed_name = " result.reserve(4096); buffer.reserve(256); - to_xml(value, result, buffer, first, fixed_name); + to_xml(value, result, buffer, first, fixed_name); escape_flags.clear(); escape_flags.shrink_to_fit(); diff --git a/tests/test_serial_xml.cpp b/tests/test_serial_xml.cpp index caa44de..8ad233c 100644 --- a/tests/test_serial_xml.cpp +++ b/tests/test_serial_xml.cpp @@ -6,28 +6,26 @@ import serial_xml; std::string clean_to_xml(auto obj) { return serial_xml::to_xml(obj, false); } -// The declaration can live outside the namespace of the unmodifiable type. -// Its member functions are never defined or called on a mock instance. -struct vector { - [[ = serial_xml::attribute, = serial_xml::name{"count"} ]] std::size_t size() const; - [[= serial_xml::name{"is_empty"}]] bool empty() const; +// The mock supplies XML annotations and points directly to the real accessors. +struct vector_mock { + [[ + = serial_xml::accessor<&std::vector::size>{}, = serial_xml::attribute, + = serial_xml::name{"count"} + ]] std::size_t size() const; + [[ = serial_xml::accessor<&std::vector::empty>{}, + = serial_xml::name{"is_empty"} ]] bool empty() const; int ignored() const; }; -template <> -struct serial_xml::mock_type> { - using type = vector; -}; - -TEST(Mock, RegisteredVectorRoot) { +TEST(Mock, ExplicitVectorRoot) { std::vector values{2, 4, 6}; - ASSERT_EQ(serial_xml::to_xml(values, false, "numbers"), + ASSERT_EQ(serial_xml::to_xml(values, false, "numbers"), "false"); } -TEST(Mock, RegisteredVectorMemberOverridesBuiltInIteration) { +TEST(Mock, AnnotatedVectorMemberOverridesBuiltInIteration) { struct VectorEnvelope { - std::vector values; + [[= serial_xml::mock{}]] std::vector values; }; ASSERT_EQ(clean_to_xml(VectorEnvelope{{2, 4, 6}}), @@ -35,6 +33,17 @@ TEST(Mock, RegisteredVectorMemberOverridesBuiltInIteration) { "VectorEnvelope>"); } +TEST(Mock, AnnotationAppliesOnlyToSelectedMember) { + struct MixedVectorEnvelope { + [[= serial_xml::mock{}]] std::vector summary; + std::vector values; + }; + + ASSERT_EQ(clean_to_xml(MixedVectorEnvelope{{2, 4}, {6, 8}}), + "false" + "68"); +} + TEST(Naming, EmptyWithName) { struct[[= serial_xml::name{"MyStruct"}]] EmptyName {}; EmptyName obj;