Features: iq::Bind and iq::Roster #4

Merged
sha512sum merged 3 commits from feature_roster_on_login into main 2024-11-26 15:25:56 +00:00
14 changed files with 364 additions and 30 deletions

12
.vscode/launch.json vendored
View file

@ -12,6 +12,18 @@
"args": [], "args": [],
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"preLaunchTask": "GCC: Build" "preLaunchTask": "GCC: Build"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug: tests",
"program": "${workspaceFolder}/build/larra_xmpp_tests",
"args": [
// --gtest_filter=POSTIVE_PATTERNS[-NEGATIVE_PATTERNS]
// "--gtest_filter=Roster*"
],
"cwd": "${workspaceFolder}",
"preLaunchTask": "GCC: Build"
} }
] ]
} }

2
.vscode/tasks.json vendored
View file

@ -104,7 +104,7 @@
"presentation": { "presentation": {
"clear": true "clear": true
}, },
"hide": true, "hide": false,
"group": { "group": {
"kind": "build" "kind": "build"
} }

View file

@ -1,3 +1,4 @@
#include <spdlog/common.h>
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include <boost/asio/co_spawn.hpp> #include <boost/asio/co_spawn.hpp>
@ -7,6 +8,8 @@
#include <larra/printer_stream.hpp> #include <larra/printer_stream.hpp>
#include <print> #include <print>
namespace iq = larra::xmpp::iq;
auto Coroutine() -> boost::asio::awaitable<void> { auto Coroutine() -> boost::asio::awaitable<void> {
SPDLOG_INFO("Connecting client..."); SPDLOG_INFO("Connecting client...");
@ -14,11 +17,32 @@ auto Coroutine() -> boost::asio::awaitable<void> {
auto client = co_await larra::xmpp::client::CreateClient<larra::xmpp::PrintStream<boost::asio::ip::tcp::socket>>( auto client = co_await larra::xmpp::client::CreateClient<larra::xmpp::PrintStream<boost::asio::ip::tcp::socket>>(
larra::xmpp::PlainUserAccount{.jid = {.username = "test1", .server = "localhost"}, .password = "test1"}, larra::xmpp::PlainUserAccount{.jid = {.username = "test1", .server = "localhost"}, .password = "test1"},
{.useTls = larra::xmpp::client::Options::kNever}); {.useTls = larra::xmpp::client::Options::kNever});
// rfc6120 7.1
// After a client authenticates with a server,
// it MUST bind a specific resource to the stream so that the server can properly address the client.
co_await std::visit( co_await std::visit(
[](auto& client) -> boost::asio::awaitable<void> { [](auto& client) -> boost::asio::awaitable<void> {
co_await client.CreateResourceBind();
},
client);
co_await std::visit(
[](auto& client) -> boost::asio::awaitable<void> {
co_await client.UpdateListOfContacts();
},
client);
// rfc6120 2.2
// Upon authenticating with a server and binding a resource (thus becoming a connected resource as defined in [XMPPCORE]),
// a client SHOULD request the roster before sending initial presence
co_await std::visit(
[](auto& client) -> boost::asio::awaitable<void> {
SPDLOG_INFO("Send presence: Available");
co_await client.Send(larra::xmpp::presence::c2s::Available{}); co_await client.Send(larra::xmpp::presence::c2s::Available{});
}, },
client); client);
} catch(const std::exception& err) { } catch(const std::exception& err) {
SPDLOG_ERROR("{}", err.what()); SPDLOG_ERROR("{}", err.what());
co_return; co_return;

View file

@ -0,0 +1,24 @@
#pragma once
#include <libxml++/libxml++.h>
#include <spdlog/spdlog.h>
#include <larra/iq.hpp>
#include <larra/jid.hpp>
#include <optional>
namespace larra::xmpp::iq {
struct Bind {
static constexpr auto kDefaultName = "bind";
static constexpr auto kDefaultNamespace = "urn:ietf:params:xml:ns:xmpp-bind";
std::optional<FullJid> jid;
friend auto operator<<(xmlpp::Element* element, const Bind& bind) -> void;
[[nodiscard]] static auto Parse(xmlpp::Element* element) -> Bind;
};
using SetBind = Set<Bind>;
using ResultBind = Result<Bind>;
} // namespace larra::xmpp::iq

View file

@ -11,16 +11,24 @@
#include <boost/asio/ssl.hpp> #include <boost/asio/ssl.hpp>
#include <boost/asio/use_awaitable.hpp> #include <boost/asio/use_awaitable.hpp>
#include <charconv> #include <charconv>
#include <larra/bind.hpp>
#include <larra/client/challenge_response.hpp> #include <larra/client/challenge_response.hpp>
#include <larra/client/options.hpp> #include <larra/client/options.hpp>
#include <larra/client/starttls_response.hpp> #include <larra/client/starttls_response.hpp>
#include <larra/client/xmpp_client_stream_features.hpp> #include <larra/client/xmpp_client_stream_features.hpp>
#include <larra/encryption.hpp> #include <larra/encryption.hpp>
#include <larra/features.hpp> #include <larra/features.hpp>
#include <larra/roster.hpp>
#include <larra/stream.hpp> #include <larra/stream.hpp>
#include <larra/user_account.hpp> #include <larra/user_account.hpp>
#include <larra/xml_stream.hpp> #include <larra/xml_stream.hpp>
#include <ranges> #include <ranges>
#include <utility>
namespace rng = std::ranges;
namespace views = std::views;
namespace iq = larra::xmpp::iq;
namespace larra::xmpp { namespace larra::xmpp {
constexpr auto kDefaultXmppPort = 5222; constexpr auto kDefaultXmppPort = 5222;
@ -29,12 +37,10 @@ constexpr auto kDefaultXmppPort = 5222;
namespace larra::xmpp::client { namespace larra::xmpp::client {
namespace rng = std::ranges;
namespace views = std::views;
template <typename Connection> template <typename Connection>
struct Client { struct Client {
constexpr Client(BareJid jid, XmlStream<Connection> connection) : jid(std::move(jid)), connection(std::move(connection)) {}; constexpr Client(BareJid jid, XmlStream<Connection> connection) : jid(std::move(jid)), connection(std::move(connection)) {
}
template <boost::asio::completion_token_for<void()> Token = boost::asio::use_awaitable_t<>> template <boost::asio::completion_token_for<void()> Token = boost::asio::use_awaitable_t<>>
constexpr auto Close(Token token = {}) { constexpr auto Close(Token token = {}) {
this->active = false; this->active = false;
@ -68,14 +74,49 @@ struct Client {
auto Send(const T& object) -> boost::asio::awaitable<void> { auto Send(const T& object) -> boost::asio::awaitable<void> {
co_await this->connection.Send(object); co_await this->connection.Send(object);
} }
[[nodiscard]] constexpr auto Jid() const -> const BareJid& { [[nodiscard]] constexpr auto Jid() const -> const FullJid& {
return this->jid; return this->jid;
} }
auto CreateResourceBind() -> boost::asio::awaitable<void> {
SPDLOG_INFO("Send IQ: Set::Bind");
co_await this->Send(::iq::SetBind{.id = "1", .payload = {}});
auto set_bind_response = co_await connection.template Read<Iq<::iq::Bind>>();
std::visit(utempl::Overloaded(
[](auto error) {
throw "Error response on IQ: Set::Bind: ''"; // TODO(unknown): Add exact error parsing
},
[&](::iq::ResultBind r) {
jid.resource = std::move(r.payload.jid->resource);
SPDLOG_INFO("Allocated resource: {}", jid.resource);
}),
set_bind_response);
co_return;
}
auto UpdateListOfContacts() -> boost::asio::awaitable<void> {

Maybe we can change namings later

Maybe we can change namings later
SPDLOG_INFO("Send IQ: Get::Roster");
co_await this->Send(::iq::GetRoster{.id = "1", .from = jid, .payload = {}});
const auto get_roster_response = co_await connection.template Read<Iq<::iq::Roster>>();
std::visit(utempl::Overloaded(
[](auto error) {
throw "Error response on IQ: Get::Roster: ''"; // TODO(unknown): Add exact error parsing
},
[&](::iq::ResultRoster r) {
roster = std::move(r.payload);
SPDLOG_INFO("New roster: {}", ToString(roster));
}),
get_roster_response);
co_return;
}
private: private:
bool active = true; bool active = true;
XmlStream<Connection> connection{}; XmlStream<Connection> connection{};
BareJid jid; FullJid jid;
::iq::Roster roster;
}; };
struct StartTlsNegotiationError : std::runtime_error { struct StartTlsNegotiationError : std::runtime_error {

View file

@ -1,7 +1,9 @@
#pragma once #pragma once
#include <larra/jid.hpp>
#include <larra/serialization.hpp> #include <larra/serialization.hpp>
#include <larra/stream_error.hpp> #include <larra/stream_error.hpp>
#include <larra/utils.hpp> #include <larra/utils.hpp>
#include <optional>
#include <string> #include <string>
namespace larra::xmpp { namespace larra::xmpp {
@ -11,6 +13,8 @@ namespace iq {
template <auto& Name, typename PayloadType> template <auto& Name, typename PayloadType>
struct BaseImplWithPayload { struct BaseImplWithPayload {
std::string id; std::string id;
Ivan-lis marked this conversation as resolved Outdated

Why std::string and not a more limited type like Jid ?

Why std::string and not a more limited type like Jid ?

Fixed

Fixed
std::optional<Jid> from{};
std::optional<Jid> to{};
PayloadType payload; PayloadType payload;
static const inline std::string kName = Name; static const inline std::string kName = Name;
static constexpr auto kDefaultName = "iq"; static constexpr auto kDefaultName = "iq";
@ -19,16 +23,45 @@ struct BaseImplWithPayload {
[[nodiscard]] constexpr auto Id(this Self&& self, std::string id) -> std::decay_t<Self> { [[nodiscard]] constexpr auto Id(this Self&& self, std::string id) -> std::decay_t<Self> {
return utils::FieldSetHelper::With<"id", BaseImplWithPayload>(std::forward<Self>(self), std::move(id)); return utils::FieldSetHelper::With<"id", BaseImplWithPayload>(std::forward<Self>(self), std::move(id));
} }
template <typename Self>
[[nodiscard]] constexpr auto To(this Self&& self, Jid to) -> std::decay_t<Self> {
Ivan-lis marked this conversation as resolved Outdated

Uses std::string for setters even though another type is stored.

Uses std::string for setters even though another type is stored.
return utils::FieldSetHelper::With<"to", BaseImplWithPayload>(std::forward<Self>(self), std::move(to));
}
template <typename Self>
[[nodiscard]] constexpr auto From(this Self&& self, Jid from) -> std::decay_t<Self> {
return utils::FieldSetHelper::With<"from", BaseImplWithPayload>(std::forward<Self>(self), std::move(from));
}
template <typename NewPayloadType, typename Self> template <typename NewPayloadType, typename Self>
[[nodiscard]] constexpr auto Payload(this Self&& self, NewPayloadType value) { [[nodiscard]] constexpr auto Payload(this Self&& self, NewPayloadType value) {
return utils::FieldSetHelper::With<"payload", BaseImplWithPayload, false>(std::forward<Self>(self), std::move(value)); return utils::FieldSetHelper::With<"payload", BaseImplWithPayload, false>(std::forward<Self>(self), std::move(value));
} }
friend constexpr auto operator<<(xmlpp::Element* element, const BaseImplWithPayload& self) { friend constexpr auto operator<<(xmlpp::Element* element, const BaseImplWithPayload& self) {
element->set_attribute("id", self.id); element->set_attribute("id", self.id);
if(self.to) {
element->set_attribute("to", ToString(*self.to));
}
if(self.from) {
element->set_attribute("from", ToString(*self.from));
}
element->set_attribute("type", kName); element->set_attribute("type", kName);
using S = Serialization<PayloadType>; using S = Serialization<PayloadType>;
S::Serialize(element->add_child_element(S::kDefaultName, S::kPrefix), self.payload); S::Serialize(element->add_child_element(S::kDefaultName, S::kPrefix), self.payload);
} }
[[nodiscard]] static constexpr auto TryParse(xmlpp::Element* element) -> std::optional<BaseImplWithPayload> {
return [&] -> std::optional<BaseImplWithPayload> {
auto node = element->get_attribute("type");
if(!node) {
return std::nullopt;
}
if(node->get_value() != Name) {
return std::nullopt;
}
return Parse(element);
}();
}
[[nodiscard]] static constexpr auto Parse(xmlpp::Element* element) -> BaseImplWithPayload { [[nodiscard]] static constexpr auto Parse(xmlpp::Element* element) -> BaseImplWithPayload {
auto node = element->get_attribute("type"); auto node = element->get_attribute("type");
if(!node) { if(!node) {
@ -41,6 +74,8 @@ struct BaseImplWithPayload {
if(!idNode) { if(!idNode) {
throw std::runtime_error("Not found attribute id for parse Iq"); throw std::runtime_error("Not found attribute id for parse Iq");
} }
auto from = element->get_attribute("from");
auto to = element->get_attribute("to");
using S = Serialization<PayloadType>; using S = Serialization<PayloadType>;
auto payload = element->get_first_child(S::kDefaultName); auto payload = element->get_first_child(S::kDefaultName);
@ -51,9 +86,13 @@ struct BaseImplWithPayload {
if(!payload2) { if(!payload2) {
throw std::runtime_error("Invalid payload for parse Iq"); throw std::runtime_error("Invalid payload for parse Iq");
} }
return {.id = idNode->get_value(), .payload = S::Parse(payload2)}; return {.id = idNode->get_value(),
.from = (from ? std::optional{Jid::Parse(from->get_value())} : std::nullopt),
Ivan-lis marked this conversation as resolved Outdated

Use BareJid::Parse when there might not be a BareJid

Use BareJid::Parse when there might not be a BareJid
.to = (to ? std::optional{Jid::Parse(to->get_value())} : std::nullopt),
.payload = S::Parse(payload2)};
} }
}; };
static constexpr auto kGetName = "get"; static constexpr auto kGetName = "get";
template <typename Payload> template <typename Payload>

View file

@ -7,24 +7,6 @@
namespace larra::xmpp { namespace larra::xmpp {
struct BareJid {
std::string username;
std::string server;
[[nodiscard]] static auto Parse(std::string_view jid) -> BareJid;
friend auto ToString(const BareJid& jid) -> std::string;
constexpr auto operator==(const BareJid&) const -> bool = default;
template <typename Self>
[[nodiscard]] constexpr auto Username(this Self&& self, std::string username) -> std::decay_t<Self> {
return utils::FieldSetHelper::With<"username", BareJid>(std::forward<Self>(self), std::move(username));
}
template <typename Self>
[[nodiscard]] constexpr auto Server(this Self&& self, std::string server) -> std::decay_t<Self> {
return utils::FieldSetHelper::With<"server", BareJid>(std::forward<Self>(self), std::move(server));
}
};
struct BareResourceJid { struct BareResourceJid {
std::string server; std::string server;
std::string resource; std::string resource;
@ -69,6 +51,28 @@ struct FullJid {
} }
}; };
struct BareJid {
std::string username;
std::string server;
constexpr operator FullJid(this auto&& self) {
return {.username = std::forward_like<decltype(self)>(self.username), .server = std::forward_like<decltype(self)>(self.server)};
Ivan-lis marked this conversation as resolved Outdated

Extra copying if there is an rvalue. Better to forward depending on the type with which it is called.

Extra copying if there is an rvalue. Better to forward depending on the type with which it is called.
}
[[nodiscard]] static auto Parse(std::string_view jid) -> BareJid;
friend auto ToString(const BareJid& jid) -> std::string;
constexpr auto operator==(const BareJid&) const -> bool = default;
template <typename Self>
[[nodiscard]] constexpr auto Username(this Self&& self, std::string username) -> std::decay_t<Self> {
return utils::FieldSetHelper::With<"username", BareJid>(std::forward<Self>(self), std::move(username));
}
template <typename Self>
[[nodiscard]] constexpr auto Server(this Self&& self, std::string server) -> std::decay_t<Self> {
return utils::FieldSetHelper::With<"server", BareJid>(std::forward<Self>(self), std::move(server));
}
};
using JidVariant = std::variant<BareJid, BareResourceJid, FullJid>; using JidVariant = std::variant<BareJid, BareResourceJid, FullJid>;
struct Jid : JidVariant { struct Jid : JidVariant {

View file

@ -0,0 +1,56 @@
#pragma once
#include <libxml++/libxml++.h>
#include <spdlog/spdlog.h>
#include <larra/iq.hpp>
#include <larra/jid.hpp>
#include <larra/utils.hpp>
#include <ranges>
#include <string>
#include <vector>
namespace larra::xmpp::iq {
struct RosterItem {
BareJid jid;
friend constexpr auto ToString(const RosterItem& item) {
return ToString(item.jid);
}
constexpr auto operator==(const RosterItem&) const -> bool = default;
friend auto operator<<(xmlpp::Element* element, const RosterItem& item) -> void;
[[nodiscard]] static auto Parse(xmlpp::Element* element) -> RosterItem;
};
struct Roster {
static constexpr auto kDefaultName = "query";
static constexpr auto kDefaultNamespace = "jabber:iq:roster";
std::vector<RosterItem> items;
friend auto ToString(const Roster& roster) -> std::string {
static constexpr std::string_view prefix = "Roster: [\n\t";
static constexpr std::string_view suffix = "]";
// \n\r\t
std::size_t total_length = std::ranges::fold_left(roster.items | std::views::transform([](const auto& el) {
return larra::xmpp::utils::AccumulateFieldLength(el.jid) + 3;
Ivan-lis marked this conversation as resolved Outdated

It would be better to move this to a .cpp file to make the header files lighter.

It would be better to move this to a .cpp file to make the header files lighter.
}),
prefix.length() + suffix.length(),
std::plus<>{});
std::string s;
s.resize(total_length);
sha512sum marked this conversation as resolved Outdated

Why not range based for?

Why not range based for?
s = prefix;
for(const auto& el : roster.items) {
s += ToString(el);
s += "\n\t";
}
return s += suffix;
}
friend auto operator<<(xmlpp::Element* element, const Roster& roster) -> void;
[[nodiscard]] static auto Parse(xmlpp::Element* element) -> Roster;
};
using GetRoster = Get<Roster>;
using ResultRoster = Result<Roster>;
Ivan-lis marked this conversation as resolved Outdated

You can create a structure that contains jid as an attribute and use automatic generation of serialization and deserialization

You can create a structure that contains jid as an attribute and use automatic generation of serialization and deserialization

Done

Done
} // namespace larra::xmpp::iq

View file

@ -115,7 +115,7 @@ struct Config : V {
requires(!HasParse<T>) requires(!HasParse<T>)
: V(AttributeConfig{}) { : V(AttributeConfig{}) {
} }
constexpr auto Base() const -> const V& { [[nodiscard]] constexpr auto Base() const -> const V& {
return static_cast<const V&>(*this); return static_cast<const V&>(*this);
} }
using type = T; using type = T;

View file

@ -73,6 +73,25 @@ struct ErrorImpl : BaseError {
} }
}; };
// Helper class to prevent parsing response stream into an expected return type if its name is a 'stream:error'
struct UnknownXmppError : BaseError {
static constexpr auto kDefaultName = "stream:error";
static constexpr std::string_view kErrorMessage = "Unknown XMPP stream error";
static constexpr auto TryParse(xmlpp::Element* element) {
return std::optional{UnknownXmppError{}};
}
static constexpr auto Parse(xmlpp::Element* element) {
return TryParse(element).value();
}
friend constexpr auto operator<<(xmlpp::Element* element, const UnknownXmppError& obj) -> void {
throw std::format("'{}' must never be written into the real stream!", kErrorMessage);
}
[[nodiscard]] constexpr auto what() const noexcept -> const char* override {
return kErrorMessage.data();
}
};
struct BadFormat : ErrorImpl<BadFormat> {}; struct BadFormat : ErrorImpl<BadFormat> {};
struct BadNamespacePrefix : ErrorImpl<BadNamespacePrefix> {}; struct BadNamespacePrefix : ErrorImpl<BadNamespacePrefix> {};
struct Conflict : ErrorImpl<Conflict> {}; struct Conflict : ErrorImpl<Conflict> {};
@ -81,7 +100,7 @@ struct HostGone : ErrorImpl<HostGone> {};
struct HostUnknown : ErrorImpl<HostUnknown> {}; struct HostUnknown : ErrorImpl<HostUnknown> {};
struct ImproperAdressing : ErrorImpl<ImproperAdressing> {}; struct ImproperAdressing : ErrorImpl<ImproperAdressing> {};
struct InternalServerError : ErrorImpl<InternalServerError> {}; struct InternalServerError : ErrorImpl<InternalServerError> {};
struct InvalidForm : ErrorImpl<InvalidForm> {}; struct InvalidFrom : ErrorImpl<InvalidFrom> {};
struct InvalidNamespace : ErrorImpl<InvalidNamespace> {}; struct InvalidNamespace : ErrorImpl<InvalidNamespace> {};
struct InvalidXml : ErrorImpl<InvalidXml> {}; struct InvalidXml : ErrorImpl<InvalidXml> {};
struct NotAuthorized : ErrorImpl<NotAuthorized> {}; struct NotAuthorized : ErrorImpl<NotAuthorized> {};
@ -109,7 +128,7 @@ using StreamError = std::variant<error::stream::BadFormat,
error::stream::HostUnknown, error::stream::HostUnknown,
error::stream::ImproperAdressing, error::stream::ImproperAdressing,
error::stream::InternalServerError, error::stream::InternalServerError,
error::stream::InvalidForm, error::stream::InvalidFrom,
error::stream::InvalidNamespace, error::stream::InvalidNamespace,
error::stream::InvalidXml, error::stream::InvalidXml,
error::stream::NotAuthorized, error::stream::NotAuthorized,
@ -123,6 +142,10 @@ using StreamError = std::variant<error::stream::BadFormat,
error::stream::UnsupportedEncoding, error::stream::UnsupportedEncoding,
error::stream::UnsupportedFeature, error::stream::UnsupportedFeature,
error::stream::UnsupportedStanzaType, error::stream::UnsupportedStanzaType,
error::stream::UnsupportedVersion>; error::stream::UnsupportedVersion,
error::stream::UnknownXmppError>;
static_assert(!std::is_same_v<typename std::variant_alternative_t<std::variant_size_v<StreamError> - 1, StreamError>, StreamError>,
"'UnknownXmppError' must be at the end of 'StreamError' variant");
} // namespace larra::xmpp } // namespace larra::xmpp

View file

@ -295,4 +295,18 @@ struct RangeToWrapper : T {
: T{std::forward<Args>(args)...} {}; : T{std::forward<Args>(args)...} {};
}; };
template <typename T>
concept LengthCalculatable = requires(const T& obj) {
{ obj.length() } -> std::convertible_to<std::size_t>;
sha512sum marked this conversation as resolved Outdated

A comment that doesn't explain anything and just repeats the code.

A comment that doesn't explain anything and just repeats the code.
} || std::convertible_to<T, std::string>;
template <typename T>
auto AccumulateFieldLength(const T& obj) -> std::size_t {
std::size_t totalLength = 0;
sha512sum marked this conversation as resolved Outdated

camelCase

camelCase
boost::pfr::for_each_field(obj, [&](const LengthCalculatable auto& field) {
totalLength += field.length(); // Accumulate length of each field
});
return totalLength;
}
} // namespace larra::xmpp::utils } // namespace larra::xmpp::utils

31
library/src/bind.cpp Normal file
View file

@ -0,0 +1,31 @@
#include <larra/bind.hpp>
namespace larra::xmpp::iq {
auto operator<<(xmlpp::Element* element, const Bind& bind) -> void {
element->set_attribute("xmlns", Bind::kDefaultNamespace);
if(bind.jid) {
auto* jid_el = element->add_child_element("jid");
jid_el->add_child_text(ToString(*bind.jid));
}
}
[[nodiscard]] auto Bind::Parse(xmlpp::Element* element) -> Bind {
const auto* jid_node = element->get_first_child("jid");
if(!jid_node) {
SPDLOG_DEBUG("No Jid Node at Iq::Bind");
return {};
}
auto* jid_el = dynamic_cast<const xmlpp::Element*>(jid_node);
if(!jid_el) {
throw std::runtime_error("dynamic_cast to const xmlpp::Element* failed");
}
const auto* text = jid_el->get_first_child_text();
if(!jid_el) {
throw std::runtime_error("No text at Iq::Bind jid child");
}
return {.jid = (jid_node ? std::optional{FullJid::Parse(text->get_content())} : std::nullopt)};
}
} // namespace larra::xmpp::iq

30
library/src/roster.cpp Normal file
View file

@ -0,0 +1,30 @@
#include <larra/roster.hpp>
#include <larra/serialization/auto.hpp>
namespace larra::xmpp::serialization {
namespace iq = larra::xmpp::iq;
template <>
constexpr auto kSerializationConfig<iq::RosterItem> = SerializationConfig<iq::RosterItem>{};
template <>
constexpr auto kSerializationConfig<iq::Roster> = SerializationConfig<iq::Roster>{}.With<"items">({Config<std::vector<iq::RosterItem>>{}});
} // namespace larra::xmpp::serialization
namespace larra::xmpp::iq {
namespace S = larra::xmpp::serialization;
auto operator<<(xmlpp::Element* element, const RosterItem& self) -> void {
S::Serialize(element, self);
}
auto RosterItem::Parse(xmlpp::Element* element) -> RosterItem {
return S::Parse<RosterItem>(element);
}
auto operator<<(xmlpp::Element* element, const Roster& self) -> void {
element->set_attribute("xmlns", Roster::kDefaultNamespace);
S::Serialize(element, self);
}
auto Roster::Parse(xmlpp::Element* element) -> Roster {
return S::Parse<Roster>(element);
}
} // namespace larra::xmpp::iq

36
tests/roster.cpp Normal file
View file

@ -0,0 +1,36 @@
#include <gtest/gtest.h>
#include <larra/jid.hpp>
#include <larra/roster.hpp>
namespace larra::xmpp {
TEST(Roster, SerializeAndParse) {
FullJid jid{.username = "test", .server = "server", .resource = "res"}; // NOLINT
auto roster = iq::GetRoster{.id = "1", .from = jid, .payload = iq::Roster{.items = {{"u1", "s1"}, {"u2", "s2"}, {"u3", "s3"}}}};
xmlpp::Document doc;
auto node = doc.create_root_node("iq");
Ivan-lis marked this conversation as resolved Outdated

Why not just throw all the necessary elements through the vector constructor in Roster constructor?

Why not just throw all the necessary elements through the vector constructor in Roster constructor?
node << roster;
auto parseRes = decltype(roster)::Parse(node);
ASSERT_EQ(roster.payload.items.size(), parseRes.payload.items.size());
for(const auto& [idx, expectEl, parsedEl] : std::views::zip(std::views::iota(0), roster.payload.items, parseRes.payload.items)) {
sha512sum marked this conversation as resolved Outdated

camelCase

camelCase
EXPECT_EQ(expectEl, parsedEl) << "Mismatched on idx: " << idx;
}
}
static constexpr std::string_view kRosterPrintExpectedData = "Roster: [\n\tu1@s1\n\tu2@s2\n\tu3@s3\n\t]";
TEST(Roster, Print) {
Ivan-lis marked this conversation as resolved Outdated

Code left in the comment

Code left in the comment
FullJid jid{.username = "test", .server = "server", .resource = "res"}; // NOLINT
auto roster = iq::GetRoster{.id = "1", .from = jid, .payload = iq::Roster{.items = {{"u1", "s1"}, {"u2", "s2"}, {"u3", "s3"}}}};
EXPECT_NO_THROW({
auto rosterStr = ToString(roster.payload);
EXPECT_EQ(kRosterPrintExpectedData.length(), rosterStr.capacity());
EXPECT_EQ(kRosterPrintExpectedData, rosterStr);
});
}
} // namespace larra::xmpp
Ivan-lis marked this conversation as resolved Outdated

EXPECT_EQ and test content

EXPECT_EQ and test content