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
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,46 @@ As you can see, there are some parameters for configuration"
- `first` -- a bool indicating whether to insert the XML header defining the file as XML. `true` means yes
- `fixed_name` -- a custom name to specify for the instance being serialized. Overrides `name` annotation.

### Schema and scalar conversion rules

`serial_xml::schema<T>()` exposes the field-to-XML mapping inferred from the same reflection and
annotations used by `to_xml`. Each descriptor reports the C++ member name, XML name, and whether
the member is an attribute or child element. A member-level `name` annotation overrides the C++
identifier; otherwise the identifier is used. `skip` members are absent from the schema. The type's
`name` annotation controls the root name, falling back to the type name. Child fields represented
by an automatically iterated container or an `iter` annotation are marked `repeated`.

The schema API prepares for deserialization; it does not parse XML. The intended reader rules are:
missing required fields are errors, missing `std::optional` fields remain disengaged, duplicate
scalar attributes or child elements are errors, and repeated child elements append in document
order. A required field is one that is neither optional nor otherwise defaulted by a future reader
configuration. These policy constants are exposed as `default_missing_required_value_policy`,
`default_missing_optional_value_policy`, `default_duplicate_value_policy`, and
`default_repeated_value_policy`.

`serial_xml::convert_scalar<T>(text)` converts already-decoded scalar text. It supports strings,
single-byte `char`, booleans (`true`, `false`, `1`, or `0`), integral values, and floating-point
values. Numeric conversions must consume the complete string and do not trim whitespace. Invalid
or out-of-range values throw `std::invalid_argument`. Entity decoding and any XML whitespace rules
belong to the XML reader.

For another scalar type, specialize `serial_xml::conversion<T>` and provide a static
`T from_xml(std::string_view)` function. For example:

```cpp
struct Temperature { double celsius; };

template <>
struct serial_xml::conversion<Temperature> {
static Temperature from_xml(std::string_view text) {
return {serial_xml::convert_scalar<double>(text)};
}
};
```

This conversion customization is separate from `format` annotations, which only control C++ to
XML text formatting.

### The `prettify` function

A simple function to prettify (add indentation and newlines) the generated XML output. The only parameter is the output and it returns a new string with the output.
Expand Down
136 changes: 136 additions & 0 deletions include/serial_xml.cxx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module;

#include <charconv>
#include <cstdint>
#include <simd> // GCC 16.1 does not have in this in the std module

Expand Down Expand Up @@ -45,6 +46,76 @@ export constexpr cdata_ cdata;
struct exclude_on_empty_ {};
export constexpr exclude_on_empty_ exclude_on_empty;

// Specialize conversion<T> to define how scalar XML text becomes T. A
// specialization provides `static T from_xml(std::string_view)`.
export template <typename T>
struct conversion;

export enum class field_kind { attribute, child };

// These policies describe the behavior intended for the XML reader. They are
// metadata only until from_xml is implemented.
export enum class missing_value_policy { error, default_initialize, disengage_optional };
export enum class duplicate_value_policy { error, use_first, use_last };
export enum class repeated_value_policy { append_in_document_order };

export struct field_schema {
std::string_view member_name;
std::string_view xml_name;
field_kind kind;
bool repeated;
};

export struct type_schema {
std::string_view xml_name;
std::vector<field_schema> fields;
};

export inline constexpr auto default_missing_required_value_policy = missing_value_policy::error;
export inline constexpr auto default_missing_optional_value_policy =
missing_value_policy::disengage_optional;
export inline constexpr auto default_duplicate_value_policy = duplicate_value_policy::error;
export inline constexpr auto default_repeated_value_policy =
repeated_value_policy::append_in_document_order;

// Convert one already-decoded XML text value to a scalar C++ value. XML entity
// decoding and whitespace normalization are the caller's responsibility.
export template <typename T>
T convert_scalar(std::string_view text) {
using value_type = std::remove_cv_t<T>;

if constexpr (requires { conversion<value_type>::from_xml(text); }) {
return conversion<value_type>::from_xml(text);
} else if constexpr (std::same_as<value_type, std::string>) {
return std::string(text);
} else if constexpr (std::same_as<value_type, bool>) {
if (text == "true" || text == "1") return true;
if (text == "false" || text == "0") return false;
throw std::invalid_argument("invalid boolean XML scalar");
} else if constexpr (std::same_as<value_type, char>) {
if (text.size() != 1) throw std::invalid_argument("XML char scalar must contain one byte");
return text.front();
} else if constexpr (std::is_integral_v<value_type>) {
value_type result{};
const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), result);
if (error != std::errc{} || end != text.data() + text.size()) {
throw std::invalid_argument("invalid integral XML scalar");
}
return result;
} else if constexpr (std::is_floating_point_v<value_type>) {
value_type result{};
const auto [end, error] =
std::from_chars(text.data(), text.data() + text.size(), result, std::chars_format::general);
if (error != std::errc{} || end != text.data() + text.size()) {
throw std::invalid_argument("invalid floating-point XML scalar");
}
return result;
} else {
static_assert(std::same_as<value_type, void>,
"No serial_xml scalar conversion exists; specialize serial_xml::conversion<T>");
}
}

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 @@ -211,6 +282,14 @@ consteval bool is_stl_handled() {
return false;
}

template <std::meta::info m>
consteval bool is_optional() {
if constexpr (get_namespace<m>() == ^^std) {
return std::meta::template_of(std::meta::dealias(m)) == ^^std::optional;
}
return false;
}

template <std::meta::info m>
consteval auto get_annotations()
-> structural_tuple::tuple<bool, bool, bool, bool, bool, bool,
Expand Down Expand Up @@ -435,6 +514,63 @@ consteval auto get_members() {
std::define_static_array(child_annotations));
}

template <typename T>
requires(std::is_class_v<T>)
type_schema make_schema() {
static constexpr auto type_info = ^^T;
std::string_view root_name;
static constexpr auto type_annotations =
std::define_static_array(std::meta::annotations_of(type_info));

template for (constexpr auto annotation : type_annotations) {
if constexpr (std::meta::has_template_arguments(std::meta::type_of(annotation)) &&
std::meta::template_of(std::meta::type_of(annotation)) == ^^::serial_xml::name) {
static constexpr auto configured_name =
std::meta::extract<typename[:std::meta::type_of(annotation):]>(annotation);
root_name = configured_name.value;
}
}

if (root_name.empty()) {
if constexpr (std::meta::has_identifier(type_info)) {
static constexpr auto default_name = std::meta::identifier_of(type_info);
root_name = default_name;
}
}

type_schema result{root_name, {}};
static constexpr auto members = get_members<type_info>();

template for (constexpr auto member : members.first) {
static constexpr auto reflected_member = member.first;
static constexpr auto member_name = std::meta::identifier_of(reflected_member);
static constexpr auto xml_name = structural_tuple::get<0>(member.second);
result.fields.push_back(field_schema{member_name, xml_name, field_kind::attribute, false});
}

template for (constexpr auto member : members.second) {
static constexpr auto reflected_member = member.first;
static constexpr auto member_name = std::meta::identifier_of(reflected_member);
static constexpr auto iter_names = structural_tuple::get<4>(member.second);
static constexpr auto no_iter = structural_tuple::get<1>(member.second);
static constexpr auto automatically_repeated = is_stl_handled<value_m_t<reflected_member>>() &&
std::ranges::range<value_t<reflected_member>> &&
!is_optional<value_m_t<reflected_member>>() &&
!no_iter;
static constexpr auto repeated = iter_names.first != nullptr || automatically_repeated;
static constexpr auto xml_name = structural_tuple::get<5>(member.second);
result.fields.push_back(field_schema{member_name, xml_name, field_kind::child, repeated});
}

return result;
}

export template <typename T>
requires(std::is_class_v<T>)
type_schema schema() {
return make_schema<T>();
}

template <auto name>
consteval auto get_attribute_prefix() {
std::string prefix;
Expand Down
57 changes: 57 additions & 0 deletions tests/test_serial_xml.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ import std;

import serial_xml;

struct TestTemperature {
double celsius;
};

template <>
struct serial_xml::conversion<TestTemperature> {
static TestTemperature from_xml(std::string_view text) {
return {serial_xml::convert_scalar<double>(text)};
}
};

std::string clean_to_xml(auto obj) { return serial_xml::to_xml(obj, false); }

TEST(Naming, EmptyWithName) {
Expand All @@ -13,6 +24,52 @@ TEST(Naming, EmptyWithName) {
ASSERT_EQ(clean_to_xml(obj), "<MyStruct/>");
}

TEST(Schema, MatchesSerializedMemberNamesAndShape) {
struct[[= serial_xml::name{"person"}]] PersonSchema {
[[= serial_xml::name{"full-name"}]] std::string name;
[[= serial_xml::attribute]] int age;
std::vector<int> scores;
std::optional<int> nickname;
[[= serial_xml::skip]] int internal;
};

const auto result = serial_xml::schema<PersonSchema>();
ASSERT_EQ(result.xml_name, "person");
ASSERT_EQ(result.fields.size(), 4);
EXPECT_EQ(result.fields[0].member_name, "age");
EXPECT_EQ(result.fields[0].xml_name, "age");
EXPECT_EQ(result.fields[0].kind, serial_xml::field_kind::attribute);
EXPECT_FALSE(result.fields[0].repeated);
EXPECT_EQ(result.fields[1].member_name, "name");
EXPECT_EQ(result.fields[1].xml_name, "full-name");
EXPECT_EQ(result.fields[1].kind, serial_xml::field_kind::child);
EXPECT_FALSE(result.fields[1].repeated);
EXPECT_EQ(result.fields[2].xml_name, "scores");
EXPECT_TRUE(result.fields[2].repeated);
EXPECT_EQ(result.fields[3].xml_name, "nickname");
EXPECT_FALSE(result.fields[3].repeated);
}

TEST(Conversion, BuiltInScalars) {
EXPECT_EQ(serial_xml::convert_scalar<int>("-42"), -42);
EXPECT_DOUBLE_EQ(serial_xml::convert_scalar<double>("1.25"), 1.25);
EXPECT_TRUE(serial_xml::convert_scalar<bool>("true"));
EXPECT_FALSE(serial_xml::convert_scalar<bool>("0"));
EXPECT_EQ(serial_xml::convert_scalar<char>("x"), 'x');
EXPECT_EQ(serial_xml::convert_scalar<std::string>("value"), "value");
}

TEST(Conversion, RejectsInvalidOrPartialInput) {
EXPECT_THROW(serial_xml::convert_scalar<int>("12tail"), std::invalid_argument);
EXPECT_THROW(serial_xml::convert_scalar<bool>("yes"), std::invalid_argument);
EXPECT_THROW(serial_xml::convert_scalar<char>("xy"), std::invalid_argument);
}

TEST(Conversion, UsesExplicitSpecialization) {
const auto value = serial_xml::convert_scalar<TestTemperature>("21.5");
EXPECT_DOUBLE_EQ(value.celsius, 21.5);
}

TEST(Naming, NamedAttribute) {
struct NamedAttribute {
[[ = serial_xml::name{"MyAttribute"}, = serial_xml::attribute ]] int x;
Expand Down
Loading