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
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,39 @@ This is the complete list of annotations:
- `[[=cdata]]` -- Emit the value inside `cdata` (`<![CDATA[your_content]]>`) 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<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 <typename T>
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<vector_mock<std::vector<int>>>{}]] std::vector<int> values;
};

// <document><values count="3"><is_empty>false</is_empty></values></document>
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<vector_mock<std::vector<int>>>(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:
Expand Down Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
21 changes: 21 additions & 0 deletions examples/mock_vector.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#include <iostream>

import std;

import serial_xml;

// The mock describes the XML view and points to the actual accessors.
template <typename T>
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<vector_mock<std::vector<int>>>{}]] std::vector<int> values;
};

int main() { std::cout << serial_xml::to_xml(document{{10, 20, 30}}) << '\n'; }
121 changes: 98 additions & 23 deletions include/serial_xml.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,30 @@ export constexpr cdata_ cdata;
struct exclude_on_empty_ {};
export constexpr exclude_on_empty_ exclude_on_empty;

export template <typename T>
struct mock {};

export template <auto Member>
struct accessor {
static constexpr auto value = Member;
};

template <typename T>
concept mock_class = std::is_class_v<T>;

template <std::meta::info m>
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'); }
Expand Down Expand Up @@ -166,15 +190,43 @@ consteval std::meta::info get_namespace() {
return m;
}

template <std::meta::info m>
template <std::meta::info m, typename Mock = void>
inline decltype(auto) get_value(auto&& s) {
return (s.[:m:]);
}

template <std::meta::info m>
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 <std::meta::info m, typename Mock = void>
requires(std::meta::is_function(m))
inline decltype(auto) get_value(auto&& s) {
return s.[:m:]();
if constexpr (mock_class<Mock>) {
static constexpr auto member = accessor_of<m>();
static_assert(member != ^^void, "Annotated mock functions require serial_xml::accessor");
if constexpr (member != ^^void) {
static_assert(std::is_invocable_v<decltype([:member:]), decltype(s)>,
"Mock accessor must be callable on the serialized object");
if constexpr (std::is_invocable_v<decltype([:member:]), decltype(s)>) {
static_assert(std::is_convertible_v<std::invoke_result_t<decltype([:member:]), decltype(s)>,
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 <std::meta::info m>
Expand Down Expand Up @@ -222,8 +274,9 @@ consteval auto get_annotations()
bool is_no_iter = !is_stl_handled<value_m_t<m>>();
bool is_raw = false;
bool is_skip = false;
bool is_unpack = std::meta::is_class_type(value_m_t<m>) && !std::formattable<value_t<m>, char> &&
!is_stl_handled<value_m_t<m>>();
bool is_unpack = std::meta::is_class_type(value_m_t<m>) &&
(mock_of<m>() != ^^void ||
(!std::formattable<value_t<m>, char> && !is_stl_handled<value_m_t<m>>()));

bool has_custom_format_function = false;
std::optional<std::pair<std::string, std::string>> iter_names;
Expand Down Expand Up @@ -357,10 +410,17 @@ consteval auto is_invalid_function() {
return false;
}

template <std::meta::info container>
template <std::meta::info container, typename Mock = void>
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<Mock>) {
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<std::meta::info,
Expand All @@ -373,6 +433,17 @@ consteval auto get_members() {
template for (constexpr auto m : members) {
static constexpr auto invalid_function = is_invalid_function<m>();

if constexpr (mock_class<Mock>) {
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<m>();

Expand Down Expand Up @@ -628,7 +699,8 @@ void add_attribute(std::string& result, std::string& buffer, const auto& value)
static constexpr auto prefix_result = get_attribute_prefix<name>();
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<T> && sizeof(T) <= 64) {
if constexpr (formatter.is_empty() && std::is_arithmetic_v<T> && !std::is_same_v<T, bool> &&
sizeof(T) <= 64) {
if constexpr (std::is_floating_point_v<T>) {
static constexpr auto format_resize = 311 + prefix_size + 1;

Expand Down Expand Up @@ -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<T> && sizeof(T) <= 64) {
if constexpr (formatter.is_empty() && std::is_arithmetic_v<T> && !std::is_same_v<T, bool> &&
sizeof(T) <= 64) {
if constexpr (std::is_floating_point_v<T>) {
static constexpr auto format_resize = 311 + combined_size;
result.resize_and_overwrite(original_size + format_resize,
Expand Down Expand Up @@ -805,7 +878,7 @@ void add_child(std::string& result, std::string& buffer, const auto& value) {
}
}

template <typename T>
template <typename Mock = void, typename T>
requires(std::is_class_v<T>)
void to_xml(const T& value, std::string& result, std::string& buffer, bool first,
const std::string& fixed_name = "");
Expand Down Expand Up @@ -877,7 +950,7 @@ auto handle_stl(std::string& result, std::string& buffer, const auto& value) ->
return false;
}

template <typename T>
template <typename Mock, typename T>
requires(std::is_class_v<T>)
void to_xml(const T& value, std::string& result, std::string& buffer, bool first,
const std::string& fixed_name) {
Expand Down Expand Up @@ -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<M>();
static constexpr auto members = get_members<M, Mock>();
static constexpr auto attribute_annotations = members.first;
static constexpr auto child_annotations = members.second;

Expand All @@ -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<true, false, true, false, false, false, m_name, formatter>(result, buffer,
get_value<m>(value));
handle_stl<true, false, true, false, false, false, m_name, formatter>(
result, buffer, get_value<m, Mock>(value));
} else {
add_attribute<m_name, formatter>(result, buffer, get_value<m>(value));
add_attribute<m_name, formatter>(result, buffer, get_value<m, Mock>(value));
}
}

Expand All @@ -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<std::meta::type_of(m_a.first)>();
static constexpr auto is_std =
is_stl_handled<std::meta::type_of(m_a.first)>() && mock_of<m_a.first>() == ^^void;

static constexpr auto m = m_a.first;
static constexpr auto m_annotations = m_a.second;
Expand Down Expand Up @@ -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<false, is_cdata, is_no_iter, is_raw, is_exclude_on_empty, is_unpack, m_name,
formatter>(result, buffer, get_value<m>(value));
formatter>(result, buffer, get_value<m, Mock>(value));
} else {
if constexpr (iter_names.first != nullptr && std::meta::is_class_type(value_m_t<m>) &&
std::ranges::range<value_t<m>>) {
Expand All @@ -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<m>(value)) {
for (const auto& item : get_value<m, Mock>(value)) {
if constexpr (is_unpack) {
to_xml(item, result, buffer, false, iter_names.first);
} else {
Expand All @@ -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<m>(value), result, buffer, false, m_name);
to_xml<typename[:mock_of<m>():]>(get_value<m, Mock>(value), result, buffer, false,
m_name);
} else {
if constexpr (is_raw) {
buffer = format_value<formatter>(get_value<m>(value));
buffer = format_value<formatter>(get_value<m, Mock>(value));

auto padded_size = get_escape_bitmask(buffer);

Expand All @@ -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<m_name, is_cdata, formatter>(result, buffer, get_value<m>(value));
add_child<m_name, is_cdata, formatter>(result, buffer, get_value<m, Mock>(value));
}
}
}
Expand All @@ -1033,7 +1108,7 @@ void to_xml(const T& value, std::string& result, std::string& buffer, bool first
}
}

export template <typename T>
export template <typename Mock = void, typename T>
requires(std::is_class_v<T>)
auto to_xml(const T& value, bool first = true, const std::string& fixed_name = "") -> std::string {
std::string result;
Expand All @@ -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<Mock>(value, result, buffer, first, fixed_name);

escape_flags.clear();
escape_flags.shrink_to_fit();
Expand Down
38 changes: 38 additions & 0 deletions tests/test_serial_xml.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<short>::size>{}, = serial_xml::attribute,
= serial_xml::name{"count"}
]] std::size_t size() const;
[[ = serial_xml::accessor<&std::vector<short>::empty>{},
= serial_xml::name{"is_empty"} ]] bool empty() const;
int ignored() const;
};

TEST(Mock, ExplicitVectorRoot) {
std::vector<short> values{2, 4, 6};
ASSERT_EQ(serial_xml::to_xml<vector_mock>(values, false, "numbers"),
"<numbers count=\"3\"><is_empty>false</is_empty></numbers>");
}

TEST(Mock, AnnotatedVectorMemberOverridesBuiltInIteration) {
struct VectorEnvelope {
[[= serial_xml::mock<vector_mock>{}]] std::vector<short> values;
};

ASSERT_EQ(clean_to_xml(VectorEnvelope{{2, 4, 6}}),
"<VectorEnvelope><values count=\"3\"><is_empty>false</is_empty></values></"
"VectorEnvelope>");
}

TEST(Mock, AnnotationAppliesOnlyToSelectedMember) {
struct MixedVectorEnvelope {
[[= serial_xml::mock<vector_mock>{}]] std::vector<short> summary;
std::vector<short> values;
};

ASSERT_EQ(clean_to_xml(MixedVectorEnvelope{{2, 4}, {6, 8}}),
"<MixedVectorEnvelope><summary count=\"2\"><is_empty>false</is_empty></summary>"
"<values><element>6</element><element>8</element></values></MixedVectorEnvelope>");
}

TEST(Naming, EmptyWithName) {
struct[[= serial_xml::name{"MyStruct"}]] EmptyName {};
EmptyName obj;
Expand Down
Loading