Implement base for nextgen rpc subsystem (#487)

Fixes #494
This commit is contained in:
Alex Kremer
2023-02-02 13:16:01 +00:00
committed by GitHub
parent 8dbf049a71
commit 023e02da15
18 changed files with 1662 additions and 7 deletions

View File

@@ -52,6 +52,9 @@ target_sources(clio PRIVATE
src/rpc/RPCHelpers.cpp
src/rpc/Counters.cpp
src/rpc/WorkQueue.cpp
## NextGen RPC
src/rpc/common/Specs.cpp
src/rpc/common/Validators.cpp
## RPC Methods
# Account
src/rpc/handlers/AccountChannels.cpp
@@ -97,12 +100,16 @@ target_link_libraries(clio_server PUBLIC clio)
if(BUILD_TESTS)
set(TEST_TARGET clio_tests)
add_executable(${TEST_TARGET}
unittests/RPCErrors.cpp
unittests/Playground.cpp
unittests/Backend.cpp
unittests/Logger.cpp
unittests/Config.cpp
unittests/ProfilerTest.cpp
unittests/DOSGuard.cpp)
unittests/DOSGuard.cpp
unittests/rpc/ErrorTests.cpp
unittests/rpc/BaseTests.cpp
unittests/rpc/handlers/TestHandlerTests.cpp
unittests/rpc/handlers/DefaultProcessorTests.cpp)
include(CMake/deps/gtest.cmake)
# if CODE_COVERAGE enable, add clio_test-ccov

View File

@@ -21,7 +21,7 @@
#include <ripple/protocol/ErrorCodes.h>
#include <boost/json.hpp>
#include <boost/json/object.hpp>
#include <optional>
#include <string>

103
src/rpc/common/AnyHandler.h Normal file
View File

@@ -0,0 +1,103 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <rpc/common/Concepts.h>
#include <rpc/common/Types.h>
#include <rpc/common/impl/Processors.h>
namespace RPCng {
/**
* @brief A type-erased Handler that can contain any (NextGen) RPC handler class
*/
class AnyHandler final
{
public:
template <
Handler HandlerType,
typename ProcessingStrategy = detail::DefaultProcessor<HandlerType>>
/* implicit */ AnyHandler(HandlerType&& handler)
: pimpl_{std::make_unique<Model<HandlerType, ProcessingStrategy>>(
std::forward<HandlerType>(handler))}
{
}
~AnyHandler() = default;
AnyHandler(AnyHandler const& other) : pimpl_{other.pimpl_->clone()}
{
}
AnyHandler&
operator=(AnyHandler const& rhs)
{
AnyHandler copy{rhs};
pimpl_.swap(copy.pimpl_);
return *this;
}
AnyHandler(AnyHandler&&) = default;
AnyHandler&
operator=(AnyHandler&&) = default;
[[nodiscard]] ReturnType
process(boost::json::value const& value) const
{
return pimpl_->process(value);
}
private:
struct Concept
{
virtual ~Concept() = default;
[[nodiscard]] virtual ReturnType
process(boost::json::value const& value) const = 0;
[[nodiscard]] virtual std::unique_ptr<Concept>
clone() const = 0;
};
template <typename HandlerType, typename ProcessorType>
struct Model : Concept
{
HandlerType handler;
ProcessorType processor;
Model(HandlerType&& handler) : handler{std::move(handler)}
{
}
[[nodiscard]] ReturnType
process(boost::json::value const& value) const override
{
return processor(handler, value);
}
[[nodiscard]] std::unique_ptr<Concept>
clone() const override
{
return std::make_unique<Model>(*this);
}
};
private:
std::unique_ptr<Concept> pimpl_;
};
} // namespace RPCng

60
src/rpc/common/Concepts.h Normal file
View File

@@ -0,0 +1,60 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <rpc/common/Types.h>
#include <boost/json/value_from.hpp>
#include <boost/json/value_to.hpp>
#include <string>
namespace RPCng {
struct RpcSpec;
/**
* @brief A concept that specifies what a requirement used with @ref FieldSpec
* must provide
*/
// clang-format off
template <typename T>
concept Requirement = requires(T a) {
{ a.verify(boost::json::value{}, std::string{}) } -> std::same_as<MaybeError>;
};
// clang-format on
/**
* @brief A concept that specifies what a Handler type must provide
*
* Note that value_from and value_to should be implemented using tag_invoke
* as per boost::json documentation for these functions.
*/
// clang-format off
template <typename T>
concept Handler = requires(T a, typename T::Input in, typename T::Output out) {
{ a.spec() } -> std::same_as<RpcSpecConstRef>;
{ a.process(in) } -> std::same_as<HandlerReturnType<decltype(out)>>;
}
&& boost::json::has_value_from<typename T::Output>::value
&& boost::json::has_value_to<typename T::Input>::value;
// clang-format on
} // namespace RPCng

42
src/rpc/common/Specs.cpp Normal file
View File

@@ -0,0 +1,42 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <rpc/common/Specs.h>
#include <boost/json/value.hpp>
namespace RPCng {
[[nodiscard]] MaybeError
FieldSpec::validate(boost::json::value const& value) const
{
return validator_(value);
}
[[nodiscard]] MaybeError
RpcSpec::validate(boost::json::value const& value) const
{
for (auto const& field : fields_)
if (auto ret = field.validate(value); not ret)
return Error{ret.error()};
return {};
}
} // namespace RPCng

70
src/rpc/common/Specs.h Normal file
View File

@@ -0,0 +1,70 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <rpc/common/Concepts.h>
#include <rpc/common/Types.h>
#include <rpc/common/impl/Factories.h>
#include <string>
#include <vector>
namespace RPCng {
/**
* @brief Represents a Specification for one field of an RPC command
*/
struct FieldSpec final
{
template <Requirement... Requirements>
FieldSpec(std::string const& key, Requirements&&... requirements)
: validator_{detail::makeFieldValidator<Requirements...>(
key,
std::forward<Requirements>(requirements)...)}
{
}
[[nodiscard]] MaybeError
validate(boost::json::value const& value) const;
private:
std::function<MaybeError(boost::json::value const&)> validator_;
};
/**
* @brief Represents a Specification of an entire RPC command
*
* Note: this should really be all constexpr and handlers would expose
* static constexpr RpcSpec spec instead. Maybe some day in the future.
*/
struct RpcSpec final
{
RpcSpec(std::initializer_list<FieldSpec> fields) : fields_{fields}
{
}
[[nodiscard]] MaybeError
validate(boost::json::value const& value) const;
private:
std::vector<FieldSpec> fields_;
};
} // namespace RPCng

56
src/rpc/common/Types.h Normal file
View File

@@ -0,0 +1,56 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <rpc/Errors.h>
#include <util/Expected.h>
#include <boost/json/value.hpp>
namespace RPCng {
/**
* @brief Return type used for Validators that can return error but don't have
* specific value to return
*/
using MaybeError = util::Expected<void, RPC::Status>;
/**
* @brief The type that represents just the error part of @ref MaybeError
*/
using Error = util::Unexpected<RPC::Status>;
/**
* @brief Return type for each individual handler
*/
template <typename OutputType>
using HandlerReturnType = util::Expected<OutputType, RPC::Status>;
/**
* @brief The final return type out of RPC engine
*/
using ReturnType = util::Expected<boost::json::value, RPC::Status>;
struct RpcSpec;
struct FieldSpec;
using RpcSpecConstRef = RpcSpec const&;
} // namespace RPCng

View File

@@ -0,0 +1,89 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <rpc/common/Validators.h>
#include <boost/json/value.hpp>
#include <string_view>
namespace RPCng::validation {
[[nodiscard]] MaybeError
Section::verify(boost::json::value const& value, std::string_view key) const
{
if (not value.is_object() or not value.as_object().contains(key.data()))
return {}; // ignore. field does not exist, let 'required' fail
// instead
auto const& res = value.at(key.data());
for (auto const& spec : specs)
{
if (auto const ret = spec.validate(res); not ret)
return Error{ret.error()};
}
return {};
}
[[nodiscard]] MaybeError
Required::verify(boost::json::value const& value, std::string_view key) const
{
if (not value.is_object() or not value.as_object().contains(key.data()))
return Error{RPC::Status{
RPC::RippledError::rpcINVALID_PARAMS,
"Required field '" + std::string{key} + "' missing"}};
return {};
}
[[nodiscard]] MaybeError
ValidateArrayAt::verify(boost::json::value const& value, std::string_view key)
const
{
if (not value.is_object() or not value.as_object().contains(key.data()))
return {}; // ignore. field does not exist, let 'required' fail
// instead
if (not value.as_object().at(key.data()).is_array())
return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS}};
auto const& arr = value.as_object().at(key.data()).as_array();
if (idx_ >= arr.size())
return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS}};
auto const& res = arr.at(idx_);
for (auto const& spec : specs_)
if (auto const ret = spec.validate(res); not ret)
return Error{ret.error()};
return {};
}
[[nodiscard]] MaybeError
CustomValidator::verify(boost::json::value const& value, std::string_view key)
const
{
if (not value.is_object() or not value.as_object().contains(key.data()))
return {}; // ignore. field does not exist, let 'required' fail
// instead
return validator_(value.as_object().at(key.data()), key);
}
} // namespace RPCng::validation

259
src/rpc/common/Validators.h Normal file
View File

@@ -0,0 +1,259 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <rpc/common/Concepts.h>
#include <rpc/common/Specs.h>
#include <rpc/common/Types.h>
namespace RPCng::validation {
/**
* @brief Check that the type is the same as what was expected
*
* @tparam Expected The expected type that value should be convertible to
* @param value The json value to check the type of
* @return true if convertible; false otherwise
*/
template <typename Expected>
[[nodiscard]] bool static checkType(boost::json::value const& value)
{
auto hasError = false;
if constexpr (std::is_same_v<Expected, bool>)
{
if (not value.is_bool())
hasError = true;
}
else if constexpr (std::is_same_v<Expected, std::string>)
{
if (not value.is_string())
hasError = true;
}
else if constexpr (
std::is_same_v<Expected, double> or std::is_same_v<Expected, float>)
{
if (not value.is_double())
hasError = true;
}
else if constexpr (
std::is_convertible_v<Expected, uint64_t> or
std::is_convertible_v<Expected, int64_t>)
{
if (not value.is_int64() && not value.is_uint64())
hasError = true;
}
else if constexpr (std::is_same_v<Expected, boost::json::array>)
{
if (not value.is_array())
hasError = true;
}
return not hasError;
}
/**
* @brief A meta-validator that acts as a spec for a sub-object/section
*/
class Section final
{
std::vector<FieldSpec> specs;
public:
explicit Section(std::initializer_list<FieldSpec> specs) : specs{specs}
{
}
[[nodiscard]] MaybeError
verify(boost::json::value const& value, std::string_view key) const;
};
/**
* @brief A validator that simply requires a field to be present
*/
struct Required final
{
[[nodiscard]] MaybeError
verify(boost::json::value const& value, std::string_view key) const;
};
/**
* @brief Validates that the type of the value is one of the given types
*/
template <typename... Types>
struct Type final
{
[[nodiscard]] MaybeError
verify(boost::json::value const& value, std::string_view key) const
{
if (not value.is_object() or not value.as_object().contains(key.data()))
return {}; // ignore. field does not exist, let 'required' fail
// instead
auto const& res = value.as_object().at(key.data());
auto const convertible = (checkType<Types>(res) || ...);
if (not convertible)
return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS}};
return {};
}
};
/**
* @brief Validate that value is between specified min and max
*/
template <typename Type>
class Between final
{
Type min_;
Type max_;
public:
explicit Between(Type min, Type max) : min_{min}, max_{max}
{
}
[[nodiscard]] MaybeError
verify(boost::json::value const& value, std::string_view key) const
{
if (not value.is_object() or not value.as_object().contains(key.data()))
return {}; // ignore. field does not exist, let 'required' fail
// instead
using boost::json::value_to;
auto const res = value_to<Type>(value.as_object().at(key.data()));
// todo: may want a way to make this code more generic (e.g. use a free
// function that can be overridden for this comparison)
if (res < min_ || res > max_)
return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS}};
return {};
}
};
/**
* @brief Validates that the value is equal to the one passed in
*/
template <typename Type>
class EqualTo final
{
Type original_;
public:
explicit EqualTo(Type original) : original_{original}
{
}
[[nodiscard]] MaybeError
verify(boost::json::value const& value, std::string_view key) const
{
if (not value.is_object() or not value.as_object().contains(key.data()))
return {}; // ignore. field does not exist, let 'required' fail
// instead
using boost::json::value_to;
auto const res = value_to<Type>(value.as_object().at(key.data()));
if (res != original_)
return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS}};
return {};
}
};
/**
* @brief Deduction guide to help disambiguate what it means to EqualTo a
* "string" without specifying the type.
*/
EqualTo(char const*)->EqualTo<std::string>;
/**
* @brief Validates that the value is one of the values passed in
*/
template <typename Type>
class OneOf final
{
std::vector<Type> options_;
public:
explicit OneOf(std::initializer_list<Type> options) : options_{options}
{
}
[[nodiscard]] MaybeError
verify(boost::json::value const& value, std::string_view key) const
{
if (not value.is_object() or not value.as_object().contains(key.data()))
return {}; // ignore. field does not exist, let 'required' fail
// instead
using boost::json::value_to;
auto const res = value_to<Type>(value.as_object().at(key.data()));
if (std::find(std::begin(options_), std::end(options_), res) ==
std::end(options_))
return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS}};
return {};
}
};
/**
* @brief Deduction guide to help disambiguate what it means to OneOf a
* few "strings" without specifying the type.
*/
OneOf(std::initializer_list<char const*>)->OneOf<std::string>;
/**
* @brief A meta-validator that specifies a list of specs to run against the
* object at the given index in the array
*/
class ValidateArrayAt final
{
std::size_t idx_;
std::vector<FieldSpec> specs_;
public:
ValidateArrayAt(std::size_t idx, std::initializer_list<FieldSpec> specs)
: idx_{idx}, specs_{specs}
{
}
[[nodiscard]] MaybeError
verify(boost::json::value const& value, std::string_view key) const;
};
/**
* @brief A meta-validator that allows to specify a custom validation function
*/
class CustomValidator final
{
std::function<MaybeError(boost::json::value const&, std::string_view)>
validator_;
public:
template <typename Fn>
explicit CustomValidator(Fn&& fn) : validator_{std::forward<Fn>(fn)}
{
}
[[nodiscard]] MaybeError
verify(boost::json::value const& value, std::string_view key) const;
};
} // namespace RPCng::validation

View File

@@ -0,0 +1,59 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <rpc/common/Concepts.h>
#include <rpc/common/Types.h>
#include <boost/json/value.hpp>
#include <optional>
namespace RPCng::detail {
template <Requirement... Requirements>
[[nodiscard]] auto
makeFieldValidator(std::string const& key, Requirements&&... requirements)
{
return [key, ... r = std::forward<Requirements>(requirements)](
boost::json::value const& j) -> MaybeError {
// clang-format off
std::optional<RPC::Status> firstFailure = std::nullopt;
// This expands in order of Requirements and stops evaluating after
// first failure which is stored in `firstFailure` and can be checked
// later on to see whether the verification failed as a whole or not.
([&j, &key, &firstFailure, req = &r]() {
if (firstFailure)
return; // already failed earlier - skip
if (auto const res = req->verify(j, key); not res)
firstFailure = res.error();
}(), ...);
// clang-format on
if (firstFailure)
return Error{firstFailure.value()};
return {};
};
}
} // namespace RPCng::detail

View File

@@ -0,0 +1,52 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <rpc/common/Concepts.h>
#include <rpc/common/Types.h>
namespace RPCng::detail {
template <Handler HandlerType>
struct DefaultProcessor final
{
[[nodiscard]] ReturnType
operator()(HandlerType const& handler, boost::json::value const& value)
const
{
using boost::json::value_from;
using boost::json::value_to;
// first we run validation
auto const spec = handler.spec();
if (auto const ret = spec.validate(value); not ret)
return Error{ret.error()}; // forward Status
auto const inData = value_to<typename HandlerType::Input>(value);
// real handler is given expected Input, not json
if (auto const ret = handler.process(inData); not ret)
return Error{ret.error()}; // forward Status
else
return value_from(ret.value());
}
};
} // namespace RPCng::detail

264
src/util/Expected.h Normal file
View File

@@ -0,0 +1,264 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Copyright (c) 2021 Ripple Labs Inc.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
/*
* NOTE:
*
* This entire file is taken from rippled and modified slightly to fit this
* codebase as well as fixing the original issue that made this necessary.
*
* The reason is that currently there is no easy way to injest the fix that is
* required to make this implementation correctly work with boost::json::value.
* Since this will be replaced by `std::expected` as soon as possible there is
* not much harm done in doing it this way.
*/
#pragma once
#include <ripple/basics/contract.h>
#include <boost/outcome.hpp>
#include <stdexcept>
#include <type_traits>
namespace util {
/** Expected is an approximation of std::expected (hoped for in C++23)
See: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p0323r10.html
The implementation is entirely based on boost::outcome_v2::result.
*/
// Exception thrown by an invalid access to Expected.
struct bad_expected_access : public std::runtime_error
{
bad_expected_access() : runtime_error("bad expected access")
{
}
};
namespace detail {
// Custom policy for Expected. Always throw on an invalid access.
struct throw_policy : public boost::outcome_v2::policy::base
{
template <class Impl>
static constexpr void
wide_value_check(Impl&& self)
{
if (!base::_has_value(std::forward<Impl>(self)))
ripple::Throw<bad_expected_access>();
}
template <class Impl>
static constexpr void
wide_error_check(Impl&& self)
{
if (!base::_has_error(std::forward<Impl>(self)))
ripple::Throw<bad_expected_access>();
}
template <class Impl>
static constexpr void
wide_exception_check(Impl&& self)
{
if (!base::_has_exception(std::forward<Impl>(self)))
ripple::Throw<bad_expected_access>();
}
};
} // namespace detail
// Definition of Unexpected, which is used to construct the unexpected
// return type of an Expected.
template <class E>
class Unexpected
{
public:
static_assert(!std::is_same<E, void>::value, "E must not be void");
Unexpected() = delete;
constexpr explicit Unexpected(E const& e) : val_(e)
{
}
constexpr explicit Unexpected(E&& e) : val_(std::move(e))
{
}
constexpr const E&
value() const&
{
return val_;
}
constexpr E&
value() &
{
return val_;
}
constexpr E&&
value() &&
{
return std::move(val_);
}
constexpr const E&&
value() const&&
{
return std::move(val_);
}
private:
E val_;
};
// Unexpected deduction guide that converts array to const*.
template <typename E, std::size_t N>
Unexpected(E (&)[N]) -> Unexpected<E const*>;
// Definition of Expected. All of the machinery comes from boost::result.
template <class T, class E>
class [[nodiscard]] Expected
: private boost::outcome_v2::result<T, E, detail::throw_policy>
{
using Base = boost::outcome_v2::result<T, E, detail::throw_policy>;
public:
template <
typename U,
typename = std::enable_if_t<std::is_convertible_v<U, T>>>
constexpr Expected(U r) : Base(T(std::forward<U>(r)))
{
}
template <
typename U,
typename = std::enable_if_t<std::is_convertible_v<U, E>>>
constexpr Expected(Unexpected<U> e) : Base(E(std::forward<U>(e.value())))
{
}
constexpr bool
has_value() const
{
return Base::has_value();
}
constexpr T const&
value() const
{
return Base::value();
}
constexpr T&
value()
{
return Base::value();
}
constexpr E const&
error() const
{
return Base::error();
}
constexpr E&
error()
{
return Base::error();
}
constexpr explicit operator bool() const
{
return has_value();
}
// Add operator* and operator-> so the Expected API looks a bit more like
// what std::expected is likely to look like. See:
// http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p0323r10.html
[[nodiscard]] constexpr T&
operator*()
{
return this->value();
}
[[nodiscard]] constexpr T const&
operator*() const
{
return this->value();
}
[[nodiscard]] constexpr T*
operator->()
{
return &this->value();
}
[[nodiscard]] constexpr T const*
operator->() const
{
return &this->value();
}
};
// Specialization of Expected<void, E>. Allows returning either success
// (without a value) or the reason for the failure.
template <class E>
class [[nodiscard]] Expected<void, E>
: private boost::outcome_v2::result<void, E, detail::throw_policy>
{
using Base = boost::outcome_v2::result<void, E, detail::throw_policy>;
public:
// The default constructor makes a successful Expected<void, E>.
// This aligns with std::expected behavior proposed in P0323R10.
constexpr Expected() : Base(boost::outcome_v2::success())
{
}
template <
typename U,
typename = std::enable_if_t<std::is_convertible_v<U, E>>>
constexpr Expected(Unexpected<U> e) : Base(E(std::forward<U>(e.value())))
{
}
constexpr E const&
error() const
{
return Base::error();
}
constexpr E&
error()
{
return Base::error();
}
constexpr explicit operator bool() const
{
return Base::has_value();
}
};
} // namespace util

31
unittests/Playground.cpp Normal file
View File

@@ -0,0 +1,31 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <util/Fixtures.h>
using namespace clio;
/*
* Use this file for temporary tests and implementations.
* Note: Please don't push your temporary work to the repo.
*/
// TEST(PlaygroundTest, MyTest)
// {
// }

245
unittests/rpc/BaseTests.cpp Normal file
View File

@@ -0,0 +1,245 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <util/Fixtures.h>
#include <rpc/RPC.h>
#include <rpc/common/AnyHandler.h>
#include <rpc/common/Specs.h>
#include <rpc/common/Validators.h>
#include <boost/json/parse.hpp>
#include <gtest/gtest.h>
#include <optional>
#include <string>
using namespace testing;
using namespace clio;
using namespace std;
using namespace RPCng;
using namespace RPCng::validation;
namespace json = boost::json;
class RPCBaseTest : public NoLoggerFixture
{
};
TEST_F(RPCBaseTest, CheckType)
{
auto const jstr = json::value("a string");
ASSERT_TRUE(checkType<string>(jstr));
ASSERT_FALSE(checkType<int>(jstr));
auto const juint = json::value(123u);
ASSERT_TRUE(checkType<uint32_t>(juint));
ASSERT_TRUE(checkType<int32_t>(juint));
ASSERT_FALSE(checkType<bool>(juint));
auto const jint = json::value(123);
ASSERT_TRUE(checkType<int32_t>(jint));
ASSERT_TRUE(checkType<uint32_t>(jint));
ASSERT_FALSE(checkType<bool>(jint));
auto const jbool = json::value(true);
ASSERT_TRUE(checkType<bool>(jbool));
ASSERT_FALSE(checkType<int>(jbool));
auto const jdouble = json::value(0.123);
ASSERT_TRUE(checkType<double>(jdouble));
ASSERT_TRUE(checkType<float>(jdouble));
ASSERT_FALSE(checkType<bool>(jdouble));
auto const jarr = json::value({1, 2, 3});
ASSERT_TRUE(checkType<json::array>(jarr));
ASSERT_FALSE(checkType<int>(jarr));
}
TEST_F(RPCBaseTest, TypeValidator)
{
auto spec = RpcSpec{
{"uint", Type<uint32_t>{}},
{"int", Type<int32_t>{}},
{"str", Type<string>{}},
{"double", Type<double>{}},
{"bool", Type<bool>{}},
{"arr", Type<json::array>{}},
};
auto passingInput = json::parse(R"({
"uint": 123,
"int": 321,
"str": "a string",
"double": 1.0,
"bool": true,
"arr": []
})");
ASSERT_TRUE(spec.validate(passingInput));
{
auto failingInput = json::parse(R"({ "uint": "a string" })");
ASSERT_FALSE(spec.validate(failingInput));
}
{
auto failingInput = json::parse(R"({ "int": "a string" })");
ASSERT_FALSE(spec.validate(failingInput));
}
{
auto failingInput = json::parse(R"({ "str": 1234 })");
ASSERT_FALSE(spec.validate(failingInput));
}
{
auto failingInput = json::parse(R"({ "double": "a string" })");
ASSERT_FALSE(spec.validate(failingInput));
}
{
auto failingInput = json::parse(R"({ "bool": "a string" })");
ASSERT_FALSE(spec.validate(failingInput));
}
{
auto failingInput = json::parse(R"({ "arr": "a string" })");
ASSERT_FALSE(spec.validate(failingInput));
}
}
TEST_F(RPCBaseTest, TypeValidatorMultipleTypes)
{
auto spec = RpcSpec{
// either int or string
{"test", Type<uint32_t, string>{}},
};
auto passingInput = json::parse(R"({ "test": "1234" })");
ASSERT_TRUE(spec.validate(passingInput));
auto passingInput2 = json::parse(R"({ "test": 1234 })");
ASSERT_TRUE(spec.validate(passingInput2));
auto failingInput = json::parse(R"({ "test": true })");
ASSERT_FALSE(spec.validate(failingInput));
}
TEST_F(RPCBaseTest, RequiredValidator)
{
auto spec = RpcSpec{
{"required", Required{}},
};
auto passingInput = json::parse(R"({ "required": "present" })");
ASSERT_TRUE(spec.validate(passingInput));
auto passingInput2 = json::parse(R"({ "required": true })");
ASSERT_TRUE(spec.validate(passingInput2));
auto failingInput = json::parse(R"({})");
ASSERT_FALSE(spec.validate(failingInput));
}
TEST_F(RPCBaseTest, BetweenValidator)
{
auto spec = RpcSpec{
{"amount", Between<uint32_t>{10u, 20u}},
};
auto passingInput = json::parse(R"({ "amount": 15 })");
ASSERT_TRUE(spec.validate(passingInput));
auto passingInput2 = json::parse(R"({ "amount": 10 })");
ASSERT_TRUE(spec.validate(passingInput2));
auto passingInput3 = json::parse(R"({ "amount": 20 })");
ASSERT_TRUE(spec.validate(passingInput3));
auto failingInput = json::parse(R"({ "amount": 9 })");
ASSERT_FALSE(spec.validate(failingInput));
auto failingInput2 = json::parse(R"({ "amount": 21 })");
ASSERT_FALSE(spec.validate(failingInput2));
}
TEST_F(RPCBaseTest, OneOfValidator)
{
auto spec = RpcSpec{
{"currency", OneOf{"XRP", "USD"}},
};
auto passingInput = json::parse(R"({ "currency": "XRP" })");
ASSERT_TRUE(spec.validate(passingInput));
auto passingInput2 = json::parse(R"({ "currency": "USD" })");
ASSERT_TRUE(spec.validate(passingInput2));
auto failingInput = json::parse(R"({ "currency": "PRX" })");
ASSERT_FALSE(spec.validate(failingInput));
}
TEST_F(RPCBaseTest, EqualToValidator)
{
auto spec = RpcSpec{
{"exact", EqualTo{"CaseSensitive"}},
};
auto passingInput = json::parse(R"({ "exact": "CaseSensitive" })");
ASSERT_TRUE(spec.validate(passingInput));
auto failingInput = json::parse(R"({ "exact": "Different" })");
ASSERT_FALSE(spec.validate(failingInput));
}
TEST_F(RPCBaseTest, ArrayAtValidator)
{
// clang-format off
auto spec = RpcSpec{
{"arr", Required{}, Type<json::array>{}, ValidateArrayAt{0, {
{"limit", Required{}, Type<uint32_t>{}, Between<uint32_t>{0, 100}},
}}},
};
// clang-format on
auto passingInput = json::parse(R"({ "arr": [{"limit": 42}] })");
ASSERT_TRUE(spec.validate(passingInput));
auto failingInput = json::parse(R"({ "arr": [{"limit": "not int"}] })");
ASSERT_FALSE(spec.validate(failingInput));
}
TEST_F(RPCBaseTest, CustomValidator)
{
// clang-format off
auto customFormatCheck = CustomValidator{
[](json::value const& value, std::string_view key) -> MaybeError {
return value.as_string().size() == 34 ?
MaybeError{} : Error{RPC::Status{"Uh oh"}};
}
};
// clang-format on
auto spec = RpcSpec{
{"taker", customFormatCheck},
};
auto passingInput =
json::parse(R"({ "taker": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59" })");
ASSERT_TRUE(spec.validate(passingInput));
auto failingInput = json::parse(R"({ "taker": "wrongformat" })");
ASSERT_FALSE(spec.validate(failingInput));
}

View File

@@ -0,0 +1,66 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <rpc/handlers/impl/FakesAndMocks.h>
#include <util/Fixtures.h>
#include <rpc/common/impl/Processors.h>
#include <boost/json/parse.hpp>
using namespace testing;
using namespace std;
using namespace RPCng;
using namespace RPCng::validation;
using namespace unittests::detail;
namespace json = boost::json;
class RPCDefaultProcessorTest : public NoLoggerFixture
{
};
TEST_F(RPCDefaultProcessorTest, ValidInput)
{
HandlerMock handler;
RPCng::detail::DefaultProcessor<HandlerMock> processor;
auto const input = json::parse(R"({ "something": "works" })");
auto const spec = RpcSpec{{"something", Required{}}};
auto const data = InOutFake{"works"};
EXPECT_CALL(handler, spec()).WillOnce(ReturnRef(spec));
EXPECT_CALL(handler, process(Eq(data))).WillOnce(Return(data));
auto const ret = processor(handler, input);
ASSERT_TRUE(ret); // no error
}
TEST_F(RPCDefaultProcessorTest, InvalidInput)
{
HandlerMock handler;
RPCng::detail::DefaultProcessor<HandlerMock> processor;
auto const input = json::parse(R"({ "other": "nope" })");
auto const spec = RpcSpec{{"something", Required{}}};
EXPECT_CALL(handler, spec()).WillOnce(ReturnRef(spec));
auto const ret = processor(handler, input);
ASSERT_FALSE(ret); // returns error
}

View File

@@ -0,0 +1,84 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <rpc/common/AnyHandler.h>
#include <rpc/handlers/impl/FakesAndMocks.h>
#include <util/Fixtures.h>
#include <boost/json/parse.hpp>
using namespace std;
using namespace RPCng;
using namespace RPCng::validation;
using namespace unittests::detail;
namespace json = boost::json;
class RPCTestHandlerTest : public NoLoggerFixture
{
};
// example handler tests
TEST_F(RPCTestHandlerTest, HandlerSuccess)
{
auto const handler = AnyHandler{HandlerFake{}};
auto const input = json::parse(R"({
"hello": "world",
"limit": 10
})");
auto const output = handler.process(input);
ASSERT_TRUE(output);
auto const val = output.value();
EXPECT_EQ(val.as_object().at("computed").as_string(), "world_10");
}
TEST_F(RPCTestHandlerTest, HandlerErrorHandling)
{
auto const handler = AnyHandler{HandlerFake{}};
auto const input = json::parse(R"({
"hello": "not world",
"limit": 10
})");
auto const output = handler.process(input);
ASSERT_FALSE(output);
auto const err = RPC::makeError(output.error());
EXPECT_EQ(err.at("error").as_string(), "invalidParams");
EXPECT_EQ(err.at("error_message").as_string(), "Invalid parameters.");
EXPECT_EQ(err.at("error_code").as_uint64(), 31);
}
TEST_F(RPCTestHandlerTest, HandlerInnerErrorHandling)
{
auto const handler = AnyHandler{FailingHandlerFake{}};
auto const input = json::parse(R"({
"hello": "world",
"limit": 10
})");
// validation succeeds but handler itself returns error
auto const output = handler.process(input);
ASSERT_FALSE(output);
auto const err = RPC::makeError(output.error());
EXPECT_EQ(err.at("error").as_string(), "Very custom error");
}

View File

@@ -0,0 +1,168 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <rpc/RPC.h>
#include <rpc/common/Specs.h>
#include <rpc/common/Validators.h>
#include <boost/json/value.hpp>
#include <boost/json/value_from.hpp>
#include <boost/json/value_to.hpp>
#include <gmock/gmock.h>
#include <optional>
#include <string>
namespace unittests::detail {
// input data for the test handlers below
struct TestInput
{
std::string hello;
std::optional<uint32_t> limit;
};
// output data produced by the test handlers below
struct TestOutput
{
std::string computed;
};
// must be implemented as per rpc/common/Concepts.h
inline TestInput
tag_invoke(boost::json::value_to_tag<TestInput>, boost::json::value const& jv)
{
std::optional<uint32_t> optLimit;
if (jv.as_object().contains("limit"))
optLimit = jv.at("limit").as_int64();
return {jv.as_object().at("hello").as_string().c_str(), optLimit};
}
// must be implemented as per rpc/common/Concepts.h
inline void
tag_invoke(
boost::json::value_from_tag,
boost::json::value& jv,
TestOutput output)
{
jv = {{"computed", output.computed}};
}
// example handler
class HandlerFake
{
public:
using Input = TestInput;
using Output = TestOutput;
using Result = RPCng::HandlerReturnType<Output>;
RPCng::RpcSpecConstRef
spec() const
{
using namespace RPCng::validation;
// clang-format off
static const RPCng::RpcSpec rpcSpec = {
{"hello", Required{}, Type<std::string>{}, EqualTo{"world"}},
{"limit", Type<uint32_t>{}, Between<uint32_t>{0, 100}} // optional field
};
// clang-format on
return rpcSpec;
}
Result
process(Input input) const
{
return Output{
input.hello + '_' + std::to_string(input.limit.value_or(0))};
}
};
// example handler that returns custom error
class FailingHandlerFake
{
public:
using Input = TestInput;
using Output = TestOutput;
using Result = RPCng::HandlerReturnType<Output>;
RPCng::RpcSpecConstRef
spec() const
{
using namespace RPCng::validation;
// clang-format off
static const RPCng::RpcSpec rpcSpec = {
{"hello", Required{}, Type<std::string>{}, EqualTo{"world"}},
{"limit", Type<uint32_t>{}, Between<uint32_t>{0u, 100u}} // optional field
};
// clang-format on
return rpcSpec;
}
Result
process([[maybe_unused]] Input input) const
{
// always fail
return RPCng::Error{RPC::Status{"Very custom error"}};
}
};
struct InOutFake
{
std::string something;
// Note: no spaceship comparison possible for std::string
friend bool
operator==(InOutFake const& lhs, InOutFake const& rhs) = default;
};
// must be implemented as per rpc/common/Concepts.h
inline InOutFake
tag_invoke(boost::json::value_to_tag<InOutFake>, boost::json::value const& jv)
{
return {jv.as_object().at("something").as_string().c_str()};
}
// must be implemented as per rpc/common/Concepts.h
inline void
tag_invoke(
boost::json::value_from_tag,
boost::json::value& jv,
InOutFake output)
{
jv = {{"something", output.something}};
}
struct HandlerMock
{
using Input = InOutFake;
using Output = InOutFake;
using Result = RPCng::HandlerReturnType<Output>;
MOCK_METHOD(RPCng::RpcSpecConstRef, spec, (), (const));
MOCK_METHOD(Result, process, (Input), (const));
};
} // namespace unittests::detail