diff --git a/README.md b/README.md index 38fae94..e44cbae 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 + +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 +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 { + [[=serial_xml::mock>>{}]] 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, 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 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) | Annotating a `std::vector` field with a mock that serializes 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..d9ea686 --- /dev/null +++ b/examples/mock_vector.cpp @@ -0,0 +1,21 @@ +#include + +import std; + +import serial_xml; + +// 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 { + [[= 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 87d82b1..d9d73ea 100644 --- a/include/serial_xml.cxx +++ b/include/serial_xml.cxx @@ -45,6 +45,30 @@ export constexpr cdata_ cdata; struct exclude_on_empty_ {}; export constexpr exclude_on_empty_ exclude_on_empty; +export template +struct mock {}; + +export template +struct accessor { + static constexpr auto value = Member; +}; + +template +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'); } constexpr bool is_num(const char c) { return (c >= '0' && c <= '9'); } @@ -166,15 +190,43 @@ consteval std::meta::info get_namespace() { return m; } -template +template inline decltype(auto) get_value(auto&& s) { return (s.[:m:]); } 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 ^^void; +} + +template requires(std::meta::is_function(m)) inline decltype(auto) get_value(auto&& s) { - return s.[:m:](); + 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:](); + } } template @@ -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) && + (mock_of() != ^^void || + (!std::formattable, char> && !is_stl_handled>())); bool has_custom_format_function = false; std::optional> iter_names; @@ -357,10 +410,17 @@ consteval auto is_invalid_function() { return false; } -template +template consteval auto get_members() { - static constexpr auto members = std::define_static_array( - std::meta::members_of(container, std::meta::access_context::current())); + static constexpr auto source = [] { + if constexpr (mock_class) { + return ^^Mock; + } 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 (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 " + "arguments"); + } + if constexpr (std::meta::annotations_of(m).empty()) { + continue; + } + } + if constexpr (!invalid_function) { static constexpr auto m_annotations = get_annotations(); @@ -628,7 +699,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 +802,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, @@ -805,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 = ""); @@ -877,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) { @@ -909,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; @@ -932,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)); } } @@ -945,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; @@ -974,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>) { @@ -984,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 { @@ -998,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); @@ -1015,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)); } } } @@ -1033,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; @@ -1042,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 a637cbc..8ad233c 100644 --- a/tests/test_serial_xml.cpp +++ b/tests/test_serial_xml.cpp @@ -6,6 +6,44 @@ import serial_xml; std::string clean_to_xml(auto obj) { return serial_xml::to_xml(obj, false); } +// 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; +}; + +TEST(Mock, ExplicitVectorRoot) { + std::vector values{2, 4, 6}; + ASSERT_EQ(serial_xml::to_xml(values, false, "numbers"), + "false"); +} + +TEST(Mock, AnnotatedVectorMemberOverridesBuiltInIteration) { + struct VectorEnvelope { + [[= serial_xml::mock{}]] std::vector values; + }; + + ASSERT_EQ(clean_to_xml(VectorEnvelope{{2, 4, 6}}), + "false"); +} + +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;