From 3f0c4d02cad26cd493588c68df4b9c89a2af3813 Mon Sep 17 00:00:00 2001 From: EJainDev Date: Sat, 26 Sep 2026 17:09:09 -0400 Subject: [PATCH] feat(stl): support more STL classes and better representations --- README.md | 13 +--- examples/stl_containers.cpp | 7 ++ include/serial_xml.cxx | 129 +++++++++++++++++++++++++----------- tests/test_serial_xml.cpp | 44 ++++++++++++ 4 files changed, 143 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 38fae94..deb3e82 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ The main reason this library is so much faster is because all of the above ones This library is annotation driven, which means that most customization points are exposed via C++26 annotations. The reason behind this design choice is to create consistency and visually associate the output structure to the definition. -By default, all members are treated as children of the parent struct with closing tags the same as the name of the member. However, if a struct is not formattable, it by default is unpacked. Many STL ranges and the `std::optional` container are also handled by default. A `std::optional` member is omitted if it does not contain a value. +By default, all members are treated as children of the parent struct with closing tags the same as the name of the member. However, if a struct is not formattable, it by default is unpacked. Any `std::ranges::range` is iterated automatically except strings and string views, which remain scalar text. Each ordinary range item uses an `` tag. Map entries use `……`. A `std::optional` member is omitted if it does not contain a value; a present optional is serialized as its contained value. This is the complete list of annotations: - `[[=attribute]]` -- Mark a struct member as a XML attribute instead of a child. @@ -118,7 +118,7 @@ This is the complete list of annotations: - `[[=name{"custom_name"}]]` -- Specify the name of this attribute or child tag to be something other than the name of the member. Note: You can also specify this on the struct to control its closing tag (eg. generate `person` instead of `Person` for `struct Person` with `[[=name{"person"}]]`). - `[[=unpack]]` -- Instead of calling `std::format` on the member object, generate an enclosing XML tag for it and serialize its members as well. - `[[=no_unpack]]` -- Call `std::format` on the member object instead of breaking it down into its children. Opposite of `unpack`. -- `[[=iter{a, b}]]` -- For classes satisfying `std::ranges::range`, iterate through each member instead of directly calling `std::format`. The first (optional) parameter is the name of the tag for each element in the range. The second (optional) parameter is the name of the range tag enclosing each element. +- `[[=iter{a, b}]]` -- For classes satisfying `std::ranges::range`, iterate through each member instead of directly calling `std::format`. The first (optional) parameter is the name of the tag for each element in the range. The second (optional) parameter is the name of the range tag enclosing each element. Automatic range iteration uses the same naming defaults. - `[[=no_iter]]` -- The opposite of `iter` to disable automatic iteration of STL ranges. See the confusion points for more information on STL handling. - `[[=format{"format_specifier"}]]` -- Add a format specifier in the call to `std::format` for that member. Do not prefix with a colon (`:`) as the library handles that on its own. `[[=format{format_function}]]` instead calls a custom formatting function that accepts the member value and returns a string-like value. - `[[=cdata]]` -- Emit the value inside `cdata` (``) tags. @@ -126,14 +126,7 @@ This is the complete list of annotations: ### 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: - - `std::vector` - - `std::array` - - `std::inplace_vector` - - `std::deque` - - `std::forward_list` - - `std::span` - - `std::valarray` +1. Ranges are automatically iterated by default, including standard containers, views, and user-defined range types. Strings and string views are treated as scalar text. Add `[[=no_iter]]` to a member to format the range as a whole. 1. The precedence order for STL handled ranges is as follows: 1. `exclude_on_empty` 1. `raw` -- note that this only applies to the outer layer of tags for the range. Not each individual element in the range diff --git a/examples/stl_containers.cpp b/examples/stl_containers.cpp index 217999a..23db11f 100644 --- a/examples/stl_containers.cpp +++ b/examples/stl_containers.cpp @@ -26,9 +26,16 @@ struct NoIter { [[= serial_xml::no_iter]] std::vector values; }; +// Other standard ranges, including ordered maps, use the same automatic policy. +struct MapExample { + std::map values; +}; + int main() { std::print("Vector (auto-iterated):\n {}\n", serial_xml::to_xml(VectorExample{{1, 2, 3}})); std::print("Empty vector:\n {}\n", serial_xml::to_xml(EmptyVector{{}})); std::print("Exclude on empty (empty vector):\n {}\n", serial_xml::to_xml(ExcludeOnEmpty{{}})); std::print("No-iterate (formatted as a whole):\n {}\n", serial_xml::to_xml(NoIter{{1, 2, 3}})); + std::print("Map entries:\n {}\n", + serial_xml::to_xml(MapExample{{{"answer", 42}, {"meaning", 42}}})); } diff --git a/include/serial_xml.cxx b/include/serial_xml.cxx index 87d82b1..d476b26 100644 --- a/include/serial_xml.cxx +++ b/include/serial_xml.cxx @@ -196,20 +196,44 @@ using value_t = typename value_type::t; template constexpr auto value_m_t = value_type::m_t; +template +struct is_string_range : std::false_type {}; + +template +struct is_string_range> : std::true_type {}; + +template +struct is_string_range> : std::true_type {}; + +template +inline constexpr bool is_string_range_v = is_string_range>::value; + +template +struct is_optional : std::false_type {}; + +template +struct is_optional> : std::true_type {}; + +template +inline constexpr bool is_optional_v = is_optional>::value; + +template +inline constexpr bool is_auto_range_v = + std::ranges::range> && !is_string_range_v && !is_optional_v; + template consteval bool is_stl_handled() { - if constexpr (get_namespace() == ^^std) { - static constexpr auto m_t = std::meta::template_of(std::meta::dealias(m)); + return is_auto_range_v || is_optional_v; +} - if constexpr (m_t == ^^std::vector || m_t == ^^std::array || m_t == ^^std::inplace_vector || - m_t == ^^std::deque || m_t == ^^std::forward_list || m_t == ^^std::span || - m_t == ^^std::valarray || m_t == ^^std::optional) { - return true; - } - } +template +struct is_pair : std::false_type {}; - return false; -} +template +struct is_pair> : std::true_type {}; + +template +inline constexpr bool is_pair_v = is_pair>::value; template consteval auto get_annotations() @@ -815,32 +839,46 @@ template bool { using T = std::decay_t; - static constexpr auto m_t = std::meta::template_of(std::meta::dealias(^^T)); - if constexpr (!is_attribute) { - if constexpr (!is_no_iter && - (m_t == ^^std::vector || m_t == ^^std::array || m_t == ^^std::inplace_vector || - m_t == ^^std::deque || m_t == ^^std::forward_list || m_t == ^^std::span || - m_t == ^^std::valarray)) { + if constexpr (!is_no_iter && is_auto_range_v) { static constexpr auto tags = get_tags(); static constexpr auto start = std::get<0>(tags); static constexpr auto start_size = std::get<1>(tags); static constexpr auto end = std::get<2>(tags); - if (value.size() > 0) { + auto item = std::ranges::begin(value); + const auto last = std::ranges::end(value); + if (item != last) { if constexpr (!is_raw) { result += start; } static constexpr auto single_name = std::define_static_string("element"); - static constexpr auto item_m_t = std::meta::dealias(^^decltype(value[0])); - - for (const auto& item : value) { - if constexpr (is_unpack || - (!std::formattable && formatter.is_empty())) { - to_xml(item, result, buffer, false, std::string(single_name)); + while (item != last) { + using item_t = std::remove_cvref_t; + if constexpr (is_pair_v) { + static constexpr auto entry_tags = get_tags(); + result += std::get<0>(entry_tags); + static constexpr auto key_name = std::define_static_string("key"); + static constexpr auto value_name = std::define_static_string("value"); + if constexpr (is_unpack || !std::formattable) { + to_xml((*item).first, result, buffer, false, std::string(key_name)); + } else { + add_child(result, buffer, (*item).first); + } + if constexpr (is_unpack || !std::formattable) { + to_xml((*item).second, result, buffer, false, std::string(value_name)); + } else { + add_child(result, buffer, (*item).second); + } + result += std::get<2>(entry_tags); } else { - add_child(result, buffer, item); + if constexpr (is_unpack || (!std::formattable && formatter.is_empty())) { + to_xml(*item, result, buffer, false, std::string(single_name)); + } else { + add_child(result, buffer, *item); + } } + ++item; } if constexpr (!is_raw) { @@ -856,7 +894,7 @@ auto handle_stl(std::string& result, std::string& buffer, const auto& value) -> } } - if constexpr (m_t == ^^std::optional) { + if constexpr (is_optional_v) { if (!value.has_value()) { return true; } @@ -978,24 +1016,35 @@ void to_xml(const T& value, std::string& result, std::string& buffer, bool first } else { if constexpr (iter_names.first != nullptr && std::meta::is_class_type(value_m_t) && std::ranges::range>) { - if constexpr (!is_raw) { - result.push_back('<'); - result.append(iter_names.second); - result.push_back('>'); - } + auto item = std::ranges::begin(get_value(value)); + const auto last = std::ranges::end(get_value(value)); + if (item == last) { + if constexpr (!is_raw && !is_exclude_on_empty) { + result.push_back('<'); + result.append(iter_names.second); + result.append("/>"); + } + } else { + if constexpr (!is_raw) { + result.push_back('<'); + result.append(iter_names.second); + result.push_back('>'); + } - for (const auto& item : get_value(value)) { - if constexpr (is_unpack) { - to_xml(item, result, buffer, false, iter_names.first); - } else { - add_child(result, buffer, item); + while (item != last) { + if constexpr (is_unpack) { + to_xml(*item, result, buffer, false, iter_names.first); + } else { + add_child(result, buffer, *item); + } + ++item; } - } - if constexpr (!is_raw) { - result.append("'); + if constexpr (!is_raw) { + result.append("'); + } } } else if constexpr (is_unpack) { to_xml(get_value(value), result, buffer, false, m_name); diff --git a/tests/test_serial_xml.cpp b/tests/test_serial_xml.cpp index a637cbc..342b773 100644 --- a/tests/test_serial_xml.cpp +++ b/tests/test_serial_xml.cpp @@ -164,6 +164,42 @@ TEST(STL, Vector) { ""); } +TEST(Ranges, ForwardList) { + struct ForwardList { + std::forward_list values; + }; + ForwardList obj{{1, 2, 3}}; + ASSERT_EQ(clean_to_xml(obj), + "123" + ""); +} + +TEST(Ranges, Set) { + struct Set { + std::set values; + }; + Set obj{{3, 1, 2}}; + ASSERT_EQ(clean_to_xml(obj), + "123"); +} + +TEST(Ranges, Map) { + struct Map { + std::map values; + }; + Map obj{{{"answer", 42}}}; + ASSERT_EQ(clean_to_xml(obj), + "answer42"); +} + +TEST(Ranges, EmptyRange) { + struct EmptyRange { + std::set values; + }; + ASSERT_EQ(clean_to_xml(EmptyRange{}), ""); +} + TEST(STL, EmptyVector) { struct EmptyVector { std::vector values; @@ -388,6 +424,14 @@ TEST(Iteration, IterSTL) { "c_val>"); } +TEST(Iteration, EmptyRange) { + struct EmptyIterRange { + [[= serial_xml::iter{"item", "items"}]] std::vector values; + }; + + ASSERT_EQ(clean_to_xml(EmptyIterRange{}), ""); +} + TEST(Iteration, SingleCharIter) { struct SingleCharIter { [[= serial_xml::iter{"v", "vals"}]] std::vector values;