Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 3 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<element>` tag. Map entries use `<element><key>…</key><value>…</value></element>`. 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.
Expand All @@ -118,22 +118,15 @@ 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` (`<![CDATA[your_content]]>`) tags.
- `[[=exclude_on_empty]]` -- Do not emit any tags when the range is empty

### 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
Expand Down
7 changes: 7 additions & 0 deletions examples/stl_containers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,16 @@ struct NoIter {
[[= serial_xml::no_iter]] std::vector<int> values;
};

// Other standard ranges, including ordered maps, use the same automatic policy.
struct MapExample {
std::map<std::string, int> 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}}}));
}
129 changes: 89 additions & 40 deletions include/serial_xml.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -196,20 +196,44 @@ using value_t = typename value_type<m>::t;
template <std::meta::info m>
constexpr auto value_m_t = value_type<m>::m_t;

template <typename T>
struct is_string_range : std::false_type {};

template <typename Char, typename Traits, typename Allocator>
struct is_string_range<std::basic_string<Char, Traits, Allocator>> : std::true_type {};

template <typename Char, typename Traits>
struct is_string_range<std::basic_string_view<Char, Traits>> : std::true_type {};

template <typename T>
inline constexpr bool is_string_range_v = is_string_range<std::remove_cvref_t<T>>::value;

template <typename T>
struct is_optional : std::false_type {};

template <typename T>
struct is_optional<std::optional<T>> : std::true_type {};

template <typename T>
inline constexpr bool is_optional_v = is_optional<std::remove_cvref_t<T>>::value;

template <typename T>
inline constexpr bool is_auto_range_v =
std::ranges::range<std::remove_cvref_t<T>> && !is_string_range_v<T> && !is_optional_v<T>;

template <std::meta::info m>
consteval bool is_stl_handled() {
if constexpr (get_namespace<m>() == ^^std) {
static constexpr auto m_t = std::meta::template_of(std::meta::dealias(m));
return is_auto_range_v<typename[:m:]> || is_optional_v<typename[:m:]>;
}

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 <typename T>
struct is_pair : std::false_type {};

return false;
}
template <typename First, typename Second>
struct is_pair<std::pair<First, Second>> : std::true_type {};

template <typename T>
inline constexpr bool is_pair_v = is_pair<std::remove_cvref_t<T>>::value;

template <std::meta::info m>
consteval auto get_annotations()
Expand Down Expand Up @@ -815,32 +839,46 @@ template <bool is_attribute, bool is_cdata, bool is_no_iter, bool is_raw, bool i
auto handle_stl(std::string& result, std::string& buffer, const auto& value) -> bool {
using T = std::decay_t<decltype(value)>;

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<T>) {
static constexpr auto tags = get_tags<name>();
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<typename[:item_m_t:], char> && formatter.is_empty())) {
to_xml(item, result, buffer, false, std::string(single_name));
while (item != last) {
using item_t = std::remove_cvref_t<decltype(*item)>;
if constexpr (is_pair_v<item_t>) {
static constexpr auto entry_tags = get_tags<single_name>();
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<decltype((*item).first), char>) {
to_xml((*item).first, result, buffer, false, std::string(key_name));
} else {
add_child<key_name, false, formatter>(result, buffer, (*item).first);
}
if constexpr (is_unpack || !std::formattable<decltype((*item).second), char>) {
to_xml((*item).second, result, buffer, false, std::string(value_name));
} else {
add_child<value_name, false, formatter>(result, buffer, (*item).second);
}
result += std::get<2>(entry_tags);
} else {
add_child<single_name, is_cdata, formatter>(result, buffer, item);
if constexpr (is_unpack || (!std::formattable<item_t, char> && formatter.is_empty())) {
to_xml(*item, result, buffer, false, std::string(single_name));
} else {
add_child<single_name, is_cdata, formatter>(result, buffer, *item);
}
}
++item;
}

if constexpr (!is_raw) {
Expand All @@ -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<T>) {
if (!value.has_value()) {
return true;
}
Expand Down Expand Up @@ -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<m>) &&
std::ranges::range<value_t<m>>) {
if constexpr (!is_raw) {
result.push_back('<');
result.append(iter_names.second);
result.push_back('>');
}
auto item = std::ranges::begin(get_value<m>(value));
const auto last = std::ranges::end(get_value<m>(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<m>(value)) {
if constexpr (is_unpack) {
to_xml(item, result, buffer, false, iter_names.first);
} else {
add_child<iter_names.first, is_cdata, formatter>(result, buffer, item);
while (item != last) {
if constexpr (is_unpack) {
to_xml(*item, result, buffer, false, iter_names.first);
} else {
add_child<iter_names.first, is_cdata, formatter>(result, buffer, *item);
}
++item;
}
}

if constexpr (!is_raw) {
result.append("</");
result.append(iter_names.second);
result.push_back('>');
if constexpr (!is_raw) {
result.append("</");
result.append(iter_names.second);
result.push_back('>');
}
}
} else if constexpr (is_unpack) {
to_xml(get_value<m>(value), result, buffer, false, m_name);
Expand Down
44 changes: 44 additions & 0 deletions tests/test_serial_xml.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,42 @@ TEST(STL, Vector) {
"</values></Vector>");
}

TEST(Ranges, ForwardList) {
struct ForwardList {
std::forward_list<int> values;
};
ForwardList obj{{1, 2, 3}};
ASSERT_EQ(clean_to_xml(obj),
"<ForwardList><values><element>1</element><element>2</element><element>3</element>"
"</values></ForwardList>");
}

TEST(Ranges, Set) {
struct Set {
std::set<int> values;
};
Set obj{{3, 1, 2}};
ASSERT_EQ(clean_to_xml(obj),
"<Set><values><element>1</element><element>2</element><element>3</element></"
"values></Set>");
}

TEST(Ranges, Map) {
struct Map {
std::map<std::string, int> values;
};
Map obj{{{"answer", 42}}};
ASSERT_EQ(clean_to_xml(obj),
"<Map><values><element><key>answer</key><value>42</value></element></values></Map>");
}

TEST(Ranges, EmptyRange) {
struct EmptyRange {
std::set<int> values;
};
ASSERT_EQ(clean_to_xml(EmptyRange{}), "<EmptyRange><values/></EmptyRange>");
}

TEST(STL, EmptyVector) {
struct EmptyVector {
std::vector<int> values;
Expand Down Expand Up @@ -388,6 +424,14 @@ TEST(Iteration, IterSTL) {
"c_val></container></IterSTL>");
}

TEST(Iteration, EmptyRange) {
struct EmptyIterRange {
[[= serial_xml::iter{"item", "items"}]] std::vector<int> values;
};

ASSERT_EQ(clean_to_xml(EmptyIterRange{}), "<EmptyIterRange><items/></EmptyIterRange>");
}

TEST(Iteration, SingleCharIter) {
struct SingleCharIter {
[[= serial_xml::iter{"v", "vals"}]] std::vector<int> values;
Expand Down
Loading