From 00e885a9e6799999bf077036236d2e3dfa1ce489 Mon Sep 17 00:00:00 2001 From: sha512sum Date: Sat, 21 Dec 2024 18:56:09 +1100 Subject: [PATCH] Add message serialization and deserialization --- .devcontainer/Dockerfile | 2 +- .forgejo/workflows/pr_check.yaml | 12 +- library/include/larra/client/client.hpp | 3 + library/include/larra/message.hpp | 156 +++++++++++++++++++ library/include/larra/serialization/auto.hpp | 119 ++++++++++++-- library/include/larra/xml_language.hpp | 20 +++ tests/main.cpp | 12 +- tests/message.cpp | 54 +++++++ tests/roster.cpp | 2 +- tests/xml_lang.cpp | 42 +++++ 10 files changed, 400 insertions(+), 22 deletions(-) create mode 100644 library/include/larra/message.hpp create mode 100644 library/include/larra/xml_language.hpp create mode 100644 tests/message.cpp create mode 100644 tests/xml_lang.cpp diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 452e3b1..4b87c43 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -30,7 +30,7 @@ RUN groupadd docker &&\ USER dev # Add cached layer with the latest LLVM-${LLVM_VER} -ARG LLVM_VER=19.1.1 +ARG LLVM_VER=19.1.5 ENV LLVM_VER=${LLVM_VER} RUN sudo mkdir -p /home/artifacts ADD https://github.com/llvm/llvm-project/releases/download/llvmorg-${LLVM_VER}/LLVM-${LLVM_VER}-Linux-X64.tar.xz /home/artifacts/ diff --git a/.forgejo/workflows/pr_check.yaml b/.forgejo/workflows/pr_check.yaml index a9f6f7c..3a63ce1 100644 --- a/.forgejo/workflows/pr_check.yaml +++ b/.forgejo/workflows/pr_check.yaml @@ -13,9 +13,9 @@ jobs: pacman -S --noconfirm cmake make gcc ninja meson pacman -S --noconfirm gtk4 gtkmm-4.0 boost spdlog fmt libxml++-5.0 gtest - - name: Setup environment - Install LLVM-19.1.3 + - name: Setup environment - Install LLVM-19.1.5 run: | - export LLVM_VER=19.1.3 + export LLVM_VER=19.1.5 echo "::group::Download LLVM-${LLVM_VER}" wget https://github.com/llvm/llvm-project/releases/download/llvmorg-${LLVM_VER}/LLVM-${LLVM_VER}-Linux-X64.tar.xz -O /LLVM-${LLVM_VER}-Linux-X64.tar.xz @@ -108,7 +108,7 @@ jobs: id: clang_format_check continue-on-error: true run: | - export LLVM_VER=19.1.3 + export LLVM_VER=19.1.5 export PATH="/home/LLVM-${LLVM_VER}/bin:${PATH}" cd ${{ github.workspace }} export REPO_FILES=$(cat repo_files_to_check.txt) @@ -150,7 +150,7 @@ jobs: id: clang_build continue-on-error: true run: | - export LLVM_VER=19.1.3 + export LLVM_VER=19.1.5 export PATH="/home/LLVM-${LLVM_VER}/bin:${PATH}" mkdir -p ${{ github.workspace }}/build_clang @@ -176,7 +176,7 @@ jobs: id: clang_tidy continue-on-error: true run: | - export LLVM_VER=19.1.3 + export LLVM_VER=19.1.5 export PATH="/home/LLVM-${LLVM_VER}/bin:${PATH}" cd ${{ github.workspace }} @@ -201,7 +201,7 @@ jobs: #- name: Clang unit tests with -fsanitize=address # run: | - # export LLVM_VER=19.1.3 + # export LLVM_VER=19.1.5 # export PATH="/home/LLVM-${LLVM_VER}/bin:${PATH}" # cd ${{ github.workspace }}/build_clang # ASAN_SYMBOLIZER_PATH=llvm-symbolizer ASAN_OPTIONS=detect_stack_use_after_return=1:check_initialization_order=1:detect_leaks=1:atexit=1:abort_on_error=1 ./larra_xmpp_tests --log_level=0 diff --git a/library/include/larra/client/client.hpp b/library/include/larra/client/client.hpp index 9fbdd3f..d822e2a 100644 --- a/library/include/larra/client/client.hpp +++ b/library/include/larra/client/client.hpp @@ -107,6 +107,9 @@ struct Client { std::move(get_roster_response)); co_return; } + [[nodiscard]] auto Roster() const -> const std::vector& { + return this->roster.items; + } private: bool active = true; diff --git a/library/include/larra/message.hpp b/library/include/larra/message.hpp new file mode 100644 index 0000000..eb3f46d --- /dev/null +++ b/library/include/larra/message.hpp @@ -0,0 +1,156 @@ +#pragma once +#include +#include +#include + +namespace larra::xmpp { + +namespace message { +struct Body { + std::string content; + std::optional language; + static constexpr auto kDefaultName = "body"; + constexpr auto operator==(const Body& other) const -> bool = default; + friend constexpr auto operator<<(xmlpp::Element* node, const Body& message) -> void { + node->add_child_text(message.content); + } + static constexpr auto Parse(xmlpp::Element* node) -> Body { + auto ptr = node->get_first_child_text(); + if(!ptr) { + throw std::runtime_error("Message::Body: [ Text node not found ]"); + } + auto lang = node->get_attribute("lang", "xml"); + return { + .content = ptr->get_content(), // + .language = lang ? std::optional{XmlLanguage::Parse(lang->get_value())} : std::nullopt // + }; + } +}; + +namespace type { + +struct Chat { + static constexpr auto TryParse(std::string_view value) -> std::optional { + return value == "chat" ? std::optional{Chat{}} : std::nullopt; + } + friend constexpr auto ToString(const Chat&) -> std::string { + return "chat"; + } + constexpr auto operator==(const Chat&) const -> bool { + return true; + }; +}; + +static constexpr auto kChat = Chat{}; + +struct Error { + static constexpr auto TryParse(std::string_view value) -> std::optional { + return value == "error" ? std::optional{Error{}} : std::nullopt; + } + friend constexpr auto ToString(const Error&) -> std::string { + return "error"; + } + constexpr auto operator==(const Error&) const -> bool { + return true; + }; +}; + +static constexpr auto kError = Error{}; + +struct GroupChat { + static constexpr auto TryParse(std::string_view value) -> std::optional { + return value == "groupchat" ? std::optional{GroupChat{}} : std::nullopt; + } + friend constexpr auto ToString(const GroupChat&) -> std::string { + return "groupchat"; + } + constexpr auto operator==(const GroupChat&) const -> bool { + return true; + }; +}; + +static constexpr auto kGroupChat = GroupChat{}; + +struct Headline { + static constexpr auto TryParse(std::string_view value) -> std::optional { + return value == "headline" ? std::optional{Headline{}} : std::nullopt; + } + friend constexpr auto ToString(const Headline&) -> std::string { + return "headline"; + } + constexpr auto operator==(const Headline&) const -> bool { + return true; + }; +}; + +static constexpr auto kHeadline = Headline{}; + +struct Normal { + static constexpr auto Parse(std::string_view value) -> Normal { + return value == "normal" ? Normal{} + : throw std::runtime_error( + std::format(R"(message::type::Normal Parsing error: [ expected "normal" but "{}" found ])", value)); + } + friend constexpr auto ToString(const Normal&) -> std::string { + return "normal"; + } + constexpr auto operator==(const Normal&) const -> bool { + return true; + }; +}; + +static constexpr auto kNormal = Normal{}; + +} // namespace type + +using TypeVariant = std::variant; + +struct Type : TypeVariant { + using TypeVariant::variant; + constexpr Type(TypeVariant variant) : TypeVariant(std::move(variant)) { + } + + static constexpr auto GetDefault() -> Type { + return type::kNormal; + } + + friend constexpr auto ToString(const Type& value) -> std::string { + return std::visit( + [](const auto& value) { + return ToString(value); + }, + value); + } +}; + +} // namespace message + +template +struct Message { + static constexpr auto kDefaultName = "message"; + static auto Parse(xmlpp::Element* element) -> Message { + return serialization::Parse(element); + } + friend auto operator<<(xmlpp::Element* element, const Message& message) -> void { + serialization::Serialize(element, message); + } + constexpr auto operator==(const Message& other) const -> bool = default; + From from; + To to; + message::Type type; + std::optional id; + XmlLanguage language; + std::vector body; +}; + +template +struct serialization::SerializationConfigT> { + static constexpr auto kValue = serialization::SerializationConfig>{} // + .template With<"type">(serialization::AttributeConfig{}) + .template With<"body">(serialization::Config>{}); +}; + +template +struct serialization::AttributeSerializer : serialization::AttributeSerializer {}; + +} // namespace larra::xmpp diff --git a/library/include/larra/serialization/auto.hpp b/library/include/larra/serialization/auto.hpp index 79486eb..7c19a18 100644 --- a/library/include/larra/serialization/auto.hpp +++ b/library/include/larra/serialization/auto.hpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include #include @@ -16,10 +15,20 @@ template struct Tag {}; template -inline constexpr auto kSerializationConfig = std::monostate{}; +struct SerializationConfigT { + static constexpr auto kValue = std::monostate{}; +}; template -inline constexpr auto kDeserializationConfig = kSerializationConfig; +inline constexpr auto kSerializationConfig = SerializationConfigT::kValue; + +template +struct DeserializationConfigT { + static constexpr auto kValue = kSerializationConfig; +}; + +template +inline constexpr auto kDeserializationConfig = DeserializationConfigT::kValue; template constexpr auto Parse(xmlpp::Element* element, Tag = {}) -> T @@ -55,10 +64,24 @@ struct MetaInfo { } }; +template +struct DefaultValue {}; + +template + requires requires { + { T::GetDefault() } -> std::convertible_to; + } +struct DefaultValue { + static constexpr auto Default() { + return T::GetDefault(); + } +}; + template struct FieldInfo { using Main = MainT; using Info = MetaInfo
; + using Type = Info::template TupleElement; static inline const std::string kName = [] { if constexpr(requires { Info::template TupleElement::kDefaultName; }) { return Info::template TupleElement::kDefaultName; @@ -66,6 +89,13 @@ struct FieldInfo { return static_cast(Info::template kFieldName); } }(); + static inline const std::string kNamespace = [] { + if constexpr(requires { Info::template TupleElement::kDefaultNamespace; }) { + return Info::template TupleElement::kDefaultNamespace; + } else { + return std::string{}; + } + }(); }; template @@ -216,9 +246,16 @@ struct ElementSerializer, Config, Info> { template struct AttributeSerializer { static constexpr auto Parse(xmlpp::Element* element) -> T { - auto node = element->get_attribute(Info::kName); + auto node = element->get_attribute(Info::kName, Info::kNamespace); if(!node) { - throw AttributeParsingError(std::format("Attribute [{}: {}] parsing error", Info::kName, nameof::nameof_full_type())); + if constexpr(requires { DefaultValue::Default(); }) { + return DefaultValue::Default(); + } + throw AttributeParsingError(std::format("Attribute [{}{}{}: {}] parsing error", + Info::kNamespace, + Info::kNamespace == std::string{} ? "" : ":", + Info::kName, + nameof::nameof_full_type())); } if constexpr(requires(std::string_view view) { T::Parse(view); }) { return T::Parse(node->get_value()); @@ -226,13 +263,26 @@ struct AttributeSerializer { return node->get_value(); } } + static constexpr auto TryParse(xmlpp::Element* element) -> std::optional + requires requires { T::TryParse(std::string_view{}); } + { + auto node = element->get_attribute(Info::kName, Info::kNamespace); + if(!node) { + throw AttributeParsingError(std::format("Attribute [{}{}{}: {}] parsing error", + Info::kNamespace, + Info::kNamespace == std::string{} ? "" : ":", + Info::kName, + nameof::nameof_full_type())); + } + return T::TryParse(node->get_value()); + } static constexpr auto Serialize(xmlpp::Element* element, const T& obj) { if constexpr(requires { { ToString(obj) } -> std::convertible_to; }) { - element->set_attribute(Info::kName, ToString(obj)); + element->set_attribute(Info::kName, ToString(obj), Info::kNamespace); } else { - element->set_attribute(Info::kName, obj); + element->set_attribute(Info::kName, obj, Info::kNamespace); } } }; @@ -240,8 +290,13 @@ struct AttributeSerializer { template struct AttributeSerializer, Info> { static constexpr auto Parse(xmlpp::Element* element) -> std::optional { - auto node = element->get_attribute(Info::kName); - return node ? std::optional{AttributeSerializer::Parse(element)} : std::nullopt; + auto node = element->get_attribute(Info::kName, Info::kNamespace); + return node ? std::optional{AttributeSerializer::Parse(element)} : [] -> std::optional { + if constexpr(requires { DefaultValue::Default(); }) { + return DefaultValue::Default(); + } + return std::nullopt; + }(); } static constexpr auto Serialize(xmlpp::Element* element, const std::optional& obj) { if(obj) { @@ -250,6 +305,52 @@ struct AttributeSerializer, Info> { } }; +template +struct AttributeSerializer, Info> { + template + static constexpr auto FunctionForType(xmlpp::Element* element) { + return [=] -> std::optional { + if constexpr(requires { AttributeSerializer::TryParse(element); }) { + return AttributeSerializer::TryParse(element); + } else { + try { + return AttributeSerializer::Parse(element); + } catch(...) { + return std::nullopt; + } + } + }; + } + static constexpr auto Parse(xmlpp::Element* element) -> std::variant { + using T = Info::Type; + auto node = element->get_attribute(Info::kName, Info::kNamespace); + if(!node) { + if constexpr(requires { DefaultValue::Default(); }) { + return DefaultValue::Default(); + } + throw AttributeParsingError(std::format("Attribute [{}{}{}: {}] parsing error", + Info::kNamespace, + Info::kNamespace == std::string{} ? "" : ":", + Info::kName, + nameof::nameof_full_type())); + } + return [&](utempl::TypeList) { + // operator* is safe because in or_else Parse returns Ts...[sizeof...(Ts) - 1] (not optional) + return *utempl::FirstOf(utempl::Tuple{FunctionForType(element)...}, std::optional>{}) + .or_else([&] -> std::optional> { + return AttributeSerializer(utempl::kTypeList)), Info>::Parse(element); + }); + }(utempl::TakeFrom(utempl::kTypeList)); + } + static constexpr auto Serialize(xmlpp::Element* element, const std::variant& obj) { + std::visit( + [&](const T& value) { + AttributeSerializer::Serialize(element, value); + }, + obj); + } +}; + namespace impl { template diff --git a/library/include/larra/xml_language.hpp b/library/include/larra/xml_language.hpp new file mode 100644 index 0000000..e897208 --- /dev/null +++ b/library/include/larra/xml_language.hpp @@ -0,0 +1,20 @@ +#pragma once +#include +namespace larra::xmpp { + +struct XmlLanguage : std::string { + static constexpr auto kDefaultName = "lang"; + static constexpr auto kDefaultNamespace = "xml"; + using std::string::basic_string; + constexpr XmlLanguage() : std::string{"en"} {}; + constexpr XmlLanguage(std::string str) : std::string(std::move(str)) { + } + static constexpr auto Parse(std::string_view view) -> XmlLanguage { + return static_cast(view); + } + friend constexpr auto ToString(XmlLanguage value) -> std::string { + return std::move(static_cast(value)); + } +}; + +} // namespace larra::xmpp diff --git a/tests/main.cpp b/tests/main.cpp index 45ab76b..f49e3ee 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -30,7 +30,7 @@ auto main(int argc, char** argv) -> int { // Define options po::options_description desc("Allowed options"); desc.add_options()("help,h", "Print help message")("log_level,l", - po::value()->default_value(SPDLOG_LEVEL_INFO), + po::value()->default_value(SPDLOG_LEVEL_OFF), "Set log level: 0=TRACE, 1=DEBUG, 2=INFO, 3=WARN, 4=ERROR, 5=CRITICAL, 6=OFF"); // Parse command-line arguments @@ -54,8 +54,10 @@ auto main(int argc, char** argv) -> int { } // Cmd options handling - spdlog::set_level(static_cast(vm["log_level"].as())); - std::println("\nEnvironment setup:\n\tCurrently set log level: {}\n", ToString(spdlog::get_level())); - + auto level = static_cast(vm["log_level"].as()); + spdlog::set_level(level); + if(level != spdlog::level::level_enum::off) { + std::println("\nEnvironment setup:\n\tCurrently set log level: {}\n", ToString(spdlog::get_level())); + } return RUN_ALL_TESTS(); -} \ No newline at end of file +} diff --git a/tests/message.cpp b/tests/message.cpp new file mode 100644 index 0000000..68826af --- /dev/null +++ b/tests/message.cpp @@ -0,0 +1,54 @@ +#include + +#include +#include + +namespace larra::xmpp { + +namespace { + +auto CreateTestData() { + auto doc = std::make_unique(); + auto node = doc->create_root_node("message"); + node->set_attribute("from", "user1@server.i2p"); + node->set_attribute("to", "user2@server.i2p"); + node->set_attribute("type", "chat"); + node->set_attribute("id", "1"); + node->set_attribute("lang", "en", "xml"); + + auto bodyNode = node->add_child_element("body"); + bodyNode->add_child_text("hello"); + return doc; +} + +const Message kMessage{ + .from = {.username = "user1", .server = "server.i2p"}, + .to = {.username = "user2", .server = "server.i2p"}, + .type = message::type::kChat, + .id = "1", + .language = "en", + .body = {{.content = "hello"}} // +}; + +} // namespace + +TEST(Parse, Message) { + auto doc = CreateTestData(); + auto node = doc->get_root_node(); + auto message = Serialization>::Parse(node); + + EXPECT_EQ(message, kMessage); +} + +TEST(Serialize, Message) { + auto expected = + R"( +hello +)"; + xmlpp::Document doc; + auto node = doc.create_root_node("message"); + Serialization>::Serialize(node, kMessage); + EXPECT_EQ(doc.write_to_string(), expected); +} + +} // namespace larra::xmpp diff --git a/tests/roster.cpp b/tests/roster.cpp index 6944a0c..75935fe 100644 --- a/tests/roster.cpp +++ b/tests/roster.cpp @@ -28,7 +28,7 @@ TEST(Roster, Print) { EXPECT_NO_THROW({ auto rosterStr = ToString(roster.payload); - EXPECT_EQ(kRosterPrintExpectedData.length(), rosterStr.capacity()); + EXPECT_EQ(kRosterPrintExpectedData.length(), rosterStr.length()); EXPECT_EQ(kRosterPrintExpectedData, rosterStr); }); } diff --git a/tests/xml_lang.cpp b/tests/xml_lang.cpp new file mode 100644 index 0000000..73c4c45 --- /dev/null +++ b/tests/xml_lang.cpp @@ -0,0 +1,42 @@ +#include + +#include +#include + +namespace larra::xmpp { + +namespace tests::message { + +struct SomeStruct { + static constexpr auto kDefaultName = "some"; + XmlLanguage language; + friend auto operator<<(xmlpp::Element*, const SomeStruct&) -> void; + static auto Parse(xmlpp::Element* node) -> SomeStruct; +}; + +} // namespace tests::message +template <> +constexpr auto serialization::kSerializationConfig = + serialization::SerializationConfig{}; + +namespace tests::message { + +auto SomeStruct::Parse(xmlpp::Element* element) -> SomeStruct { + return serialization::Parse(element); +} + +auto operator<<(xmlpp::Element* element, const SomeStruct& some) -> void { + serialization::Serialize(element, some); +}; + +} // namespace tests::message + +TEST(Parse, XmlLanguage) { + xmlpp::Document doc; + auto node = doc.create_root_node("some"); + node->set_attribute("lang", "en", "xml"); + auto value = Serialization::Parse(node); + EXPECT_EQ(value.language, "en"); +} + +} // namespace larra::xmpp