From 2c0111f9d733a986971b67c72fcb892b7e3bafc9 Mon Sep 17 00:00:00 2001 From: sha512sum Date: Fri, 18 Oct 2024 15:03:19 +0000 Subject: [PATCH] Add automatic serialization/deserialization generation --- .forgejo/workflows/pr_check.yaml | 3 +- library/include/larra/serialization.hpp | 4 +- library/include/larra/serialization/auto.hpp | 270 ++++++++++++++++++ library/include/larra/serialization/error.hpp | 30 ++ tests/serialization.cpp | 137 ++++++++- 5 files changed, 440 insertions(+), 4 deletions(-) create mode 100644 library/include/larra/serialization/auto.hpp create mode 100644 library/include/larra/serialization/error.hpp diff --git a/.forgejo/workflows/pr_check.yaml b/.forgejo/workflows/pr_check.yaml index df57c81..93dc9e3 100644 --- a/.forgejo/workflows/pr_check.yaml +++ b/.forgejo/workflows/pr_check.yaml @@ -129,7 +129,8 @@ jobs: -B ${{ github.workspace }}/build_gcc \ -GNinja -DCMAKE_BUILD_TYPE=Release \ -DENABLE_EXAMPLES=ON \ - -DENABLE_TESTS=ON + -DENABLE_TESTS=ON \ + -DCMAKE_CXX_FLAGS="-ftemplate-backtrace-limit=0" cmake --build ${{ github.workspace }}/build_gcc --parallel `nproc` - name: GCC unit tests diff --git a/library/include/larra/serialization.hpp b/library/include/larra/serialization.hpp index 31b127a..a24e0a8 100644 --- a/library/include/larra/serialization.hpp +++ b/library/include/larra/serialization.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -96,7 +97,8 @@ template struct Serialization : SerializationBase { [[nodiscard]] static constexpr auto Parse(xmlpp::Element* element) -> T { if(!Serialization::StartCheck(element)) { - throw std::runtime_error("StartCheck failed"); + throw serialization::ParsingError{ + std::format("[{}: {}] parsing error: [ StartCheck failed ]", Serialization::kDefaultName, nameof::nameof_full_type())}; } return T::Parse(element); } diff --git a/library/include/larra/serialization/auto.hpp b/library/include/larra/serialization/auto.hpp new file mode 100644 index 0000000..6d85d72 --- /dev/null +++ b/library/include/larra/serialization/auto.hpp @@ -0,0 +1,270 @@ +#pragma once +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace larra::xmpp::serialization { + +template +struct Tag {}; + +template +inline constexpr auto kSerializationConfig = std::monostate{}; + +template +inline constexpr auto kDeserializationConfig = kSerializationConfig; + +template +constexpr auto Parse(xmlpp::Element* element, Tag = {}) -> T + requires(!std::same_as)>, std::monostate>); + +template +constexpr auto Parse(xmlpp::Element* element, Tag = {}) -> T; + +template +constexpr auto Serialize(xmlpp::Element* node, const T& element) -> void + requires(!std::same_as)>, std::monostate>); + +template +constexpr auto Serialize(xmlpp::Element* node, const T& element) -> void; + +template +struct MetaInfo { + static constexpr std::size_t kSize = boost::pfr::tuple_size_v; + template + using TupleElement = boost::pfr::tuple_element_t; + + template + static constexpr std::string_view kFieldName = boost::pfr::get_name(); + + template + static constexpr auto Get(Self&& self) -> decltype(auto) { + return boost::pfr::get(std::forward(self)); + } +}; + +template +struct FieldInfo { + using Main = MainT; + using Info = MetaInfo
; + static inline const std::string kName = [] { + if constexpr(requires { Info::template TupleElement::kDefaultName; }) { + return Info::template TupleElement::kDefaultName; + } else { + return static_cast(Info::template kFieldName); + } + }(); +}; + +template +struct Config { + std::optional defaultValue; +}; + +// GCC workaround: operator== + +struct AttributeConfig { + auto operator==(const AttributeConfig&) const -> bool = default; + template + constexpr auto operator==(const Config&) const -> bool { + return false; + } + template + friend constexpr auto operator==(const Config&, const AttributeConfig&) -> bool { + return false; + } +}; + +template +constexpr auto operator==(const Config&, const Config&) -> bool { + return false; +} + +template <> +struct Config { + std::optional defaultValue; +}; + +namespace impl { +template +concept HasParse = requires(xmlpp::Element* e) { + { T::Parse(e) } -> std::same_as; +}; + +template +struct Config : V { + using V::V; + constexpr Config() + requires HasParse + : V(::larra::xmpp::serialization::Config{}) { + } + + constexpr Config() + requires(!HasParse) + : V(AttributeConfig{}) { + } + constexpr auto Base() const -> const V& { + return static_cast(*this); + } + using type = T; +}; + +} // namespace impl + +template +struct ElementConfig { + using type = impl::Config>>; +}; + +template +struct ElementSerializer { + static constexpr auto Parse(xmlpp::Element* element) { + auto node = element->get_first_child(Info::kName); + if(!node) { + throw ElementParsingError(std::format("[{}: {}] parsing error: [ Not found ]", Info::kName, nameof::nameof_full_type())); + } + auto elementNode = dynamic_cast(node); + if(!node) { + throw ElementParsingError(std::format("[{}: {}] parsing error: [ Invalid node ]", Info::kName, nameof::nameof_full_type())); + } + try { + return ::larra::xmpp::serialization::Parse(elementNode, Tag{}); + } catch(const std::exception& error) { + throw ElementParsingError(std::format("[{}: {}] parsing error: [ {} ]", Info::kName, nameof::nameof_full_type(), error.what())); + } + } + static constexpr auto Serialize(xmlpp::Element* node, const T& element) { + auto created = node->add_child_element(Info::kName); + if(!node) { + throw ElementSerializaionError( + std::format("[{}: {}] serialization error: [ node creation failed ]", Info::kName, nameof::nameof_full_type())); + } + try { + ::larra::xmpp::serialization::Serialize(created, element); + } catch(const std::exception& err) { + throw ElementSerializaionError( + std::format("[{}: {}] serialization error: [ {} ]", Info::kName, nameof::nameof_full_type(), err.what())); + } + } +}; + +namespace impl { + +template +consteval auto FindElement(std::string_view field, utempl::TypeList = {}) { + auto fields = boost::pfr::names_as_array(); + return std::ranges::find(fields, field) - fields.begin(); +} + +template +auto ParseField(xmlpp::Element* main) -> std::decay_t::type { + using Type = std::decay_t::type; + if constexpr(std::holds_alternative(Config.Base())) { + xmlpp::Attribute* node = main->get_attribute(Info::kName); + if(!node) { + throw AttributeParsingError(std::format("Attribute [{}: {}] parsing error", Info::kName, nameof::nameof_full_type())); + } + if constexpr(requires(std::string_view view) { Type::Parse(view); }) { + return Type::Parse(node->get_value()); + } else { + return node->get_value(); + } + } else { + return ElementSerializer::Parse(main); + } +} + +template +auto SerializeField(xmlpp::Element* main, const T& obj) { + if constexpr(std::holds_alternative(Config.Base())) { + auto node = main->set_attribute(Info::kName, [&] -> decltype(auto) { + if constexpr(requires { + { ToString(obj) } -> std::convertible_to; + }) { + return ToString(obj); + } else { + return obj; + } + }()); + if(!node) { + throw AttributeSerializationError( + std::format("[{}: {}] parsing error: [ node creation failed ]", Info::kName, nameof::nameof_full_type())); + } + } else { + ElementSerializer::Serialize(main, obj); + } +} + +} // namespace impl + +template +struct SerializationConfig { + decltype([] { + return [](auto... is) { + return utempl::Tuple>::type...>{}; + } | utempl::kSeq>; + }()) tuple{}; + template // NOLINTNEXTLINE + consteval auto With(this Self&& self, ElementConfig>::type config) -> SerializationConfig { + auto tuple = std::forward_like(self.tuple); + Get(tuple) = std::move(config); + return {std::move(tuple)}; + } + template + constexpr auto With(this Self&& self, ElementConfig(Name), T>>::type config) + -> SerializationConfig { + return std::forward(self).template With(Name)>(std::move(config)); + } +}; + +template +constexpr auto Parse(xmlpp::Element* element, Tag) -> T { + return Serialization::Parse(element); +} +template +constexpr auto Parse(xmlpp::Element* element, Tag) -> T + requires(!std::same_as)>, std::monostate>) +{ + static constexpr SerializationConfig config = kDeserializationConfig; + constexpr auto tuple = utempl::Map(config.tuple, [](auto& ref) { + return &ref; + }); + return utempl::Unpack(utempl::PackConstexprWrapper(), [&](auto... configs) { + try { + return T{impl::ParseField<*((*configs).second), FieldInfo>(element)...}; + } catch(const ParsingError& error) { + throw ElementParsingError(std::format("[{}] parsing error: [ {} ]", nameof::nameof_full_type(), error.what())); + } + }); +} + +template +constexpr auto Serialize(xmlpp::Element* node, const T& element) -> void + requires(!std::same_as)>, std::monostate>) +{ + static constexpr SerializationConfig config = kSerializationConfig; + constexpr auto tuple = utempl::Map(config.tuple, [](auto& ref) { + return &ref; + }); + + return utempl::Unpack(utempl::PackConstexprWrapper(), [&](auto... configs) { + try { + (impl::SerializeField<*((*configs).second), FieldInfo>(node, boost::pfr::get<(*configs).first>(element)), ...); + } catch(const ParsingError& error) { + throw ElementParsingError(std::format("[{}] parsing error: [ {} ]", nameof::nameof_full_type(), error.what())); + } + }); +} + +template +constexpr auto Serialize(xmlpp::Element* node, const T& element) -> void { + Serialization::Serialize(node, element); +} + +} // namespace larra::xmpp::serialization diff --git a/library/include/larra/serialization/error.hpp b/library/include/larra/serialization/error.hpp new file mode 100644 index 0000000..851ed08 --- /dev/null +++ b/library/include/larra/serialization/error.hpp @@ -0,0 +1,30 @@ +#pragma once +#include + +namespace larra::xmpp::serialization { + +struct ParsingError : std::runtime_error { + using std::runtime_error::runtime_error; +}; + +struct AttributeParsingError : ParsingError { + using ParsingError::ParsingError; +}; + +struct ElementParsingError : ParsingError { + using ParsingError::ParsingError; +}; + +struct SerializationError : std::runtime_error { + using std::runtime_error::runtime_error; +}; + +struct AttributeSerializationError : SerializationError { + using SerializationError::SerializationError; +}; + +struct ElementSerializaionError : SerializationError { + using SerializationError::SerializationError; +}; + +} // namespace larra::xmpp::serialization diff --git a/tests/serialization.cpp b/tests/serialization.cpp index 89e23ac..3e724a9 100644 --- a/tests/serialization.cpp +++ b/tests/serialization.cpp @@ -1,8 +1,13 @@ #include +#include #include +#include +#include #include +using namespace std::literals; + namespace larra::xmpp { TEST(Parse, Variant) { @@ -21,8 +26,136 @@ TEST(Serialize, Variant) { auto node = doc.create_root_node("stream:error"); S::Serialize(node, data); EXPECT_EQ(doc.write_to_string(), - std::string_view{"\n\n"}); + "\n\n"sv); +} + +namespace tests::serialization { + +struct SomeStruct { + static constexpr auto kDefaultName = "some"; + std::string value; + [[nodiscard]] static auto Parse(xmlpp::Element* element) -> SomeStruct; + friend auto operator<<(xmlpp::Element* element, const SomeStruct& self); +}; + +struct SomeStruct2 { + static constexpr auto kDefaultName = "some2"; + SomeStruct value; + [[nodiscard]] static auto Parse(xmlpp::Element* element) -> SomeStruct2; + friend auto operator<<(xmlpp::Element* element, const SomeStruct2& self); +}; + +struct SomeStruct3 { + static constexpr auto kDefaultName = "some3"; + int value; + [[nodiscard]] static auto Parse(xmlpp::Element* element) -> SomeStruct3; +}; + +struct SomeStruct4 { + static constexpr auto kDefaultName = "some4"; + SomeStruct3 value; + [[nodiscard]] static auto Parse(xmlpp::Element* element) -> SomeStruct4; +}; + +struct SomeStruct5 { + static constexpr auto kDefaultName = "some5"; + BareJid value; + [[nodiscard]] static auto Parse(xmlpp::Element* element) -> SomeStruct5; + friend auto operator<<(xmlpp::Element* element, const SomeStruct5& self); +}; + +} // namespace tests::serialization + +namespace serialization { + +template <> +constexpr auto kSerializationConfig = SerializationConfig{}; + +template <> +constexpr auto kSerializationConfig = SerializationConfig{}; + +template <> +constexpr auto kSerializationConfig = SerializationConfig{}; + +template <> +constexpr auto kSerializationConfig = SerializationConfig{}; + +} // namespace serialization + +namespace tests::serialization { + +auto SomeStruct::Parse(xmlpp::Element* element) -> SomeStruct { + return ::larra::xmpp::serialization::Parse(element); +} + +auto SomeStruct2::Parse(xmlpp::Element* element) -> SomeStruct2 { + return ::larra::xmpp::serialization::Parse(element); +} + +auto SomeStruct3::Parse(xmlpp::Element*) -> SomeStruct3 { + return {.value = 42}; // NOLINT +} + +auto SomeStruct4::Parse(xmlpp::Element* element) -> SomeStruct4 { + return ::larra::xmpp::serialization::Parse(element); +} + +auto SomeStruct5::Parse(xmlpp::Element* element) -> SomeStruct5 { + return ::larra::xmpp::serialization::Parse(element); +} + +auto operator<<(xmlpp::Element* element, const SomeStruct& self) { + ::larra::xmpp::serialization::Serialize(element, self); +} + +auto operator<<(xmlpp::Element* element, const SomeStruct2& self) { + ::larra::xmpp::serialization::Serialize(element, self); +} + +auto operator<<(xmlpp::Element* element, const SomeStruct5& self) { + ::larra::xmpp::serialization::Serialize(element, self); +} + +} // namespace tests::serialization + +TEST(AutoParse, Basic) { + xmlpp::Document doc; + auto node = doc.create_root_node("some2"); + node = node->add_child_element("some"); + node->set_attribute("value", "Hello"); + auto a = Serialization::Parse(node); + EXPECT_EQ(a.value, "Hello"sv); + auto b = Serialization::Parse(doc.get_root_node()); + EXPECT_EQ(b.value.value, "Hello"sv); + EXPECT_THROW(std::ignore = tests::serialization::SomeStruct2::Parse(node), serialization::ParsingError); + auto node2 = node->add_child_element("some4"); + node2->add_child_element("some3"); + auto c = Serialization::Parse(node2); + EXPECT_EQ(c.value.value, 42); +} + +TEST(AutoParse, Attribute) { + xmlpp::Document doc; + auto node = doc.create_root_node("some5"); + node->set_attribute("value", "user@server.i2p"); + auto a = Serialization::Parse(node); + EXPECT_EQ(a.value.server, "server.i2p"sv); + EXPECT_EQ(a.value.username, "user"sv); +} + +TEST(AutoSerialize, Basic) { + xmlpp::Document doc; + auto node = doc.create_root_node("some2"); + node << tests::serialization::SomeStruct2{.value = {.value = "testData"}}; + EXPECT_EQ(doc.write_to_string(), "\n\n"); +} + +TEST(AutoSerialize, Attribute) { + xmlpp::Document doc; + auto node = doc.create_root_node("some5"); + node << tests::serialization::SomeStruct5{.value = {.username = "user", .server = "server.i2p"}}; + EXPECT_EQ(doc.write_to_string(), "\n\n"); } } // namespace larra::xmpp