Add Stream Errors
This commit is contained in:
parent
145bf7bd80
commit
bb336da4d9
5 changed files with 320 additions and 60 deletions
|
@ -55,6 +55,7 @@ CPMAddPackage(
|
||||||
|
|
||||||
CPMAddPackage("gh:zeux/pugixml@1.14")
|
CPMAddPackage("gh:zeux/pugixml@1.14")
|
||||||
CPMAddPackage("gh:fmtlib/fmt#10.2.1")
|
CPMAddPackage("gh:fmtlib/fmt#10.2.1")
|
||||||
|
CPMAddPackage("gh:Neargye/nameof@0.10.4")
|
||||||
|
|
||||||
CPMAddPackage(
|
CPMAddPackage(
|
||||||
NAME spdlog
|
NAME spdlog
|
||||||
|
@ -162,12 +163,14 @@ target_include_directories(larra_xmpp PUBLIC
|
||||||
|
|
||||||
if(TARGET Boost::pfr)
|
if(TARGET Boost::pfr)
|
||||||
target_link_libraries(larra_xmpp PUBLIC
|
target_link_libraries(larra_xmpp PUBLIC
|
||||||
Boost::asio Boost::serialization utempl::utempl pugixml::pugixml OpenSSL::SSL
|
Boost::asio Boost::serialization utempl::utempl pugixml::pugixml
|
||||||
|
OpenSSL::SSL nameof::nameof
|
||||||
OpenSSL::Crypto spdlog xmlplusplus ${LIBXML2_LIBRARIES})
|
OpenSSL::Crypto spdlog xmlplusplus ${LIBXML2_LIBRARIES})
|
||||||
else()
|
else()
|
||||||
find_package(Boost 1.85.0 REQUIRED)
|
find_package(Boost 1.85.0 REQUIRED)
|
||||||
target_link_libraries(larra_xmpp PUBLIC
|
target_link_libraries(larra_xmpp PUBLIC
|
||||||
utempl::utempl ${Boost_LIBRARIES} pugixml::pugixml OpenSSL::SSL
|
utempl::utempl ${Boost_LIBRARIES} pugixml::pugixml OpenSSL::SSL
|
||||||
|
nameof::nameof
|
||||||
OpenSSL::Crypto spdlog xmlplusplus ${LIBXML2_LIBRARIES})
|
OpenSSL::Crypto spdlog xmlplusplus ${LIBXML2_LIBRARIES})
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
#include <boost/asio/use_awaitable.hpp>
|
#include <boost/asio/use_awaitable.hpp>
|
||||||
#include <boost/asio/write.hpp>
|
#include <boost/asio/write.hpp>
|
||||||
#include <boost/system/result.hpp>
|
#include <boost/system/result.hpp>
|
||||||
|
#include <larra/serialization.hpp>
|
||||||
#include <larra/utils.hpp>
|
#include <larra/utils.hpp>
|
||||||
#include <stack>
|
#include <stack>
|
||||||
#include <utempl/utils.hpp>
|
#include <utempl/utils.hpp>
|
||||||
|
@ -17,32 +18,6 @@ struct _xmlError;
|
||||||
|
|
||||||
namespace larra::xmpp {
|
namespace larra::xmpp {
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
concept AsXml = requires(xmlpp::Element* element, const T& obj) {
|
|
||||||
element << obj;
|
|
||||||
{ T::kDefaultName } -> std::convertible_to<const std::string&>;
|
|
||||||
};
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
concept HasDefaultNamespace = requires {
|
|
||||||
{ T::kDefaultNamespace } -> std::convertible_to<const std::string&>;
|
|
||||||
};
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
concept HasDefaultPrefix = requires {
|
|
||||||
{ T::kPrefix } -> std::convertible_to<const std::string&>;
|
|
||||||
};
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
concept HasAddXmlDecl = requires {
|
|
||||||
{ T::kAddXmlDecl } -> std::convertible_to<bool>;
|
|
||||||
};
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
concept HasRemoveEnd = requires {
|
|
||||||
{ T::kRemoveEnd } -> std::convertible_to<bool>;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct XmlGroup : xmlpp::Element {
|
struct XmlGroup : xmlpp::Element {
|
||||||
using Element::Element;
|
using Element::Element;
|
||||||
};
|
};
|
||||||
|
@ -240,16 +215,16 @@ struct RawXmlStream : Stream {
|
||||||
template <typename T>
|
template <typename T>
|
||||||
auto Read(auto& stream) -> boost::asio::awaitable<T> {
|
auto Read(auto& stream) -> boost::asio::awaitable<T> {
|
||||||
auto doc = co_await this->Read(stream);
|
auto doc = co_await this->Read(stream);
|
||||||
co_return T::Parse(doc->get_root_node());
|
co_return Serialization<T>::Parse(doc->get_root_node());
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
auto Read(auto& stream) -> boost::asio::awaitable<T>
|
auto Read(auto& stream) -> boost::asio::awaitable<T>
|
||||||
requires requires(std::unique_ptr<xmlpp::Document> ptr) {
|
requires requires(std::unique_ptr<xmlpp::Document> ptr) {
|
||||||
{ T::Parse(std::move(ptr)) } -> std::same_as<T>;
|
{ Serialization<T>::Parse(std::move(ptr)) } -> std::same_as<T>;
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
co_return T::Parse(co_await this->Read(stream));
|
co_return Serialization<T>::Parse(co_await this->Read(stream));
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
|
@ -286,36 +261,9 @@ struct RawXmlStream : Stream {
|
||||||
template <AsXml T>
|
template <AsXml T>
|
||||||
auto Send(const T& xso, auto& stream) const -> boost::asio::awaitable<void> {
|
auto Send(const T& xso, auto& stream) const -> boost::asio::awaitable<void> {
|
||||||
xmlpp::Document doc;
|
xmlpp::Document doc;
|
||||||
const std::string empty;
|
using S = Serialization<std::decay_t<T>>;
|
||||||
const std::string namespaceStr = [&] -> std::string {
|
S::Serialize(doc.create_root_node(S::kDefaultName, S::kDefaultNamespace, S::kPrefix), xso);
|
||||||
if constexpr(HasDefaultNamespace<T>) {
|
co_await this->Send(doc, stream, S::kAddXmlDecl, S::kRemoveEnd);
|
||||||
return T::kDefaultNamespace;
|
|
||||||
} else {
|
|
||||||
return empty;
|
|
||||||
}
|
|
||||||
}();
|
|
||||||
const std::string prefixStr = [&] -> decltype(auto) {
|
|
||||||
if constexpr(HasDefaultPrefix<T>) {
|
|
||||||
return T::kPrefix;
|
|
||||||
} else {
|
|
||||||
return empty;
|
|
||||||
}
|
|
||||||
}();
|
|
||||||
const bool bAddXmlDecl = [&] -> bool {
|
|
||||||
if constexpr(HasAddXmlDecl<T>) {
|
|
||||||
return T::kAddXmlDecl;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}();
|
|
||||||
const bool removeEnd = [&] -> bool {
|
|
||||||
if constexpr(HasRemoveEnd<T>) {
|
|
||||||
return T::kRemoveEnd;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}();
|
|
||||||
|
|
||||||
doc.create_root_node(T::kDefaultName, namespaceStr, prefixStr) << xso;
|
|
||||||
co_await this->Send(doc, stream, bAddXmlDecl, removeEnd);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Send(const AsXml auto& xso) -> boost::asio::awaitable<void> {
|
auto Send(const AsXml auto& xso) -> boost::asio::awaitable<void> {
|
||||||
|
|
143
library/include/larra/serialization.hpp
Normal file
143
library/include/larra/serialization.hpp
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
#pragma once
|
||||||
|
#include <libxml++/libxml++.h>
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <utempl/utils.hpp>
|
||||||
|
|
||||||
|
namespace larra::xmpp {
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
concept AsXml = requires(xmlpp::Element* element, const T& obj) {
|
||||||
|
element << obj;
|
||||||
|
{ T::kDefaultName } -> std::convertible_to<const std::string&>;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
concept HasDefaultName = requires {
|
||||||
|
{ T::kDefaultName } -> std::convertible_to<const std::string&>;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
concept HasDefaultNamespace = requires {
|
||||||
|
{ T::kDefaultNamespace } -> std::convertible_to<const std::string&>;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
concept HasDefaultPrefix = requires {
|
||||||
|
{ T::kPrefix } -> std::convertible_to<const std::string&>;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
concept HasAddXmlDecl = requires {
|
||||||
|
{ T::kAddXmlDecl } -> std::convertible_to<bool>;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
concept HasRemoveEnd = requires {
|
||||||
|
{ T::kRemoveEnd } -> std::convertible_to<bool>;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
concept HasTryParse = requires(xmlpp::Element* element) {
|
||||||
|
{ T::TryParse(element) } -> std::same_as<std::optional<T>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T = void>
|
||||||
|
struct SerializationBase {
|
||||||
|
inline static const std::string kDefaultName = [] {
|
||||||
|
if constexpr(HasDefaultName<T>) {
|
||||||
|
return T::kDefaultName;
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
inline static const std::string kDefaultNamespace = [] {
|
||||||
|
if constexpr(HasDefaultNamespace<T>) {
|
||||||
|
return T::kDefaultNamespace;
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
inline static const std::string kPrefix = [] {
|
||||||
|
if constexpr(HasDefaultPrefix<T>) {
|
||||||
|
return T::kPrefix;
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
static constexpr bool kAddXmlDecl = [] {
|
||||||
|
if constexpr(HasAddXmlDecl<T>) {
|
||||||
|
return T::kAddXmlDecl;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
static constexpr bool kRemoveEnd = [] {
|
||||||
|
if constexpr(HasRemoveEnd<T>) {
|
||||||
|
return T::kRemoveEnd;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
struct Serialization : SerializationBase<T> {
|
||||||
|
[[nodiscard]] static constexpr auto Parse(xmlpp::Element* element) -> T {
|
||||||
|
return T::Parse(element);
|
||||||
|
}
|
||||||
|
[[nodiscard]] static constexpr auto TryParse(xmlpp::Element* element) -> std::optional<T> {
|
||||||
|
if constexpr(HasTryParse<T>) {
|
||||||
|
return T::TryParse(element);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
return T::Parse(element);
|
||||||
|
} catch(const std::exception& e) {
|
||||||
|
SPDLOG_WARN("Failed Parse but no TryParse found: {}", e.what());
|
||||||
|
} catch(...) {
|
||||||
|
SPDLOG_WARN("Failed Parse but no TryParse found");
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
static constexpr auto Serialize(xmlpp::Element* element, const T& object) -> void {
|
||||||
|
element << object;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
struct Serialization<std::optional<T>> : SerializationBase<T> {
|
||||||
|
[[nodiscard]] static constexpr auto Parse(xmlpp::Element* element) -> std::optional<T> {
|
||||||
|
return Serialization<T>::TryParse(element);
|
||||||
|
}
|
||||||
|
[[nodiscard]] static constexpr auto TryParse(xmlpp::Element* element) -> std::optional<std::optional<T>> {
|
||||||
|
auto response = Serialization<T>::TryParse(element);
|
||||||
|
return response ? *response : std::nullopt;
|
||||||
|
}
|
||||||
|
static constexpr auto Serialize(xmlpp::Element* element, const T& object) -> void {
|
||||||
|
element << object;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename... Ts>
|
||||||
|
struct Serialization<std::variant<Ts...>> : SerializationBase<> {
|
||||||
|
[[nodiscard]] static constexpr auto TryParse(xmlpp::Element* element) -> std::optional<std::variant<Ts...>> {
|
||||||
|
return utempl::FirstOf(utempl::Tuple{[&] {
|
||||||
|
return Serialization<Ts>::TryParse(element);
|
||||||
|
}...},
|
||||||
|
std::optional<std::variant<Ts...>>{});
|
||||||
|
}
|
||||||
|
[[nodiscard]] static constexpr auto Parse(xmlpp::Element* element) -> std::variant<Ts...> {
|
||||||
|
return Serialization::TryParse(element).value();
|
||||||
|
}
|
||||||
|
static constexpr auto Serialize(xmlpp::Element* element, const std::variant<Ts...>& object) -> void {
|
||||||
|
std::visit(
|
||||||
|
[&](const auto& object) {
|
||||||
|
element << object;
|
||||||
|
},
|
||||||
|
object);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace larra::xmpp
|
138
library/include/larra/stream_error.hpp
Normal file
138
library/include/larra/stream_error.hpp
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
#pragma once
|
||||||
|
#include <libxml++/libxml++.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <nameof.hpp>
|
||||||
|
#include <ranges>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
|
namespace larra::xmpp {
|
||||||
|
|
||||||
|
namespace impl {
|
||||||
|
// std::isupper not declared as constexpr
|
||||||
|
constexpr auto IsUpper(char ch) -> bool {
|
||||||
|
return ch >= 'A' && ch <= 'Z';
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr auto ToLower(char ch) -> char {
|
||||||
|
return (ch >= 'A' && ch <= 'Z') ? static_cast<char>(ch + ('a' - 'A')) : ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
constexpr auto ToKebabCaseName() -> std::string_view {
|
||||||
|
static constexpr auto rawStr = nameof::nameof_short_type<T>();
|
||||||
|
|
||||||
|
constexpr auto str = [] {
|
||||||
|
return rawStr | std::views::transform([](auto ch) {
|
||||||
|
return impl::IsUpper(ch) ? std::array<char, 2>{'-', impl::ToLower(ch)} : std::array<char, 2>{ch, '\0'};
|
||||||
|
}) |
|
||||||
|
std::views::join | std::views::filter([](char ch) {
|
||||||
|
return ch != '\0';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
constexpr auto size = std::ranges::fold_left(str(),
|
||||||
|
std::size_t{},
|
||||||
|
[](auto accum, auto) {
|
||||||
|
return accum + 1;
|
||||||
|
}) -
|
||||||
|
1;
|
||||||
|
static constexpr auto arr = [&] {
|
||||||
|
std::array<char, size> response; // NOLINT
|
||||||
|
std::ranges::copy(str() | std::views::drop(1), response.begin());
|
||||||
|
return response;
|
||||||
|
}();
|
||||||
|
return {arr.data(), size};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace impl
|
||||||
|
|
||||||
|
namespace error::stream {
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
struct BaseError : std::exception {
|
||||||
|
static constexpr auto kDefaultName = "error";
|
||||||
|
static constexpr auto kDefaultNamespace = "stream";
|
||||||
|
static inline const auto kKebabCaseName = static_cast<std::string>(impl::ToKebabCaseName<T>());
|
||||||
|
static inline const std::string kErrorContentNamespace = "urn:ietf:params:xml:ns:xmpp-streams";
|
||||||
|
static constexpr auto kErrorMessage = [] -> std::string_view {
|
||||||
|
static constexpr auto str = [] {
|
||||||
|
return std::array{std::string_view{"Stream Error: "}, nameof::nameof_short_type<T>(), std::string_view{"\0", 1}} | std::views::join;
|
||||||
|
};
|
||||||
|
constexpr auto size = std::ranges::fold_left(str(), std::size_t{}, [](auto accum, auto) {
|
||||||
|
return accum + 1;
|
||||||
|
});
|
||||||
|
static constexpr auto array = [] {
|
||||||
|
std::array<char, size> response; // NOLINT
|
||||||
|
std::ranges::copy(str(), response.begin());
|
||||||
|
return response;
|
||||||
|
}();
|
||||||
|
return {array.data(), array.size() - 1};
|
||||||
|
}();
|
||||||
|
static constexpr auto TryParse(xmlpp::Element* element) -> std::optional<T> {
|
||||||
|
return element ? element->get_first_child(kKebabCaseName) ? std::optional{T{}} : std::nullopt : std::nullopt;
|
||||||
|
}
|
||||||
|
static constexpr auto Parse(xmlpp::Element* element) -> T {
|
||||||
|
return TryParse(element).value();
|
||||||
|
}
|
||||||
|
friend constexpr auto operator<<(xmlpp::Element* element, const T& obj) -> void {
|
||||||
|
auto node = element->add_child_element(kKebabCaseName);
|
||||||
|
node->set_namespace_declaration(kErrorContentNamespace);
|
||||||
|
}
|
||||||
|
[[nodiscard]] constexpr auto what() const noexcept -> const char* override {
|
||||||
|
return kErrorMessage.data();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BadFormat : BaseError<BadFormat> {};
|
||||||
|
struct BadNamespacePrefix : BaseError<BadNamespacePrefix> {};
|
||||||
|
struct Conflict : BaseError<Conflict> {};
|
||||||
|
struct ConnectionTimeout : BaseError<ConnectionTimeout> {};
|
||||||
|
struct HostGone : BaseError<HostGone> {};
|
||||||
|
struct HostUnknown : BaseError<HostUnknown> {};
|
||||||
|
struct ImproperAdressing : BaseError<ImproperAdressing> {};
|
||||||
|
struct InternalServerError : BaseError<InternalServerError> {};
|
||||||
|
struct InvalidForm : BaseError<InvalidForm> {};
|
||||||
|
struct InvalidNamespace : BaseError<InvalidNamespace> {};
|
||||||
|
struct InvalidXml : BaseError<InvalidXml> {};
|
||||||
|
struct NotAuthorized : BaseError<NotAuthorized> {};
|
||||||
|
struct NotWellFormed : BaseError<NotWellFormed> {};
|
||||||
|
struct PolicyViolation : BaseError<PolicyViolation> {};
|
||||||
|
struct RemoteConnectionFailed : BaseError<RemoteConnectionFailed> {};
|
||||||
|
struct Reset : BaseError<Reset> {};
|
||||||
|
struct ResourceConstraint : BaseError<ResourceConstraint> {};
|
||||||
|
struct RestrictedXml : BaseError<RestrictedXml> {};
|
||||||
|
struct SeeOtherHost : BaseError<SeeOtherHost> {};
|
||||||
|
struct SystemShutdown : BaseError<SystemShutdown> {};
|
||||||
|
struct UndefinedCondition : BaseError<UndefinedCondition> {};
|
||||||
|
struct UnsupportedEncoding : BaseError<UnsupportedEncoding> {};
|
||||||
|
struct UnsupportedFeature : BaseError<UnsupportedFeature> {};
|
||||||
|
struct UnsupportedStanzaType : BaseError<UnsupportedStanzaType> {};
|
||||||
|
struct UnsupportedVersion : BaseError<UnsupportedVersion> {};
|
||||||
|
|
||||||
|
} // namespace error::stream
|
||||||
|
|
||||||
|
using StreamError = std::variant<error::stream::BadFormat,
|
||||||
|
error::stream::BadNamespacePrefix,
|
||||||
|
error::stream::Conflict,
|
||||||
|
error::stream::ConnectionTimeout,
|
||||||
|
error::stream::HostGone,
|
||||||
|
error::stream::HostUnknown,
|
||||||
|
error::stream::ImproperAdressing,
|
||||||
|
error::stream::InternalServerError,
|
||||||
|
error::stream::InvalidForm,
|
||||||
|
error::stream::InvalidNamespace,
|
||||||
|
error::stream::InvalidXml,
|
||||||
|
error::stream::NotAuthorized,
|
||||||
|
error::stream::NotWellFormed,
|
||||||
|
error::stream::PolicyViolation,
|
||||||
|
error::stream::RemoteConnectionFailed,
|
||||||
|
error::stream::RestrictedXml,
|
||||||
|
error::stream::SeeOtherHost,
|
||||||
|
error::stream::SystemShutdown,
|
||||||
|
error::stream::UndefinedCondition,
|
||||||
|
error::stream::UnsupportedEncoding,
|
||||||
|
error::stream::UnsupportedFeature,
|
||||||
|
error::stream::UnsupportedStanzaType,
|
||||||
|
error::stream::UnsupportedVersion>;
|
||||||
|
|
||||||
|
} // namespace larra::xmpp
|
28
tests/serialization.cpp
Normal file
28
tests/serialization.cpp
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <larra/serialization.hpp>
|
||||||
|
#include <larra/stream_error.hpp>
|
||||||
|
|
||||||
|
namespace larra::xmpp {
|
||||||
|
|
||||||
|
TEST(Parse, Variant) {
|
||||||
|
xmlpp::Document doc;
|
||||||
|
auto node = doc.create_root_node("stream:error");
|
||||||
|
node->add_child_element("unsupported-stanza-type");
|
||||||
|
node->set_namespace_declaration("urn:ietf:params:xml:ns:xmpp-streams");
|
||||||
|
auto streamError = Serialization<StreamError>::Parse(node);
|
||||||
|
EXPECT_TRUE(std::get_if<error::stream::UnsupportedStanzaType>(&streamError));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Serialize, Variant) {
|
||||||
|
using S = Serialization<StreamError>;
|
||||||
|
StreamError data = error::stream::UnsupportedStanzaType{};
|
||||||
|
xmlpp::Document doc;
|
||||||
|
auto node = doc.create_root_node("stream:error");
|
||||||
|
S::Serialize(node, data);
|
||||||
|
EXPECT_EQ(doc.write_to_string(),
|
||||||
|
std::string_view{"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<stream:error><unsupported-stanza-type "
|
||||||
|
"xmlns=\"urn:ietf:params:xml:ns:xmpp-streams\"/></stream:error>\n"});
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace larra::xmpp
|
Loading…
Reference in a new issue