Feature: Added proper IQ Stanza error handling #21

Merged
sha512sum merged 5 commits from feature/handling_stanza_errors into main 2024-12-24 06:11:48 +00:00
14 changed files with 477 additions and 65 deletions

View file

@ -134,6 +134,8 @@ jobs:
-GNinja -DCMAKE_BUILD_TYPE=Release \ -GNinja -DCMAKE_BUILD_TYPE=Release \
-DENABLE_EXAMPLES=ON \ -DENABLE_EXAMPLES=ON \
-DENABLE_TESTS=ON \ -DENABLE_TESTS=ON \
-DMAX_LOG_LEVEL=0 \
-DTEST_MAX_LOG_LEVEL=0 \
-DCMAKE_CXX_FLAGS="-ftemplate-backtrace-limit=0" -DCMAKE_CXX_FLAGS="-ftemplate-backtrace-limit=0"
cmake --build ${{ github.workspace }}/build_gcc --parallel `nproc` cmake --build ${{ github.workspace }}/build_gcc --parallel `nproc`
@ -142,7 +144,7 @@ jobs:
continue-on-error: true continue-on-error: true
run: | run: |
cd ${{ github.workspace }}/build_gcc cd ${{ github.workspace }}/build_gcc
./larra_xmpp_tests ./larra_xmpp_tests --log_level=0
- name: Clang build (only configuring) - name: Clang build (only configuring)
id: clang_build id: clang_build
@ -160,6 +162,8 @@ jobs:
-GNinja -DCMAKE_BUILD_TYPE=Release \ -GNinja -DCMAKE_BUILD_TYPE=Release \
-DENABLE_EXAMPLES=ON \ -DENABLE_EXAMPLES=ON \
-DENABLE_TESTS=ON \ -DENABLE_TESTS=ON \
-DMAX_LOG_LEVEL=0 \
-DTEST_MAX_LOG_LEVEL=0 \
-DCMAKE_CXX_FLAGS="-stdlib=libc++ -I/home/LLVM-${LLVM_VER}/include/c++/v1 -fno-modules" -DCMAKE_CXX_FLAGS="-stdlib=libc++ -I/home/LLVM-${LLVM_VER}/include/c++/v1 -fno-modules"
echo "::group::compile_commands.json content" echo "::group::compile_commands.json content"
@ -200,4 +204,4 @@ jobs:
# export LLVM_VER=19.1.3 # export LLVM_VER=19.1.3
# export PATH="/home/LLVM-${LLVM_VER}/bin:${PATH}" # export PATH="/home/LLVM-${LLVM_VER}/bin:${PATH}"
# cd ${{ github.workspace }}/build_clang # 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 # 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

6
.vscode/launch.json vendored
View file

@ -9,7 +9,10 @@
"request": "launch", "request": "launch",
"name": "Debug: connect", "name": "Debug: connect",
"program": "${workspaceFolder}/build/examples/output/connect", "program": "${workspaceFolder}/build/examples/output/connect",
"args": [], "args": [
"--log_level=0"
// --gtest_filter=POSTIVE_PATTERNS[-NEGATIVE_PATTERNS]
],
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"preLaunchTask": "GCC: Build" "preLaunchTask": "GCC: Build"
}, },
@ -19,6 +22,7 @@
"name": "Debug: tests", "name": "Debug: tests",
"program": "${workspaceFolder}/build/larra_xmpp_tests", "program": "${workspaceFolder}/build/larra_xmpp_tests",
"args": [ "args": [
"--log_level=0"
// --gtest_filter=POSTIVE_PATTERNS[-NEGATIVE_PATTERNS] // --gtest_filter=POSTIVE_PATTERNS[-NEGATIVE_PATTERNS]
// "--gtest_filter=Roster*" // "--gtest_filter=Roster*"
], ],

13
.vscode/tasks.json vendored
View file

@ -79,8 +79,8 @@
"command": [ "command": [
"cd ${workspaceFolder} &&", "cd ${workspaceFolder} &&",
"mkdir -p build && cd build &&", "mkdir -p build && cd build &&",
"cmake cmake -Wno-dev ", "cmake -Wno-dev ",
" -DCMAKE_BUILD_TYPE=Debug -DENABLE_EXAMPLES=ON -DENABLE_TESTS=ON .." " -DCMAKE_BUILD_TYPE=Debug -DENABLE_EXAMPLES=ON -DENABLE_TESTS=ON -DMAX_LOG_LEVEL=0 -DTEST_MAX_LOG_LEVEL=0 .."
], ],
"options": { "options": {
"env": { "env": {
@ -138,6 +138,7 @@
"mkdir -p build_clang && cd build_clang &&", "mkdir -p build_clang && cd build_clang &&",
"cmake -Wno-dev ", "cmake -Wno-dev ",
" -DCMAKE_BUILD_TYPE=Debug -DENABLE_EXAMPLES=ON -DENABLE_TESTS=ON ", " -DCMAKE_BUILD_TYPE=Debug -DENABLE_EXAMPLES=ON -DENABLE_TESTS=ON ",
" -DMAX_LOG_LEVEL=0 -DTEST_MAX_LOG_LEVEL=0",
" -DCMAKE_CXX_FLAGS=\"-stdlib=libstdc++\"", " -DCMAKE_CXX_FLAGS=\"-stdlib=libstdc++\"",
" -DCMAKE_EXE_LINKER_FLAGS=\"-L/usr/lib/x86_64-linux-gnu -lstdc++\" .." " -DCMAKE_EXE_LINKER_FLAGS=\"-L/usr/lib/x86_64-linux-gnu -lstdc++\" .."
], ],
@ -160,6 +161,7 @@
"mkdir -p build_clang && cd build_clang &&", "mkdir -p build_clang && cd build_clang &&",
"cmake -Wno-dev ", "cmake -Wno-dev ",
" -DCMAKE_BUILD_TYPE=Debug -DENABLE_EXAMPLES=ON -DENABLE_TESTS=ON", " -DCMAKE_BUILD_TYPE=Debug -DENABLE_EXAMPLES=ON -DENABLE_TESTS=ON",
" -DMAX_LOG_LEVEL=0 -DTEST_MAX_LOG_LEVEL=0",
// //
// Uncomment for GCC standart library: libstdc++ // Uncomment for GCC standart library: libstdc++
//" -DCMAKE_CXX_FLAGS=\"-stdlib=libstdc++ -fno-omit-frame-pointer -g -fsanitize=address,undefined,leak,function,nullability,vptr\"", //" -DCMAKE_CXX_FLAGS=\"-stdlib=libstdc++ -fno-omit-frame-pointer -g -fsanitize=address,undefined,leak,function,nullability,vptr\"",
@ -191,7 +193,7 @@
"command": [ "command": [
"cd ${workspaceFolder} &&", "cd ${workspaceFolder} &&",
"mkdir -p build_clang && cd build_clang &&", "mkdir -p build_clang && cd 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 ; echo \"exit code: $?\"", "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 ; echo \"exit code: $?\"",
], ],
"presentation": { "presentation": {
"clear": true "clear": true
@ -211,6 +213,7 @@
"mkdir -p build_clang && cd build_clang &&", "mkdir -p build_clang && cd build_clang &&",
"cmake -Wno-dev ", "cmake -Wno-dev ",
" -DCMAKE_BUILD_TYPE=Debug -DENABLE_EXAMPLES=ON -DENABLE_TESTS=ON ", " -DCMAKE_BUILD_TYPE=Debug -DENABLE_EXAMPLES=ON -DENABLE_TESTS=ON ",
" -DMAX_LOG_LEVEL=0 -DTEST_MAX_LOG_LEVEL=0",
" -DCMAKE_CXX_FLAGS=\"-stdlib=libstdc++ -fsanitize-memory-track-origins=2 -fno-omit-frame-pointer -fno-optimize-sibling-calls -g -fsanitize=memory\"", " -DCMAKE_CXX_FLAGS=\"-stdlib=libstdc++ -fsanitize-memory-track-origins=2 -fno-omit-frame-pointer -fno-optimize-sibling-calls -g -fsanitize=memory\"",
" -DCMAKE_EXE_LINKER_FLAGS=\"-L/usr/lib/x86_64-linux-gnu -lstdc++ -fsanitize-memory-track-origins=2 -fno-omit-frame-pointer -fno-optimize-sibling-calls -g -fsanitize=memory\" ..", " -DCMAKE_EXE_LINKER_FLAGS=\"-L/usr/lib/x86_64-linux-gnu -lstdc++ -fsanitize-memory-track-origins=2 -fno-omit-frame-pointer -fno-optimize-sibling-calls -g -fsanitize=memory\" ..",
], ],
@ -236,7 +239,7 @@
"command": [ "command": [
"cd ${workspaceFolder} &&", "cd ${workspaceFolder} &&",
"mkdir -p build_clang && cd build_clang &&", "mkdir -p build_clang && cd build_clang &&",
"MSAN_SYMBOLIZER_PATH=llvm-symbolizer MSAN_OPTIONS=abort_on_error=1 ./larra_xmpp_tests ; echo \"exit code: $?\"", "MSAN_SYMBOLIZER_PATH=llvm-symbolizer MSAN_OPTIONS=abort_on_error=1 ./larra_xmpp_tests --log_level=0 ; echo \"exit code: $?\"",
], ],
"presentation": { "presentation": {
"clear": true "clear": true
@ -270,7 +273,7 @@
"command": [ "command": [
"cd ${workspaceFolder} &&", "cd ${workspaceFolder} &&",
"mkdir -p build_clang && cd build_clang &&", "mkdir -p build_clang && cd build_clang &&",
"MSAN_SYMBOLIZER_PATH=llvm-symbolizer MSAN_OPTIONS=abort_on_error=1 ./larra_xmpp_tests ; echo \"exit code: $?\"", "MSAN_SYMBOLIZER_PATH=llvm-symbolizer MSAN_OPTIONS=abort_on_error=1 ./larra_xmpp_tests --log_level=0 ; echo \"exit code: $?\"",
], ],
"presentation": { "presentation": {
"clear": true "clear": true

View file

@ -18,10 +18,12 @@ set(CMAKE_CXX_EXTENSIONS OFF)
set(FMT_MODULE OFF) set(FMT_MODULE OFF)
set(UTEMPL_MODULE OFF) set(UTEMPL_MODULE OFF)
set(CXX_EXTENSIONS NO) set(CXX_EXTENSIONS NO)
set(BOOST_INCLUDE_LIBRARIES "pfr;asio;serialization") set(BOOST_INCLUDE_LIBRARIES "pfr;asio;serialization;program_options")
option(CPM_USE_LOCAL_PACKAGES "Use local packages" ON) option(CPM_USE_LOCAL_PACKAGES "Use local packages" ON)
option(UTEMPL_USE_LOCAL_PACKAGE "Use utempl local package" OFF) option(UTEMPL_USE_LOCAL_PACKAGE "Use utempl local package" OFF)
option(BUILD_EXECUTABLE ON) option(BUILD_EXECUTABLE ON)
set(MAX_LOG_LEVEL 2 CACHE STRING "Available log levels: 0=TRACE, 1=DEBUG, 2=INFO, 3=WARN, 4=ERROR, 5=CRITICAL, 6=OFF")
set(TEST_MAX_LOG_LEVEL 2 CACHE STRING "Available log levels: 0=TRACE, 1=DEBUG, 2=INFO, 3=WARN, 4=ERROR, 5=CRITICAL, 6=OFF")
set(UTEMPL_URL set(UTEMPL_URL
"https://sha512sum.xyz/git/sha512sum/utempl" "https://sha512sum.xyz/git/sha512sum/utempl"
CACHE STRING "utempl repository URL") CACHE STRING "utempl repository URL")
@ -175,11 +177,11 @@ 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 Boost::asio Boost::serialization Boost::program_options utempl::utempl
OpenSSL::SSL nameof::nameof fmt::fmt OpenSSL::SSL nameof::nameof fmt::fmt
OpenSSL::Crypto spdlog xmlplusplus ${LIBXML2_LIBRARIES}) OpenSSL::Crypto spdlog xmlplusplus ${LIBXML2_LIBRARIES})
else() else()
find_package(Boost 1.85.0 COMPONENTS serialization REQUIRED) find_package(Boost 1.85.0 COMPONENTS serialization program_options REQUIRED)
target_link_libraries(larra_xmpp PUBLIC target_link_libraries(larra_xmpp PUBLIC
utempl::utempl ${Boost_LIBRARIES} OpenSSL::SSL utempl::utempl ${Boost_LIBRARIES} OpenSSL::SSL
nameof::nameof fmt::fmt nameof::nameof fmt::fmt
@ -259,6 +261,7 @@ if(ENABLE_TESTS)
target_sources(larra_xmpp_tests PUBLIC ${SOURCES}) target_sources(larra_xmpp_tests PUBLIC ${SOURCES})
target_link_libraries(larra_xmpp_tests GTest::gtest_main target_link_libraries(larra_xmpp_tests GTest::gtest_main
larra_xmpp) larra_xmpp)
target_compile_definitions(larra_xmpp_tests PRIVATE SPDLOG_ACTIVE_LEVEL=${TEST_MAX_LOG_LEVEL})
set_property(TARGET larra_xmpp_tests PROPERTY CXX_STANDARD 23) set_property(TARGET larra_xmpp_tests PROPERTY CXX_STANDARD 23)
include(GoogleTest) include(GoogleTest)
gtest_discover_tests(larra_xmpp_tests) gtest_discover_tests(larra_xmpp_tests)
@ -270,6 +273,7 @@ if(ENABLE_EXAMPLES)
get_filename_component(EXAMPLE_NAME ${EXAMPLE_SRC} NAME_WE) get_filename_component(EXAMPLE_NAME ${EXAMPLE_SRC} NAME_WE)
add_executable(${EXAMPLE_NAME} ${EXAMPLE_SRC}) add_executable(${EXAMPLE_NAME} ${EXAMPLE_SRC})
target_link_libraries(${EXAMPLE_NAME} larra_xmpp) target_link_libraries(${EXAMPLE_NAME} larra_xmpp)
target_compile_definitions(${EXAMPLE_NAME} PRIVATE SPDLOG_ACTIVE_LEVEL=${TEST_MAX_LOG_LEVEL})
set_property(TARGET ${EXAMPLE_NAME} PROPERTY CXX_STANDARD 23) set_property(TARGET ${EXAMPLE_NAME} PROPERTY CXX_STANDARD 23)
set_target_properties(${EXAMPLE_NAME} PROPERTIES set_target_properties(${EXAMPLE_NAME} PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/examples/output") RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/examples/output")

View file

@ -3,11 +3,28 @@
#include <boost/asio/co_spawn.hpp> #include <boost/asio/co_spawn.hpp>
#include <boost/asio/detached.hpp> #include <boost/asio/detached.hpp>
#include <boost/program_options.hpp>
#include <larra/client/client.hpp> #include <larra/client/client.hpp>
#include <larra/presence.hpp> #include <larra/presence.hpp>
#include <larra/printer_stream.hpp> #include <larra/printer_stream.hpp>
#include <print> #include <print>
// clang-format off
constexpr auto ToString(spdlog::level::level_enum e) {
switch (e) {
case spdlog::level::trace: return "TRACE";
case spdlog::level::debug: return "DEBUG";
case spdlog::level::info: return "INFO";
case spdlog::level::warn: return "WARNING";
case spdlog::level::err: return "ERROR";
case spdlog::level::critical: return "CRITICAL";
case spdlog::level::off: return "OFF";
default:
return "INVALID";
}
}
// clang-format on
namespace iq = larra::xmpp::iq; namespace iq = larra::xmpp::iq;
auto Coroutine() -> boost::asio::awaitable<void> { auto Coroutine() -> boost::asio::awaitable<void> {
@ -50,8 +67,39 @@ auto Coroutine() -> boost::asio::awaitable<void> {
SPDLOG_INFO("Done connecting client!"); SPDLOG_INFO("Done connecting client!");
} }
auto main() -> int { namespace po = boost::program_options;
spdlog::set_level(spdlog::level::trace);
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<int>()->default_value(SPDLOG_LEVEL_INFO),
"Set log level: 0=TRACE, 1=DEBUG, 2=INFO, 3=WARN, 4=ERROR, 5=CRITICAL, 6=OFF");
// Parse command-line arguments
po::variables_map vm;
try {
po::store(po::parse_command_line(argc, argv, desc), vm);
po::notify(vm);
if(vm["log_level"].as<int>() < spdlog::level::level_enum::trace || vm["log_level"].as<int>() > spdlog::level::level_enum::off) {
throw std::invalid_argument{
std::format("Invalid argument value for '--log_level' option. Check option description for more details")};
}
if(vm["log_level"].as<int>() < SPDLOG_ACTIVE_LEVEL) {
SPDLOG_WARN("Specified log_level '{}' is lower than max available one '{}'. Log level will be changed according to the maximum one",
vm["log_level"].as<int>(),
SPDLOG_ACTIVE_LEVEL);
}
} catch(const std::exception& e) {
SPDLOG_CRITICAL("Cmd parse error: {}", e.what());
return 1;
}
// Cmd options handling
spdlog::set_level(static_cast<spdlog::level::level_enum>(vm["log_level"].as<int>()));
std::println("\nEnvironment setup:\n\tCurrently set log level: {}\n", ToString(spdlog::get_level()));
boost::asio::io_context io_context; boost::asio::io_context io_context;
boost::asio::co_spawn(io_context, Coroutine(), boost::asio::detached); boost::asio::co_spawn(io_context, Coroutine(), boost::asio::detached);
io_context.run(); io_context.run();

View file

@ -84,14 +84,12 @@ struct Client {
auto set_bind_response = co_await connection.template Read<Iq<::iq::Bind>>(); auto set_bind_response = co_await connection.template Read<Iq<::iq::Bind>>();
std::visit(utempl::Overloaded( std::visit(utempl::Overloaded(
[](auto error) {
throw "Error response on IQ: Set::Bind: ''"; // TODO(unknown): Add exact error parsing
},
[&](::iq::ResultBind r) { [&](::iq::ResultBind r) {
jid.resource = std::move(r.payload.jid->resource); jid.resource = std::move(r.payload.jid->resource);
SPDLOG_INFO("Allocated resource: {}", jid.resource); SPDLOG_INFO("Allocated resource: {}", jid.resource);
}), },
set_bind_response); IqErrThrowVisitor{"Error response on IQ: Set::Bind"}),
std::move(set_bind_response));
co_return; co_return;
} }
@ -101,14 +99,12 @@ struct Client {
const auto get_roster_response = co_await connection.template Read<Iq<::iq::Roster>>(); const auto get_roster_response = co_await connection.template Read<Iq<::iq::Roster>>();
std::visit(utempl::Overloaded( std::visit(utempl::Overloaded(
[](auto error) {
throw "Error response on IQ: Get::Roster: ''"; // TODO(unknown): Add exact error parsing
},
[&](::iq::ResultRoster r) { [&](::iq::ResultRoster r) {
roster = std::move(r.payload); roster = std::move(r.payload);
SPDLOG_INFO("New roster: {}", ToString(roster)); SPDLOG_INFO("New roster: {}", ToString(roster));
}), },
get_roster_response); IqErrThrowVisitor{"Error response on IQ: Get::Roster"}),
std::move(get_roster_response));
co_return; co_return;
} }

View file

@ -1,9 +1,12 @@
#pragma once #pragma once
#include <spdlog/spdlog.h>
#include <larra/jid.hpp> #include <larra/jid.hpp>
#include <larra/serialization.hpp> #include <larra/serialization.hpp>
#include <larra/stream_error.hpp> #include <larra/stanza_error.hpp>
#include <larra/utils.hpp> #include <larra/utils.hpp>
#include <optional> #include <optional>
#include <stdexcept>
#include <string> #include <string>
namespace larra::xmpp { namespace larra::xmpp {
@ -115,7 +118,41 @@ using Error = BaseImplWithPayload<kErrorName, Payload>;
} // namespace iq } // namespace iq
using IqError = iq::Error<stanza::error::StanzaError>;
template <typename Payload> template <typename Payload>
using Iq = std::variant<iq::Get<Payload>, iq::Set<Payload>, iq::Result<Payload>, iq::Error<Payload>>; using Iq = std::variant<iq::Get<Payload>, iq::Set<Payload>, iq::Result<Payload>, IqError>;
struct IqErrThrowVisitor {
Ivan-lis marked this conversation as resolved Outdated

Maybe it would be better to move the templates from here to the () operators?

Maybe it would be better to move the templates from here to the () operators?
constexpr IqErrThrowVisitor(std::string_view baseErrorMsg) : baseErrorMsg(baseErrorMsg) {
}
template <typename Payload>
void operator()(const iq::Get<Payload>&) {
static constexpr std::string_view getErrorMsg = ": 'Get' is an invalid type for IQ result. Expected 'Result' or 'Error'";
auto finalErrorMsg = std::string(baseErrorMsg).append(getErrorMsg);
SPDLOG_ERROR(finalErrorMsg);
throw std::runtime_error{finalErrorMsg};
}
template <typename Payload>
void operator()(const iq::Set<Payload>&) {
static constexpr std::string_view getErrorMsg = ": 'Set' is an invalid type for IQ result. Expected 'Result' or 'Error'";
auto finalErrorMsg = std::string(baseErrorMsg).append(getErrorMsg);
SPDLOG_ERROR(finalErrorMsg);
throw std::runtime_error{finalErrorMsg};
}
void operator()(IqError err) {
SPDLOG_ERROR(baseErrorMsg);
std::visit(
[](auto exception) {
throw exception;
},
std::move(err.payload));
}
std::string_view baseErrorMsg;
};
} // namespace larra::xmpp } // namespace larra::xmpp

View file

@ -0,0 +1,148 @@
#pragma once
#include <libxml++/libxml++.h>
#include <larra/utils.hpp>
#include <optional>
#include <variant>
namespace larra::xmpp::stanza::error {
struct StanzaBaseError : std::exception {};
// DO NOT MOVE TO ANOTHER NAMESPACE(where no heirs). VIA friend A FUNCTION IS ADDED THAT VIA ADL WILL BE SEARCHED FOR HEIRS
// C++20 modules very unstable in clangd :(
template <typename T>
struct StanzaErrorImpl : StanzaBaseError {
static constexpr auto kDefaultName = "error";
static constexpr auto kDefaultNamespace = "urn:ietf:params:xml:ns:xmpp-stanzas";
static inline const auto kKebabCaseName = static_cast<std::string>(utils::ToKebabCaseName<T>());
static constexpr auto kErrorMessage = [] -> std::string_view {
static constexpr auto name = nameof::nameof_short_type<T>();
static constexpr auto str = [] {
return std::array{std::string_view{"Stanza IQ Error: "}, std::string_view{name}, std::string_view{"\0", 1}} | std::views::join;
};
static constexpr auto array = str() | std::ranges::to<utils::RangeToWrapper<std::array<char, std::ranges::distance(str())>>>();
return {array.data(), array.size() - 1};
}();
std::optional<std::string> by{};
std::string type;
// TODO(unknown): Add "optional text children" support for stanza error. Check "XML Stanzas" -> "Syntax" for more details
static constexpr auto TryParse(xmlpp::Element* element) -> std::optional<T> {
if(not element) {
return std::nullopt;
}
auto by = element->get_attribute("by");
auto type = element->get_attribute("type");
if(not type) {
return std::nullopt;
}
auto node = element->get_first_child(kKebabCaseName);
if(not node) {
return std::nullopt;
}
T obj;
obj.type = type->get_value();
if(by) {
obj.by = std::optional{by->get_value()};
}
return obj;
}
static constexpr auto Parse(xmlpp::Element* element) -> T {
return TryParse(element).value();
}
friend constexpr auto operator<<(xmlpp::Element* element, const T& obj) -> void {
element->set_attribute("type", obj.type);
if(obj.by) {
element->set_attribute("by", *obj.by);
}
auto node = element->add_child_element(kKebabCaseName);
node->set_namespace_declaration(kDefaultNamespace);
}
constexpr auto operator==(const StanzaErrorImpl<T>&) const -> bool {
return true;
};
[[nodiscard]] constexpr auto what() const noexcept -> const char* override {
return kErrorMessage.data();
}
};
// Helper class to prevent parsing response stream into an expected return type if its name is an 'error'
struct UnknownStanzaError : StanzaBaseError {
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{UnknownStanzaError{}};
}
static constexpr auto Parse(xmlpp::Element* element) {
return TryParse(element).value();
}
friend constexpr auto operator<<(xmlpp::Element* element, const UnknownStanzaError& obj) -> void {
throw std::runtime_error{std::format("'{}' must never be written into the real stream!", kErrorMessage)};
}
constexpr auto operator==(const UnknownStanzaError&) const -> bool {
return true;
};
[[nodiscard]] constexpr auto what() const noexcept -> const char* override {
return kErrorMessage.data();
}
};
struct BadRequest : StanzaErrorImpl<BadRequest> {};
struct Conflict : StanzaErrorImpl<Conflict> {};
struct FeatureNotImplemented : StanzaErrorImpl<FeatureNotImplemented> {};
struct Forbidden : StanzaErrorImpl<Forbidden> {};
struct Gone : StanzaErrorImpl<Gone> {};
struct InternalServerError : StanzaErrorImpl<InternalServerError> {};
struct ItemNotFound : StanzaErrorImpl<ItemNotFound> {};
struct JidMalformed : StanzaErrorImpl<JidMalformed> {};
struct NotAcceptable : StanzaErrorImpl<NotAcceptable> {};
struct NotAllowed : StanzaErrorImpl<NotAllowed> {};
struct NotAuthorized : StanzaErrorImpl<NotAuthorized> {};
struct PolicyViolation : StanzaErrorImpl<PolicyViolation> {};
struct RecipientUnavailable : StanzaErrorImpl<RecipientUnavailable> {};
struct Redirect : StanzaErrorImpl<Redirect> {};
struct RegistrationRequired : StanzaErrorImpl<RegistrationRequired> {};
struct RemoteServerNotFound : StanzaErrorImpl<RemoteServerNotFound> {};
struct RemoteServerTimeout : StanzaErrorImpl<RemoteServerTimeout> {};
struct ResourceConstraint : StanzaErrorImpl<ResourceConstraint> {};
struct ServiceUnavailable : StanzaErrorImpl<ServiceUnavailable> {};
struct SubscriptionRequired : StanzaErrorImpl<SubscriptionRequired> {};
struct UndefinedCondition : StanzaErrorImpl<UndefinedCondition> {};
struct UnexpectedRequest : StanzaErrorImpl<UnexpectedRequest> {};
using StanzaError = std::variant<BadRequest,
Conflict,
FeatureNotImplemented,
Forbidden,
Gone,
InternalServerError,
ItemNotFound,
JidMalformed,
NotAcceptable,
NotAllowed,
NotAuthorized,
PolicyViolation,
RecipientUnavailable,
Redirect,
RegistrationRequired,
RemoteServerNotFound,
RemoteServerTimeout,
ResourceConstraint,
ServiceUnavailable,
SubscriptionRequired,
UndefinedCondition,
UnexpectedRequest,
UnknownStanzaError>;
static_assert(std::is_same_v<typename std::variant_alternative_t<std::variant_size_v<StanzaError> - 1, StanzaError>, UnknownStanzaError>,
"'UnknownStanzaError' must be at the end of 'StanzaError' variant");
} // namespace larra::xmpp::stanza::error

View file

@ -2,43 +2,10 @@
#include <libxml++/libxml++.h> #include <libxml++/libxml++.h>
#include <larra/utils.hpp> #include <larra/utils.hpp>
#include <nameof.hpp>
#include <ranges>
#include <variant> #include <variant>
namespace larra::xmpp { 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';
}) //
| std::views::drop(1);
};
static constexpr auto arr = str() | std::ranges::to<utils::RangeToWrapper<std::array<char, std::ranges::distance(str())>>>();
return {arr.data(), arr.size()};
}
} // namespace impl
namespace error::stream { namespace error::stream {
struct BaseError : std::exception {}; struct BaseError : std::exception {};
@ -48,7 +15,7 @@ struct BaseError : std::exception {};
template <typename T> template <typename T>
struct ErrorImpl : BaseError { struct ErrorImpl : BaseError {
static constexpr auto kDefaultName = "stream:error"; static constexpr auto kDefaultName = "stream:error";
static inline const auto kKebabCaseName = static_cast<std::string>(impl::ToKebabCaseName<T>()); static inline const auto kKebabCaseName = static_cast<std::string>(utils::ToKebabCaseName<T>());
static constexpr auto kErrorMessage = [] -> std::string_view { static constexpr auto kErrorMessage = [] -> std::string_view {
static constexpr auto name = nameof::nameof_short_type<T>(); static constexpr auto name = nameof::nameof_short_type<T>();
@ -85,7 +52,7 @@ struct UnknownXmppError : BaseError {
return TryParse(element).value(); return TryParse(element).value();
} }
friend constexpr auto operator<<(xmlpp::Element* element, const UnknownXmppError& obj) -> void { friend constexpr auto operator<<(xmlpp::Element* element, const UnknownXmppError& obj) -> void {
throw std::format("'{}' must never be written into the real stream!", kErrorMessage); throw std::runtime_error{std::format("'{}' must never be written into the real stream!", kErrorMessage)};
} }
[[nodiscard]] constexpr auto what() const noexcept -> const char* override { [[nodiscard]] constexpr auto what() const noexcept -> const char* override {
return kErrorMessage.data(); return kErrorMessage.data();
@ -145,7 +112,8 @@ using StreamError = std::variant<error::stream::BadFormat,
error::stream::UnsupportedVersion, error::stream::UnsupportedVersion,
error::stream::UnknownXmppError>; error::stream::UnknownXmppError>;
static_assert(!std::is_same_v<typename std::variant_alternative_t<std::variant_size_v<StreamError> - 1, StreamError>, StreamError>, static_assert(
std::is_same_v<typename std::variant_alternative_t<std::variant_size_v<StreamError> - 1, StreamError>, error::stream::UnknownXmppError>,
"'UnknownXmppError' must be at the end of 'StreamError' variant"); "'UnknownXmppError' must be at the end of 'StreamError' variant");
} // namespace larra::xmpp } // namespace larra::xmpp

View file

@ -1,5 +1,7 @@
#pragma once #pragma once
#include <boost/pfr.hpp> #include <boost/pfr.hpp>
#include <nameof.hpp>
#include <ranges>
#include <utempl/utils.hpp> #include <utempl/utils.hpp>
namespace larra::xmpp::utils { namespace larra::xmpp::utils {
@ -309,4 +311,31 @@ auto AccumulateFieldLength(const T& obj) -> std::size_t {
return totalLength; return totalLength;
} }
template <typename T>
constexpr auto ToKebabCaseName() -> std::string_view {
static constexpr auto rawStr = nameof::nameof_short_type<T>();
// std::isupper and std::tolower are not declared as constexpr
static constexpr auto isUpper = [](char ch) {
return ch >= 'A' && ch <= 'Z';
};
static constexpr auto toLower = [](char ch) {
return (ch >= 'A' && ch <= 'Z') ? static_cast<char>(ch + ('a' - 'A')) : ch;
};
constexpr auto str = [] {
return rawStr //
| std::views::transform([](auto ch) {
return isUpper(ch) ? std::array<char, 2>{'-', toLower(ch)} : std::array<char, 2>{ch, '\0'};
}) //
| std::views::join //
| std::views::filter([](char ch) {
return ch != '\0';
}) //
| std::views::drop(1);
};
static constexpr auto arr = str() | std::ranges::to<utils::RangeToWrapper<std::array<char, std::ranges::distance(str())>>>();
return {arr.data(), arr.size()};
}
} // namespace larra::xmpp::utils } // namespace larra::xmpp::utils

View file

@ -2,7 +2,7 @@
namespace larra::xmpp::iq { namespace larra::xmpp::iq {
auto operator<<(xmlpp::Element* element, const Bind& bind) -> void { auto operator<<(xmlpp::Element* element, const Bind& bind) -> void {
element->set_attribute("xmlns", Bind::kDefaultNamespace); element->set_namespace_declaration(Bind::kDefaultNamespace);
if(bind.jid) { if(bind.jid) {
auto* jid_el = element->add_child_element("jid"); auto* jid_el = element->add_child_element("jid");

View file

@ -21,7 +21,7 @@ auto RosterItem::Parse(xmlpp::Element* element) -> RosterItem {
} }
auto operator<<(xmlpp::Element* element, const Roster& self) -> void { auto operator<<(xmlpp::Element* element, const Roster& self) -> void {
element->set_attribute("xmlns", Roster::kDefaultNamespace); element->set_namespace_declaration(Roster::kDefaultNamespace);
S::Serialize(element, self); S::Serialize(element, self);
} }
auto Roster::Parse(xmlpp::Element* element) -> Roster { auto Roster::Parse(xmlpp::Element* element) -> Roster {

View file

@ -1,10 +1,23 @@
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <spdlog/spdlog.h>
#include <larra/iq.hpp> #include <larra/iq.hpp>
#include <stdexcept>
namespace { namespace {
static constexpr auto kExpectedData = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<iq id=\"id\" type=\"get\"><some>37</some></iq>\n"; static constexpr auto kForbiddenErrorData = R"(<?xml version="1.0" encoding="UTF-8"?>
<iq id="42" to="name@server/res" type="error"><error type="auth"><forbidden xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/></error>
</iq>)";
static constexpr auto kExpectedSetData = R"(<?xml version="1.0" encoding="UTF-8"?>
<iq id="id" type="set"><some>37</some></iq>
)";
static constexpr auto kExpectedData = R"(<?xml version="1.0" encoding="UTF-8"?>
<iq id="id" type="get"><some>37</some></iq>
)";
struct SomeStruct { struct SomeStruct {
int value; int value;
constexpr auto operator==(const SomeStruct&) const -> bool = default; constexpr auto operator==(const SomeStruct&) const -> bool = default;
@ -43,4 +56,101 @@ TEST(IQ, Parse) {
EXPECT_EQ(S::Parse(node), iq); EXPECT_EQ(S::Parse(node), iq);
} }
TEST(IQ, ParseForbiddenError) {
std::istringstream xml_stream(kForbiddenErrorData);
xmlpp::DomParser parser;
parser.parse_stream(xml_stream);
using S = Serialization<Iq<SomeStruct>>;
xmlpp::Document* doc = parser.get_document();
auto iqRes = S::Parse(doc->get_root_node());
ASSERT_TRUE(std::holds_alternative<IqError>(iqRes));
auto errorRes = std::get<IqError>(iqRes);
ASSERT_TRUE(std::holds_alternative<stanza::error::Forbidden>(errorRes.payload));
}
TEST(IQ, IqErrThrowVisitorThrow) {
std::istringstream xml_stream(kForbiddenErrorData);
xmlpp::DomParser parser;
parser.parse_stream(xml_stream);
using S = Serialization<Iq<SomeStruct>>;
xmlpp::Document* doc = parser.get_document();
auto iqRes = S::Parse(doc->get_root_node());
ASSERT_TRUE(std::holds_alternative<IqError>(iqRes));
static constexpr auto visitorErrMsg = "Test Error";
static constexpr auto throwErrMsg = "Stanza IQ Error: Forbidden";
try {
std::visit(utempl::Overloaded([](iq::Result<SomeStruct> r) {}, IqErrThrowVisitor{visitorErrMsg}), std::move(iqRes));
} catch(const stanza::error::StanzaBaseError& err) {
ASSERT_STREQ(throwErrMsg, err.what());
return;
} catch(const std::runtime_error& err) {
ASSERT_TRUE(false) << "Invalid throw type throw";
} catch(...) {
ASSERT_TRUE(false) << "Unexpected throw";
}
ASSERT_TRUE(false) << "Expected throwing an exception due to an error in output";
}
TEST(IQ, IqErrThrowVisitorThrowGet) {
std::istringstream xml_stream(kExpectedData);
xmlpp::DomParser parser;
parser.parse_stream(xml_stream);
using S = Serialization<Iq<SomeStruct>>;
xmlpp::Document* doc = parser.get_document();
auto iqRes = S::Parse(doc->get_root_node());
ASSERT_TRUE(std::holds_alternative<iq::Get<SomeStruct>>(iqRes)) << "\tERROR: Unexpected parse result";
static constexpr auto visitorErrMsg = "Test Error";
static constexpr auto throwErrMsg = "Test Error: 'Get' is an invalid type for IQ result. Expected 'Result' or 'Error'";
try {
std::visit(utempl::Overloaded([](iq::Result<SomeStruct> r) {}, IqErrThrowVisitor{"Test Error"}), std::move(iqRes));
} catch(const stanza::error::StanzaBaseError& err) {
ASSERT_TRUE(false) << "\tERROR: Invalid throw type throw";
} catch(const std::runtime_error& err) {
ASSERT_STREQ(throwErrMsg, err.what());
return;
} catch(...) {
ASSERT_TRUE(false) << "\tERROR: Unexpected throw";
}
ASSERT_TRUE(false) << "\tERROR: Expected throwing an exception due to an error in output";
}
TEST(IQ, IqErrThrowVisitorThrowSet) {
std::istringstream xml_stream(kExpectedSetData);
xmlpp::DomParser parser;
parser.parse_stream(xml_stream);
using S = Serialization<Iq<SomeStruct>>;
xmlpp::Document* doc = parser.get_document();
auto iqRes = S::Parse(doc->get_root_node());
ASSERT_TRUE(std::holds_alternative<iq::Set<SomeStruct>>(iqRes)) << "\tERROR: Unexpected parse result";
static constexpr auto visitorErrMsg = "Test Error";
static constexpr auto throwErrMsg = "Test Error: 'Set' is an invalid type for IQ result. Expected 'Result' or 'Error'";
try {
std::visit(utempl::Overloaded([](iq::Result<SomeStruct> r) {}, IqErrThrowVisitor{"Test Error"}), std::move(iqRes));
} catch(const stanza::error::StanzaBaseError& err) {
ASSERT_TRUE(false) << "\tERROR: Invalid throw type throw";
} catch(const std::runtime_error& err) {
ASSERT_STREQ(throwErrMsg, err.what());
return;
} catch(...) {
ASSERT_TRUE(false) << "\tERROR: Unexpected throw";
}
ASSERT_TRUE(false) << "\tERROR: Expected throwing an exception due to an error in output";
}
} // namespace larra::xmpp } // namespace larra::xmpp

61
tests/main.cpp Normal file
View file

@ -0,0 +1,61 @@
#include <gtest/gtest.h>
#include <spdlog/spdlog.h>
#include <boost/program_options.hpp>
#include <exception>
#include <print>
#include <stdexcept>
// clang-format off
constexpr auto ToString(spdlog::level::level_enum e) {
switch (e) {
case spdlog::level::trace: return "TRACE";
case spdlog::level::debug: return "DEBUG";
case spdlog::level::info: return "INFO";
case spdlog::level::warn: return "WARNING";
case spdlog::level::err: return "ERROR";
case spdlog::level::critical: return "CRITICAL";
case spdlog::level::off: return "OFF";
default:
return "INVALID";
}
}
// clang-format on
namespace po = boost::program_options;
auto main(int argc, char** argv) -> int {
::testing::InitGoogleTest(&argc, argv);
// Define options
po::options_description desc("Allowed options");
desc.add_options()("help,h", "Print help message")("log_level,l",
po::value<int>()->default_value(SPDLOG_LEVEL_INFO),
"Set log level: 0=TRACE, 1=DEBUG, 2=INFO, 3=WARN, 4=ERROR, 5=CRITICAL, 6=OFF");
// Parse command-line arguments
po::variables_map vm;
try {
po::store(po::parse_command_line(argc, argv, desc), vm);
po::notify(vm);
if(vm["log_level"].as<int>() < spdlog::level::level_enum::trace || vm["log_level"].as<int>() > spdlog::level::level_enum::off) {
throw std::invalid_argument{
std::format("Invalid argument value for '--log_level' option. Check option description for more details")};
}
if(vm["log_level"].as<int>() < SPDLOG_ACTIVE_LEVEL) {
SPDLOG_WARN("Specified log_level '{}' is lower than max available one '{}'. Log level will be changed according to the maximum one",
vm["log_level"].as<int>(),
SPDLOG_ACTIVE_LEVEL);
}
} catch(const std::exception& e) {
SPDLOG_CRITICAL("Cmd parse error: {}", e.what());
return 1;
}
// Cmd options handling
spdlog::set_level(static_cast<spdlog::level::level_enum>(vm["log_level"].as<int>()));
std::println("\nEnvironment setup:\n\tCurrently set log level: {}\n", ToString(spdlog::get_level()));
return RUN_ALL_TESTS();
}