mirror of
https://github.com/XRPLF/clio.git
synced 2026-09-27 15:38:04 +00:00
refactor: Migrate handlers to rpc-spec (G) (#3213)
This commit is contained in:
@@ -16,9 +16,6 @@ target_sources(
|
||||
CredentialHelpers.cpp
|
||||
Counters.cpp
|
||||
WorkQueue.cpp
|
||||
common/Specs.cpp
|
||||
common/Validators.cpp
|
||||
common/MetaProcessors.cpp
|
||||
common/impl/APIVersionParser.cpp
|
||||
common/impl/HandlerProvider.cpp
|
||||
common/impl/HandlerRegistry.cpp
|
||||
|
||||
@@ -4,27 +4,74 @@
|
||||
|
||||
The RPC subsystem is where the common framework for handling incoming JSON requests is implemented.
|
||||
|
||||
Request validation and deserialisation are **not** defined here. They live in the shared
|
||||
[`xrpl-rpc-spec`](https://github.com/XRPLF/rpc-spec) library, consumed as `<rpcspec/...>`, so that
|
||||
one spec per RPC method can be shared verbatim between Clio and xrpld. Clio's job is to supply the
|
||||
handler body and the response shape.
|
||||
|
||||
## Components
|
||||
|
||||
See the [common](https://github.com/XRPLF/clio/blob/develop/src/rpc/common) subfolder.
|
||||
|
||||
- **AnyHandler**: The type-erased wrapper that allows for storing different handlers in one map/vector.
|
||||
- **RpcSpec/FieldSpec**: The RPC specification classes, used to specify how incoming JSON is to be validated before it's parsed and passed on to individual handler implementations.
|
||||
- **Validators/Modifiers**: A bunch of supported validators and modifiers that can be specified as requirements for each `FieldSpec` to make up the final `RpcSpec` of any given RPC handler.
|
||||
- **Concepts**: The `SomeHandler` concept and its parts, which define what a handler must provide.
|
||||
- **HandlerRegistry**: The table mapping a method name to its handler factory, plus whether the
|
||||
method is Clio-only (and therefore never forwarded to xrpld).
|
||||
|
||||
From the spec library:
|
||||
|
||||
- **`rpc::spec::HandlerFor<Input>`**: Base class supplying the static `parseInput` and `spec`
|
||||
entry points a handler needs. It resolves the versioned spec for `Input` through ADL, so a
|
||||
handler names only its `Input` type.
|
||||
- **`rpcspec/handlers/<method>/Types.hpp`**: The strongly-typed `Input` struct for a method
|
||||
(`xrpl::AccountID`, `xrpl::uint256`, `LedgerSpecifier`, ... rather than `std::string`).
|
||||
- **`rpcspec/handlers/<method>/Spec.hpp`**: The consteval spec declaring that method's fields,
|
||||
their validators and their converters.
|
||||
|
||||
## Implementing a handler
|
||||
|
||||
See [tests/unit/rpc](https://github.com/XRPLF/clio/tree/develop/tests/unit/rpc) for examples.
|
||||
See the existing handlers in [src/rpc/handlers](https://github.com/XRPLF/clio/tree/develop/src/rpc/handlers)
|
||||
for examples; `NFTInfo` is a small one.
|
||||
|
||||
Handlers need to fulfil the requirements specified by the `SomeHandler` concept (see `rpc/common/Concepts.hpp`):
|
||||
Handlers need to fulfil the requirements specified by the `SomeHandler` concept (see
|
||||
`rpc/common/Concepts.hpp`):
|
||||
|
||||
- Expose types:
|
||||
- `Input` - The POD struct which acts as input for the handler
|
||||
- Derive from `rpc::spec::HandlerFor<rpc::spec::handlers::<method>::Input>`. This supplies:
|
||||
|
||||
- `Output` - The POD struct which acts as output of a valid handler invocation
|
||||
- `Input` — the strongly-typed input struct, owned by the spec library rather than declared here
|
||||
|
||||
- Have a `spec(uint32_t)` member function returning a const reference to an `RpcSpec` describing the JSON input for the specified API version.
|
||||
- `static parseInput(boost::json::value const&, uint32_t apiVersion)` — validates and
|
||||
deserialises in one pass, returning `std::expected<Input, Status>`
|
||||
|
||||
- Have a `process(Input)` member function that operates on `Input` POD and returns `HandlerReturnType<Output>`
|
||||
- `static spec(uint32_t apiVersion)` — returns a type-erased `rpc::spec::RpcSpecView`
|
||||
|
||||
- Implement `value_from` and `value_to` support using `tag_invoke` as per `boost::json` documentation for these functions.
|
||||
If the method takes no input at all, skip the base class and expose only `process(Context const&)`.
|
||||
|
||||
- Expose an `Output` POD struct which acts as output of a valid handler invocation.
|
||||
|
||||
- Have a `process(Input const&, Context const&)` member function returning
|
||||
`HandlerReturnType<Output>`. Cross-field checks that cannot be expressed in the spec belong here.
|
||||
|
||||
- Implement `value_from` support for `Output` using `tag_invoke` as per `boost::json`
|
||||
documentation. A `value_to` for `Input` is **not** needed — the spec deserialises it.
|
||||
|
||||
- Register the method in `rpc/common/impl/HandlerRegistry.cpp`.
|
||||
|
||||
If the method has no spec yet, add `Types.hpp` and `Spec.hpp` for it in the spec library first.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Do not hand-write `template struct rpc::spec::HandlerFor<...>;`, and do not include
|
||||
> `<rpcspec/HandlerForDefs.hpp>` or `<rpcspec/handlers/*/Spec.hpp>` from a handler. The explicit
|
||||
> instantiations are generated by `rpcspec_generate_instantiations()` in `src/rpc/CMakeLists.txt`,
|
||||
> one translation unit per method that has a `Spec.hpp`. A useful side effect is that every spec is
|
||||
> compiled by Clio's build, so spec-side mistakes surface here.
|
||||
|
||||
## Error messages
|
||||
|
||||
A method's error codes and messages are part of its wire contract: clients parse them, so an
|
||||
altered `error`, `error_code` or `error_message` is a breaking change. If a test disagrees with the
|
||||
code, fix the spec rather than the expectation.
|
||||
|
||||
Clio and xrpld do not always word the same failure identically. The spec library expresses those
|
||||
differences per server rather than unifying early — `ifServerClio()` / `ifServerXrpld()` for
|
||||
whole validators, and the `RPCSPEC_IS_CLIO` / `RPCSPEC_IS_XRPLD` macros for anything finer.
|
||||
|
||||
@@ -569,12 +569,12 @@ getLedgerHeaderFromLedgerSpecifier(
|
||||
return *maybeLgrInfo;
|
||||
}
|
||||
|
||||
if (resolved.isShortcut()) {
|
||||
auto const shortcut = std::get<rpc::spec::LedgerShortcut>(resolved.value);
|
||||
ASSERT(
|
||||
shortcut == rpc::spec::LedgerShortcut::Validated,
|
||||
"current/closed ledgers must be forwarded before dispatch"
|
||||
);
|
||||
// `current` and `closed` name ledgers Clio does not hold. Forwarding diverts them for the
|
||||
// methods xrpld can answer; Clio-only methods are not forwarded, so they reach here.
|
||||
if (resolved.isShortcut() and
|
||||
std::get<rpc::spec::LedgerShortcut>(resolved.value) !=
|
||||
rpc::spec::LedgerShortcut::Validated) {
|
||||
return std::unexpected{Status{RippledError::RpcInvalidParams, "ledgerIndexMalformed"}};
|
||||
}
|
||||
|
||||
auto const ledgerSequence = resolved.isSequence() ? std::get<uint32_t>(resolved.value) : maxSeq;
|
||||
|
||||
@@ -313,8 +313,12 @@ getLedgerHeaderFromHashOrSeq(
|
||||
* only serves validated data. An unspecified ledger resolves via
|
||||
* @c LedgerSpecifier::resolved(), which the spec library fixes to @c validated for Clio.
|
||||
*
|
||||
* @c current and @c closed cannot reach here: @ref specifiesCurrentOrClosedLedger forwards
|
||||
* those upstream before dispatch.
|
||||
* @c current and @c closed name ledgers Clio does not hold, and yield @c invalidParams with
|
||||
* @c ledgerIndexMalformed.
|
||||
* @ref specifiesCurrentOrClosedLedger diverts such requests to xrpld before dispatch, but only
|
||||
* for methods xrpld can answer: @c ForwardingProxy::shouldForward returns early for Clio-only
|
||||
* methods, so those arrive here with the shortcut intact. The spec does not reject the two
|
||||
* shortcuts either, since xrpld needs them.
|
||||
*
|
||||
* @param backend The backend to use
|
||||
* @param yield The coroutine context
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "rpc/common/ValidationHelpers.hpp"
|
||||
|
||||
#include <boost/json/value.hpp>
|
||||
#include <boost/json/value_to.hpp>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace rpc::check {
|
||||
/**
|
||||
* @brief Warning that checks can return
|
||||
*/
|
||||
struct Warning {
|
||||
/**
|
||||
* @brief Construct a new Warning object
|
||||
*
|
||||
* @param code The warning code
|
||||
* @param message The warning message
|
||||
*/
|
||||
Warning(WarningCode code, std::string message)
|
||||
: warningCode(code), extraMessage(std::move(message))
|
||||
{
|
||||
}
|
||||
|
||||
bool
|
||||
operator==(Warning const& other) const = default;
|
||||
|
||||
WarningCode warningCode;
|
||||
std::string extraMessage;
|
||||
};
|
||||
using Warnings = std::vector<Warning>;
|
||||
|
||||
/**
|
||||
* @brief Check for a deprecated fields
|
||||
*/
|
||||
template <typename... T>
|
||||
class Deprecated;
|
||||
|
||||
/**
|
||||
* @brief Check if a field is deprecated
|
||||
*/
|
||||
template <>
|
||||
class Deprecated<> final {
|
||||
public:
|
||||
/**
|
||||
* @brief Check if a field is deprecated
|
||||
*
|
||||
* @param value The json value to check
|
||||
* @param key The key to check
|
||||
* @return A warning if the field is deprecated or std::nullopt otherwise
|
||||
*/
|
||||
[[nodiscard]] static std::optional<Warning>
|
||||
check(boost::json::value const& value, std::string_view key)
|
||||
{
|
||||
if (value.is_object() and value.as_object().contains(key)) {
|
||||
return Warning{
|
||||
WarningCode::WarnRpcDeprecated, fmt::format("Field '{}' is deprecated.", key)
|
||||
};
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Check if a value of a field is deprecated
|
||||
* @tparam T The type of the field
|
||||
*/
|
||||
template <typename T>
|
||||
class Deprecated<T> final {
|
||||
T value_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new Deprecated object
|
||||
*
|
||||
* @param val The value that is deprecated
|
||||
*/
|
||||
Deprecated(T val) : value_(std::move(val))
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if a value of a field is deprecated
|
||||
*
|
||||
* @param value The json value to check
|
||||
* @param key The key to check
|
||||
* @return A warning if the field is deprecated or std::nullopt otherwise
|
||||
*/
|
||||
[[nodiscard]] std::optional<Warning>
|
||||
check(boost::json::value const& value, std::string_view key) const
|
||||
{
|
||||
if (value.is_object() and value.as_object().contains(key) and
|
||||
validation::checkType<T>(value.as_object().at(key))) {
|
||||
using boost::json::value_to;
|
||||
auto const res = value_to<T>(value.as_object().at(key));
|
||||
if (value_ == res) {
|
||||
return Warning{
|
||||
WarningCode::WarnRpcDeprecated,
|
||||
fmt::format("Value '{}' for field '{}' is deprecated", value_, key)
|
||||
};
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Deduction guide for Deprecated
|
||||
*/
|
||||
template <typename... T>
|
||||
Deprecated(T&&...) -> Deprecated<T...>;
|
||||
|
||||
} // namespace rpc::check
|
||||
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "rpc/common/Checkers.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
|
||||
#include <boost/json/value.hpp>
|
||||
@@ -18,38 +17,6 @@
|
||||
|
||||
namespace rpc {
|
||||
|
||||
struct RpcSpec;
|
||||
|
||||
/**
|
||||
* @brief Specifies what a requirement used with @ref rpc::FieldSpec must provide.
|
||||
*/
|
||||
template <typename T>
|
||||
concept SomeRequirement = requires(T a, boost::json::value lval) {
|
||||
{ a.verify(lval, std::string{}) } -> std::same_as<MaybeError>;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Specifies what a modifier used with @ref rpc::FieldSpec must provide.
|
||||
*/
|
||||
template <typename T>
|
||||
concept SomeModifier = requires(T a, boost::json::value lval) {
|
||||
{ a.modify(lval, std::string{}) } -> std::same_as<MaybeError>;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Specifies what a check used with @ref rpc::FieldSpec must provide.
|
||||
*/
|
||||
template <typename T>
|
||||
concept SomeCheck = requires(T a, boost::json::value lval) {
|
||||
{ a.check(lval, std::string{}) } -> std::same_as<std::optional<check::Warning>>;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief The requirements of a processor to be used with @ref rpc::FieldSpec.
|
||||
*/
|
||||
template <typename T>
|
||||
concept SomeProcessor = (SomeRequirement<T> or SomeModifier<T>);
|
||||
|
||||
/**
|
||||
* @brief A process function that expects both some Input and a Context.
|
||||
*/
|
||||
@@ -67,25 +34,12 @@ concept SomeContextProcessWithoutInput = requires(T a, T::Output out, Context co
|
||||
{ a.process(ctx) } -> std::same_as<HandlerReturnType<decltype(out)>>;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Specifies what a Handler with Input must provide.
|
||||
*/
|
||||
template <typename T>
|
||||
concept SomeHandlerWithInput = requires(T a, uint32_t version) {
|
||||
{ a.spec(version) } -> std::same_as<RpcSpec const&>;
|
||||
} and SomeContextProcessWithInput<T> and boost::json::has_value_to<typename T::Input>::value;
|
||||
|
||||
/**
|
||||
* @brief Specifies what a Handler validated by the shared consteval spec must provide.
|
||||
*
|
||||
* Such a handler inherits @c rpc::spec::HandlerFor<Input> from the spec library, which
|
||||
* supplies a static @c parseInput (validate and deserialise in one pass) and a static
|
||||
* @c spec returning a type-erased @c RpcSpecView. Presence of @c parseInput is what
|
||||
* selects this path over @c SomeHandlerWithInput.
|
||||
*
|
||||
* The two input paths are mutually exclusive by construction: a legacy handler returns
|
||||
* @c RpcSpec @c const& from a non-static @c spec and needs a @c value_to for its Input,
|
||||
* neither of which holds here. @c kIsSingleInputPath asserts that below.
|
||||
* @c spec returning a type-erased @c RpcSpecView.
|
||||
*/
|
||||
template <typename T>
|
||||
concept SomeHandlerWithTypedInput = requires(uint32_t version, boost::json::value jv) {
|
||||
@@ -100,21 +54,11 @@ concept SomeHandlerWithTypedInput = requires(uint32_t version, boost::json::valu
|
||||
template <typename T>
|
||||
concept SomeHandlerWithoutInput = SomeContextProcessWithoutInput<T>;
|
||||
|
||||
/**
|
||||
* @brief True when @p T does not straddle the legacy and typed input paths.
|
||||
*
|
||||
* Guards the @c if @c constexpr chain in @c DefaultProcessor - were a handler to satisfy
|
||||
* both, the dispatch order alone would silently decide which spec ran.
|
||||
*/
|
||||
template <typename T>
|
||||
constexpr bool kIsSingleInputPath = not(SomeHandlerWithInput<T> and SomeHandlerWithTypedInput<T>);
|
||||
|
||||
/**
|
||||
* @brief Specifies what a Handler type must provide.
|
||||
*/
|
||||
template <typename T>
|
||||
concept SomeHandler =
|
||||
(SomeHandlerWithInput<T> or SomeHandlerWithTypedInput<T> or SomeHandlerWithoutInput<T>) and
|
||||
concept SomeHandler = (SomeHandlerWithTypedInput<T> or SomeHandlerWithoutInput<T>) and
|
||||
boost::json::has_value_from<typename T::Output>::value;
|
||||
|
||||
} // namespace rpc
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
#include "rpc/common/MetaProcessors.hpp"
|
||||
|
||||
#include "rpc/common/Types.hpp"
|
||||
|
||||
#include <boost/json/value.hpp>
|
||||
#include <rpcspec/Errors.hpp>
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace rpc::meta {
|
||||
|
||||
[[nodiscard]] MaybeError
|
||||
Section::verify(boost::json::value& value, std::string_view key) const
|
||||
{
|
||||
if (not value.is_object() or not value.as_object().contains(key))
|
||||
return {}; // ignore. field does not exist, let 'required' fail instead
|
||||
|
||||
auto& res = value.as_object().at(key);
|
||||
|
||||
// if it is not a json object, let other validators fail
|
||||
if (!res.is_object())
|
||||
return {};
|
||||
|
||||
for (auto const& spec : specs_) {
|
||||
if (auto const ret = spec.process(res); not ret)
|
||||
return Error{ret.error()};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] MaybeError
|
||||
ValidateArrayAt::verify(boost::json::value& value, std::string_view key) const
|
||||
{
|
||||
if (not value.is_object() or not value.as_object().contains(key))
|
||||
return {}; // ignore. field does not exist, let 'required' fail instead
|
||||
|
||||
if (not value.as_object().at(key).is_array())
|
||||
return Error{Status{RippledError::RpcInvalidParams}};
|
||||
|
||||
auto& arr = value.as_object().at(key).as_array();
|
||||
if (idx_ >= arr.size())
|
||||
return Error{Status{RippledError::RpcInvalidParams}};
|
||||
|
||||
auto& res = arr.at(idx_);
|
||||
for (auto const& spec : specs_) {
|
||||
if (auto const ret = spec.process(res); not ret)
|
||||
return Error{ret.error()};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace rpc::meta
|
||||
@@ -1,224 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "rpc/common/Concepts.hpp"
|
||||
#include "rpc/common/Specs.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
|
||||
#include <boost/json/value.hpp>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <initializer_list>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace rpc::meta {
|
||||
|
||||
/**
|
||||
* @brief A meta-processor that acts as a spec for a sub-object/section.
|
||||
*/
|
||||
class Section final {
|
||||
std::vector<FieldSpec> specs_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct new section validator from a list of specs.
|
||||
*
|
||||
* @param specs List of specs @ref FieldSpec
|
||||
*/
|
||||
explicit Section(std::initializer_list<FieldSpec> specs) : specs_{specs}
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Verify that the JSON value representing the section is valid according to the given
|
||||
* specs.
|
||||
*
|
||||
* @param value The JSON value representing the outer object
|
||||
* @param key The key used to retrieve the section from the outer object
|
||||
* @return Possibly an error
|
||||
*/
|
||||
[[nodiscard]] MaybeError
|
||||
verify(boost::json::value& value, std::string_view key) const;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A meta-processor 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:
|
||||
/**
|
||||
* @brief Constructs a processor that validates the specified element of a JSON array.
|
||||
*
|
||||
* @param idx The index inside the array to validate
|
||||
* @param specs The specifications to validate against
|
||||
*/
|
||||
ValidateArrayAt(std::size_t idx, std::initializer_list<FieldSpec> specs)
|
||||
: idx_{idx}, specs_{specs}
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Verify that the JSON array element at given index is valid according the stored specs.
|
||||
*
|
||||
* @param value The JSON value representing the outer object
|
||||
* @param key The key used to retrieve the array from the outer object
|
||||
* @return Possibly an error
|
||||
*/
|
||||
[[nodiscard]] MaybeError
|
||||
verify(boost::json::value& value, std::string_view key) const;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A meta-processor that specifies a list of requirements to run against when the type
|
||||
* matches the template parameter.
|
||||
*/
|
||||
template <typename Type>
|
||||
class IfType final {
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs a validator that validates the specs if the type matches.
|
||||
* @param requirements The requirements to validate against
|
||||
*/
|
||||
template <SomeRequirement... Requirements>
|
||||
explicit IfType(Requirements&&... requirements)
|
||||
: processor_(
|
||||
[... r = std::forward<Requirements>(
|
||||
requirements
|
||||
)](boost::json::value& j, std::string_view key) -> MaybeError {
|
||||
std::optional<Status> firstFailure = std::nullopt;
|
||||
|
||||
// the check logic is the same as fieldspec
|
||||
(
|
||||
[&j, &key, &firstFailure, req = &r]() {
|
||||
if (firstFailure)
|
||||
return;
|
||||
|
||||
if (auto const res = req->verify(j, key); not res)
|
||||
firstFailure = res.error();
|
||||
}(),
|
||||
...);
|
||||
|
||||
if (firstFailure)
|
||||
return Error{*firstFailure};
|
||||
|
||||
return {};
|
||||
}
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
IfType(IfType const&) = default;
|
||||
IfType(IfType&&) = default;
|
||||
|
||||
/**
|
||||
* @brief Verify that the element is valid according to the stored requirements when type
|
||||
* matches.
|
||||
*
|
||||
* @param value The JSON value representing the outer object
|
||||
* @param key The key used to retrieve the element from the outer object
|
||||
* @return Possibly an error
|
||||
*/
|
||||
[[nodiscard]] MaybeError
|
||||
verify(boost::json::value& value, std::string_view key) const
|
||||
{
|
||||
if (not value.is_object() or not value.as_object().contains(key))
|
||||
return {}; // ignore. field does not exist, let 'required' fail instead
|
||||
|
||||
if (not rpc::validation::checkType<Type>(value.as_object().at(key)))
|
||||
return {}; // ignore if type does not match
|
||||
|
||||
return processor_(value, key);
|
||||
}
|
||||
|
||||
private:
|
||||
std::function<MaybeError(boost::json::value&, std::string_view)> processor_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A meta-processor that wraps a validator and produces a custom error in case the wrapped
|
||||
* validator fails.
|
||||
*/
|
||||
template <typename RequirementOrModifierType>
|
||||
requires SomeRequirement<RequirementOrModifierType> or SomeModifier<RequirementOrModifierType>
|
||||
class WithCustomError final {
|
||||
RequirementOrModifierType reqOrModifier_;
|
||||
Status error_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs a validator that calls the given validator `req` and returns a custom error
|
||||
* `err` in case `req` fails.
|
||||
*
|
||||
* @param reqOrModifier The requirement to validate against
|
||||
* @param err The custom error to return in case `req` fails
|
||||
*/
|
||||
WithCustomError(RequirementOrModifierType reqOrModifier, Status err)
|
||||
: reqOrModifier_{std::move(reqOrModifier)}, error_{std::move(err)}
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Runs the stored validator and produces a custom error if the wrapped validator fails.
|
||||
*
|
||||
* @param value The JSON value representing the outer object
|
||||
* @param key The key used to retrieve the element from the outer object
|
||||
* @return Possibly an error
|
||||
*/
|
||||
[[nodiscard]] MaybeError
|
||||
verify(boost::json::value const& value, std::string_view key) const
|
||||
requires SomeRequirement<RequirementOrModifierType>
|
||||
{
|
||||
if (auto const res = reqOrModifier_.verify(value, key); not res)
|
||||
return Error{error_};
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Runs the stored validator and produces a custom error if the wrapped validator fails.
|
||||
* This is an overload for the requirement which can modify the value. Such as IfType.
|
||||
*
|
||||
* @param value The JSON value representing the outer object, this value can be modified by the
|
||||
* requirement inside
|
||||
* @param key The key used to retrieve the element from the outer object
|
||||
* @return Possibly an error
|
||||
*/
|
||||
[[nodiscard]] MaybeError
|
||||
verify(boost::json::value& value, std::string_view key) const
|
||||
requires SomeRequirement<RequirementOrModifierType>
|
||||
{
|
||||
if (auto const res = reqOrModifier_.verify(value, key); not res)
|
||||
return Error{error_};
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Runs the stored modifier and produces a custom error if the wrapped modifier fails.
|
||||
*
|
||||
* @param value The JSON value representing the outer object. This value can be modified by the
|
||||
* modifier.
|
||||
* @param key The key used to retrieve the element from the outer object
|
||||
* @return Possibly an error
|
||||
*/
|
||||
MaybeError
|
||||
modify(boost::json::value& value, std::string_view key) const
|
||||
requires SomeModifier<RequirementOrModifierType>
|
||||
|
||||
{
|
||||
if (auto const res = reqOrModifier_.modify(value, key); not res)
|
||||
return Error{error_};
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace rpc::meta
|
||||
@@ -1,161 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
#include "util/JsonUtils.hpp"
|
||||
|
||||
#include <boost/json/value.hpp>
|
||||
#include <boost/json/value_to.hpp>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
|
||||
#include <concepts>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace rpc::modifiers {
|
||||
|
||||
/**
|
||||
* @brief Clamp value between min and max.
|
||||
*/
|
||||
template <typename Type>
|
||||
class Clamp final {
|
||||
Type min_;
|
||||
Type max_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct the modifier storing min and max values.
|
||||
*
|
||||
* @param min
|
||||
* @param max
|
||||
*/
|
||||
explicit Clamp(Type min, Type max) : min_{min}, max_{max}
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Clamp the value to stored min and max values.
|
||||
*
|
||||
* @param value The JSON value representing the outer object
|
||||
* @param key The key used to retrieve the modified value from the outer object
|
||||
* @return Possibly an error
|
||||
*/
|
||||
[[nodiscard]] MaybeError
|
||||
modify(boost::json::value& value, std::string_view key) const
|
||||
{
|
||||
using boost::json::value_to;
|
||||
|
||||
if (not value.is_object() or not value.as_object().contains(key))
|
||||
return {}; // ignore. field does not exist, let 'required' fail instead
|
||||
|
||||
// clamp to min_ and max_
|
||||
auto const oldValue = value_to<Type>(value.as_object().at(key));
|
||||
value.as_object()[key] = std::clamp<Type>(oldValue, min_, max_);
|
||||
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Convert input string to lower case.
|
||||
*
|
||||
* Note: the conversion is only performed if the input value is a string.
|
||||
*/
|
||||
struct ToLower final {
|
||||
/**
|
||||
* @brief Update the input string to lower case.
|
||||
*
|
||||
* @param value The JSON value representing the outer object
|
||||
* @param key The key used to retrieve the modified value from the outer object
|
||||
* @return Possibly an error
|
||||
*/
|
||||
[[nodiscard]] static MaybeError
|
||||
modify(boost::json::value& value, std::string_view key)
|
||||
{
|
||||
if (not value.is_object() or not value.as_object().contains(key))
|
||||
return {}; // ignore. field does not exist, let 'required' fail instead
|
||||
|
||||
if (not value.as_object().at(key).is_string())
|
||||
return {}; // ignore for non-string types
|
||||
|
||||
value.as_object()[key] =
|
||||
util::toLower(boost::json::value_to<std::string>(value.as_object().at(key)));
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Convert input string to integer.
|
||||
*
|
||||
* Note: the conversion is only performed if the input value is a string.
|
||||
*/
|
||||
struct ToNumber final {
|
||||
/**
|
||||
* @brief Update the input string to integer if it can be converted to integer by stoi.
|
||||
*
|
||||
* @param value The JSON value representing the outer object
|
||||
* @param key The key used to retrieve the modified value from the outer object
|
||||
* @return Possibly an error
|
||||
*/
|
||||
[[nodiscard]] static MaybeError
|
||||
modify(boost::json::value& value, std::string_view key)
|
||||
{
|
||||
if (not value.is_object() or not value.as_object().contains(key))
|
||||
return {}; // ignore. field does not exist, let 'required' fail instead
|
||||
|
||||
if (not value.as_object().at(key).is_string())
|
||||
return {}; // ignore for non-string types
|
||||
|
||||
auto const strInt = boost::json::value_to<std::string>(value.as_object().at(key));
|
||||
if (strInt.contains('.'))
|
||||
return Error{Status{RippledError::RpcInvalidParams}}; // maybe a float
|
||||
|
||||
try {
|
||||
value.as_object()[key] = std::stoi(strInt);
|
||||
} catch (std::exception& e) {
|
||||
return Error{Status{RippledError::RpcInvalidParams}};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Customised modifier allowing user define how to modify input in provided callable.
|
||||
*/
|
||||
class CustomModifier final {
|
||||
std::function<MaybeError(boost::json::value&, std::string_view)> modifier_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs a custom modifier from any supported callable.
|
||||
*
|
||||
* @tparam Fn The type of callable
|
||||
* @param fn The callable/function object
|
||||
*/
|
||||
template <typename Fn>
|
||||
requires std::invocable<Fn, boost::json::value&, std::string_view>
|
||||
explicit CustomModifier(Fn&& fn) : modifier_{std::forward<Fn>(fn)}
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Modify the JSON value according to the custom modifier function stored.
|
||||
*
|
||||
* @param value The JSON value representing the outer object
|
||||
* @param key The key used to retrieve the tested value from the outer object
|
||||
* @return Any compatible user-provided error if modify/verify failed; otherwise no error is
|
||||
* returned
|
||||
*/
|
||||
[[nodiscard]] MaybeError
|
||||
modify(boost::json::value& value, std::string_view key) const
|
||||
{
|
||||
if (not value.is_object() or not value.as_object().contains(key))
|
||||
return {}; // ignore. field does not exist, let 'required' fail instead
|
||||
|
||||
return modifier_(value.as_object().at(key), key);
|
||||
};
|
||||
};
|
||||
|
||||
} // namespace rpc::modifiers
|
||||
@@ -1,63 +0,0 @@
|
||||
#include "rpc/common/Specs.hpp"
|
||||
|
||||
#include "rpc/common/Checkers.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
|
||||
#include <boost/json/array.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
#include <rpcspec/Errors.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace rpc {
|
||||
|
||||
[[nodiscard]] MaybeError
|
||||
FieldSpec::process(boost::json::value& value) const
|
||||
{
|
||||
return processor_(value);
|
||||
}
|
||||
|
||||
[[nodiscard]] check::Warnings
|
||||
FieldSpec::check(boost::json::value const& value) const
|
||||
{
|
||||
return checker_(value);
|
||||
}
|
||||
|
||||
[[nodiscard]] MaybeError
|
||||
RpcSpec::process(boost::json::value& value) const
|
||||
{
|
||||
for (auto const& field : fields_) {
|
||||
if (auto ret = field.process(value); not ret)
|
||||
return Error{ret.error()};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] boost::json::array
|
||||
RpcSpec::check(boost::json::value const& value) const
|
||||
{
|
||||
std::unordered_map<WarningCode, std::vector<std::string>> warnings;
|
||||
for (auto const& field : fields_) {
|
||||
auto fieldWarnings = field.check(value);
|
||||
for (auto& fw : fieldWarnings) {
|
||||
warnings[fw.warningCode].push_back(std::move(fw.extraMessage));
|
||||
}
|
||||
}
|
||||
|
||||
boost::json::array result;
|
||||
for (auto const& [code, messages] : warnings) {
|
||||
auto warningObject = makeWarning(code);
|
||||
auto& warningMessage = warningObject["message"].as_string();
|
||||
for (auto const& message : messages) {
|
||||
warningMessage.append(" ").append(message);
|
||||
}
|
||||
result.push_back(std::move(warningObject));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace rpc
|
||||
@@ -1,132 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "rpc/common/Checkers.hpp"
|
||||
#include "rpc/common/Concepts.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
#include "rpc/common/impl/Factories.hpp"
|
||||
|
||||
#include <boost/json/array.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
|
||||
#include <initializer_list>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace rpc {
|
||||
|
||||
/**
|
||||
* @brief Represents a Specification for one field of an RPC command.
|
||||
*/
|
||||
struct FieldSpec final {
|
||||
/**
|
||||
* @brief Construct a field specification out of a set of processors.
|
||||
*
|
||||
* @tparam Processors The types of processors
|
||||
* @param key The key in a JSON object that the field validates
|
||||
* @param processors The processors, each of them have to fulfil the @ref rpc::SomeProcessor
|
||||
* concept
|
||||
*/
|
||||
template <SomeProcessor... Processors>
|
||||
FieldSpec(std::string const& key, Processors&&... processors)
|
||||
: processor_{
|
||||
impl::makeFieldProcessor<Processors...>(key, std::forward<Processors>(processors)...)
|
||||
}
|
||||
, checker_{impl::kEmptyFieldChecker}
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Construct a field specification out of a set of checkers.
|
||||
*
|
||||
* @tparam Checks The types of checkers
|
||||
* @param key The key in a JSON object that the field validates
|
||||
* @param checks The checks, each of them have to fulfil the @ref rpc::SomeCheck concept
|
||||
*/
|
||||
template <SomeCheck... Checks>
|
||||
FieldSpec(std::string const& key, Checks&&... checks)
|
||||
: processor_{impl::kEmptyFieldProcessor}
|
||||
, checker_{impl::makeFieldChecker<Checks...>(key, std::forward<Checks>(checks)...)}
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Processes the passed JSON value using the stored processors.
|
||||
*
|
||||
* @param value The JSON value to validate and/or modify
|
||||
* @return Nothing on success; Status on error
|
||||
*/
|
||||
[[nodiscard]] MaybeError
|
||||
process(boost::json::value& value) const;
|
||||
|
||||
/**
|
||||
* @brief Checks the passed JSON value using the stored checkers.
|
||||
*
|
||||
* @param value The JSON value to validate
|
||||
* @return A vector of warnings (empty if no warnings)
|
||||
*/
|
||||
[[nodiscard]] check::Warnings
|
||||
check(boost::json::value const& value) const;
|
||||
|
||||
private:
|
||||
impl::FieldSpecProcessor processor_;
|
||||
impl::FieldChecker checker_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @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 {
|
||||
/**
|
||||
* @brief Construct a full RPC request specification.
|
||||
*
|
||||
* @param fields The fields of the RPC specification @ref FieldSpec
|
||||
*/
|
||||
RpcSpec(std::initializer_list<FieldSpec> fields) : fields_{fields}
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Construct a full RPC request specification from another spec and additional fields.
|
||||
*
|
||||
* @param other The other spec to copy fields from
|
||||
* @param additionalFields The additional fields to add to the spec
|
||||
*/
|
||||
RpcSpec(RpcSpec const& other, std::initializer_list<FieldSpec> additionalFields)
|
||||
: fields_{other.fields_}
|
||||
{
|
||||
for (auto& f : additionalFields)
|
||||
fields_.push_back(f);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Processes the passed JSON value using the stored field specs.
|
||||
*
|
||||
* @param value The JSON value to validate and/or modify
|
||||
* @return Nothing on success; Status on error
|
||||
*/
|
||||
[[nodiscard]] MaybeError
|
||||
process(boost::json::value& value) const;
|
||||
|
||||
/**
|
||||
* @brief Checks the passed JSON value using the stored field specs.
|
||||
*
|
||||
* @param value The JSON value to validate
|
||||
* @return JSON array of warnings (empty if no warnings)
|
||||
*/
|
||||
[[nodiscard]] boost::json::array
|
||||
check(boost::json::value const& value) const;
|
||||
|
||||
private:
|
||||
std::vector<FieldSpec> fields_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief An alias for a const reference to @ref RpcSpec.
|
||||
*/
|
||||
using RpcSpecConstRef = RpcSpec const&;
|
||||
|
||||
} // namespace rpc
|
||||
@@ -1,117 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <boost/json/array.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
namespace rpc::validation {
|
||||
namespace impl {
|
||||
|
||||
template <std::unsigned_integral Expected>
|
||||
void
|
||||
clampAs(boost::json::value& value)
|
||||
{
|
||||
if (value.is_uint64()) {
|
||||
auto const valueUint = value.as_uint64();
|
||||
if (valueUint > static_cast<uint64_t>(std::numeric_limits<Expected>::max()))
|
||||
value = std::numeric_limits<Expected>::max();
|
||||
} else if (value.is_int64()) {
|
||||
auto const valueInt = value.as_int64();
|
||||
if (valueInt > static_cast<int64_t>(std::numeric_limits<Expected>::max()))
|
||||
value = std::numeric_limits<Expected>::max();
|
||||
}
|
||||
}
|
||||
|
||||
template <std::signed_integral Expected>
|
||||
void
|
||||
clampAs(boost::json::value& value)
|
||||
{
|
||||
if (value.is_uint64()) {
|
||||
auto const valueUint = value.as_uint64();
|
||||
if (valueUint > static_cast<uint64_t>(std::numeric_limits<Expected>::max()))
|
||||
value = std::numeric_limits<Expected>::max();
|
||||
} else if (value.is_int64()) {
|
||||
auto const valueInt = value.as_int64();
|
||||
if (valueInt > static_cast<int64_t>(std::numeric_limits<Expected>::max())) {
|
||||
value = std::numeric_limits<Expected>::max();
|
||||
} else if (valueInt < static_cast<int64_t>(std::numeric_limits<Expected>::min())) {
|
||||
value = std::numeric_limits<Expected>::min();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace impl
|
||||
|
||||
/**
|
||||
* @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
|
||||
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_same_v<Expected, boost::json::array>) {
|
||||
if (not value.is_array())
|
||||
hasError = true;
|
||||
} else if constexpr (std::is_same_v<Expected, boost::json::object>) {
|
||||
if (not value.is_object())
|
||||
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;
|
||||
// if the type specified is unsigned, it should not be negative
|
||||
if constexpr (std::is_unsigned_v<Expected>) {
|
||||
if (value.is_int64() and value.as_int64() < 0)
|
||||
hasError = true;
|
||||
}
|
||||
}
|
||||
|
||||
return not hasError;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check that the type is the same as what was expected optionally clamping it into range.
|
||||
*
|
||||
* This is used to automatically clamp the value into the range available to the specified type. It
|
||||
* is needed in order to avoid Min, Max and other validators throw "not exact" error from Boost.Json
|
||||
* library if the value does not fit in the specified type.
|
||||
*
|
||||
* @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
|
||||
checkTypeAndClamp(boost::json::value& value)
|
||||
{
|
||||
if (not checkType<Expected>(value))
|
||||
return false; // fails basic type check
|
||||
|
||||
if constexpr (std::is_integral_v<Expected> and not std::is_same_v<Expected, bool>)
|
||||
impl::clampAs<Expected>(value);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace rpc::validation
|
||||
@@ -1,312 +0,0 @@
|
||||
#include "rpc/common/Validators.hpp"
|
||||
|
||||
#include "rpc/JS.hpp"
|
||||
#include "rpc/RPCHelpers.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
#include "util/AccountUtils.hpp"
|
||||
#include "util/TimeUtils.hpp"
|
||||
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
#include <boost/json/value_to.hpp>
|
||||
#include <fmt/format.h>
|
||||
#include <rpcspec/Errors.hpp>
|
||||
#include <rpcspec/LedgerTypes.hpp>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <charconv>
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
|
||||
namespace rpc::validation {
|
||||
|
||||
[[nodiscard]] MaybeError
|
||||
Required::verify(boost::json::value const& value, std::string_view key)
|
||||
{
|
||||
if (not value.is_object() or not value.as_object().contains(key)) {
|
||||
return Error{Status{
|
||||
RippledError::RpcInvalidParams, "Required field '" + std::string{key} + "' missing"
|
||||
}};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] MaybeError
|
||||
TimeFormatValidator::verify(boost::json::value const& value, std::string_view key) const
|
||||
{
|
||||
using boost::json::value_to;
|
||||
|
||||
if (not value.is_object() or not value.as_object().contains(key))
|
||||
return {}; // ignore. field does not exist, let 'required' fail instead
|
||||
|
||||
if (not value.as_object().at(key).is_string())
|
||||
return Error{Status{RippledError::RpcInvalidParams}};
|
||||
|
||||
auto const ret =
|
||||
util::systemTpFromUtcStr(value_to<std::string>(value.as_object().at(key)), format_);
|
||||
if (!ret)
|
||||
return Error{Status{RippledError::RpcInvalidParams}};
|
||||
|
||||
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))
|
||||
return {}; // ignore. field does not exist, let 'required' fail instead
|
||||
|
||||
return validator_(value.as_object().at(key), key);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
checkIsU32Numeric(std::string_view sv)
|
||||
{
|
||||
uint32_t unused = 0;
|
||||
auto [_, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), unused);
|
||||
|
||||
return ec == std::errc();
|
||||
}
|
||||
|
||||
CustomValidator CustomValidators::uint160HexStringValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
return makeHexStringValidator<xrpl::uint160>(value, key);
|
||||
}};
|
||||
|
||||
CustomValidator CustomValidators::uint192HexStringValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
return makeHexStringValidator<xrpl::uint192>(value, key);
|
||||
}};
|
||||
|
||||
CustomValidator CustomValidators::uint256HexStringValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
return makeHexStringValidator<xrpl::uint256>(value, key);
|
||||
}};
|
||||
|
||||
CustomValidator CustomValidators::ledgerIndexValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view /* key */) -> MaybeError {
|
||||
auto err = Error{Status{RippledError::RpcInvalidParams, "ledgerIndexMalformed"}};
|
||||
|
||||
if (!value.is_string() && !(value.is_uint64() || value.is_int64()))
|
||||
return err;
|
||||
|
||||
if (value.is_string() && value.as_string() != "validated" &&
|
||||
!checkIsU32Numeric(boost::json::value_to<std::string>(value)))
|
||||
return err;
|
||||
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
CustomValidator CustomValidators::accountBase58Validator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
if (!value.is_string())
|
||||
return Error{Status{RippledError::RpcInvalidParams, std::string(key) + "NotString"}};
|
||||
|
||||
auto const account =
|
||||
util::parseBase58Wrapper<xrpl::AccountID>(boost::json::value_to<std::string>(value));
|
||||
if (!account || account->isZero())
|
||||
return Error{Status{ClioError::RpcMalformedAddress}};
|
||||
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
CustomValidator CustomValidators::accountMarkerValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
if (!value.is_string())
|
||||
return Error{Status{RippledError::RpcInvalidParams, std::string(key) + "NotString"}};
|
||||
|
||||
// TODO: we are using parseAccountCursor from RPCHelpers, after we
|
||||
// remove all old handler, this function can be moved to here
|
||||
if (!parseAccountCursor(boost::json::value_to<std::string>(value))) {
|
||||
// align with the current error message
|
||||
return Error{Status{RippledError::RpcInvalidParams, "Malformed cursor."}};
|
||||
}
|
||||
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
CustomValidator CustomValidators::accountTypeValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
if (!value.is_string()) {
|
||||
return Error{Status{
|
||||
RippledError::RpcInvalidParams, fmt::format("Invalid field '{}', not string.", key)
|
||||
}};
|
||||
}
|
||||
|
||||
auto const type =
|
||||
rpc::spec::accountOwnedLedgerTypeFromStr(boost::json::value_to<std::string>(value));
|
||||
if (type == xrpl::ltANY) {
|
||||
return Error{
|
||||
Status{RippledError::RpcInvalidParams, fmt::format("Invalid field '{}'.", key)}
|
||||
};
|
||||
}
|
||||
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
CustomValidator CustomValidators::currencyValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
if (!value.is_string())
|
||||
return Error{Status{RippledError::RpcInvalidParams, std::string(key) + "NotString"}};
|
||||
|
||||
auto const currencyStr = boost::json::value_to<std::string>(value);
|
||||
if (currencyStr.empty())
|
||||
return Error{Status{RippledError::RpcInvalidParams, std::string(key) + "IsEmpty"}};
|
||||
|
||||
xrpl::Currency currency;
|
||||
if (!xrpl::toCurrency(currency, currencyStr))
|
||||
return Error{Status{ClioError::RpcMalformedCurrency, "malformedCurrency"}};
|
||||
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
CustomValidator CustomValidators::bookTakerValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
if (!value.is_object())
|
||||
return {}; // type is validated separately by Type<object>
|
||||
|
||||
auto const& obj = value.as_object();
|
||||
bool const hasCurrency = obj.contains("currency");
|
||||
bool const hasMptId = obj.contains("mpt_issuance_id");
|
||||
|
||||
if (!hasCurrency && !hasMptId) {
|
||||
return Error{Status{
|
||||
RippledError::RpcInvalidParams, fmt::format("Missing field '{}.currency'.", key)
|
||||
}};
|
||||
}
|
||||
|
||||
if (hasMptId && (hasCurrency || obj.contains("issuer"))) {
|
||||
return Error{
|
||||
Status{RippledError::RpcInvalidParams, fmt::format("Invalid field '{}'.", key)}
|
||||
};
|
||||
}
|
||||
|
||||
// Wrong type -> invalidParams (xrpld's validateTakerJSON), checked here before the
|
||||
// per-field validators so they can own bad *values* -> dst/srcAmtMalformed.
|
||||
if ((hasCurrency && !obj.at(JS(currency)).is_string()) ||
|
||||
(hasMptId && !obj.at(JS(mpt_issuance_id)).is_string())) {
|
||||
return Error{Status{
|
||||
RippledError::RpcInvalidParams,
|
||||
fmt::format("Invalid field '{}.currency', not string.", key)
|
||||
}};
|
||||
}
|
||||
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
CustomValidator CustomValidators::currencyIssueValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
if (not value.is_object())
|
||||
return Error{Status{RippledError::RpcInvalidParams, std::string(key) + "NotObject"}};
|
||||
|
||||
try {
|
||||
parseIssue(value.as_object());
|
||||
} catch (std::runtime_error const&) {
|
||||
return Error{Status{ClioError::RpcMalformedRequest}};
|
||||
}
|
||||
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
CustomValidator CustomValidators::credentialTypeValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
if (not value.is_string()) {
|
||||
return Error{Status{
|
||||
ClioError::RpcMalformedAuthorizedCredentials, std::string(key) + " NotString"
|
||||
}};
|
||||
}
|
||||
|
||||
auto const& credTypeHex = xrpl::strUnHex(value.as_string());
|
||||
if (!credTypeHex.has_value()) {
|
||||
return Error{Status{
|
||||
ClioError::RpcMalformedAuthorizedCredentials, std::string(key) + " NotHexString"
|
||||
}};
|
||||
}
|
||||
|
||||
if (credTypeHex->empty()) {
|
||||
return Error{
|
||||
Status{ClioError::RpcMalformedAuthorizedCredentials, std::string(key) + " is empty"}
|
||||
};
|
||||
}
|
||||
|
||||
if (credTypeHex->size() > xrpl::kMaxCredentialTypeLength) {
|
||||
return Error{Status{
|
||||
ClioError::RpcMalformedAuthorizedCredentials,
|
||||
std::string(key) + " greater than max length"
|
||||
}};
|
||||
}
|
||||
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
CustomValidator CustomValidators::authorizeCredentialValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
if (not value.is_array())
|
||||
return Error{Status{ClioError::RpcMalformedRequest, std::string(key) + " not array"}};
|
||||
|
||||
auto const& authCred = value.as_array();
|
||||
if (authCred.empty()) {
|
||||
return Error{Status{
|
||||
ClioError::RpcMalformedAuthorizedCredentials,
|
||||
fmt::format("Requires at least one element in authorized_credentials array.")
|
||||
}};
|
||||
}
|
||||
|
||||
if (authCred.size() > xrpl::kMaxCredentialsArraySize) {
|
||||
return Error{Status{
|
||||
ClioError::RpcMalformedAuthorizedCredentials,
|
||||
fmt::format(
|
||||
"Max {} number of credentials in authorized_credentials array",
|
||||
xrpl::kMaxCredentialsArraySize
|
||||
)
|
||||
}};
|
||||
}
|
||||
|
||||
for (auto const& credObj : value.as_array()) {
|
||||
if (!credObj.is_object()) {
|
||||
return Error{Status{
|
||||
ClioError::RpcMalformedAuthorizedCredentials,
|
||||
"authorized_credentials elements in array are not objects."
|
||||
}};
|
||||
}
|
||||
auto const& obj = credObj.as_object();
|
||||
|
||||
if (!obj.contains("issuer")) {
|
||||
return Error{Status{
|
||||
ClioError::RpcMalformedAuthorizedCredentials,
|
||||
"Field 'Issuer' is required but missing."
|
||||
}};
|
||||
}
|
||||
|
||||
// don't want to change issuer error message to be about credentials
|
||||
if (!accountBase58Validator.verify(credObj, "issuer")) {
|
||||
return Error{
|
||||
Status{ClioError::RpcMalformedAuthorizedCredentials, "issuer NotString"}
|
||||
};
|
||||
}
|
||||
|
||||
if (!obj.contains("credential_type")) {
|
||||
return Error{Status{
|
||||
ClioError::RpcMalformedAuthorizedCredentials,
|
||||
"Field 'CredentialType' is required but missing."
|
||||
}};
|
||||
}
|
||||
|
||||
if (auto const err = credentialTypeValidator.verify(credObj, "credential_type"); !err)
|
||||
return err;
|
||||
}
|
||||
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
} // namespace rpc::validation
|
||||
@@ -1,615 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
#include "rpc/common/ValidationHelpers.hpp"
|
||||
|
||||
#include <boost/json/array.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
#include <fmt/format.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
|
||||
#include <concepts>
|
||||
#include <ctime>
|
||||
#include <functional>
|
||||
#include <initializer_list>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace rpc::validation {
|
||||
|
||||
/**
|
||||
* @brief A validator that simply requires a field to be present.
|
||||
*/
|
||||
struct Required final {
|
||||
/**
|
||||
* @brief Verify that the JSON value is present and not null.
|
||||
*
|
||||
* @param value The JSON value representing the outer object
|
||||
* @param key The key used to retrieve the tested value from the outer object
|
||||
* @return An error if validation failed; otherwise no error is returned
|
||||
*/
|
||||
[[nodiscard]] static MaybeError
|
||||
verify(boost::json::value const& value, std::string_view key);
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A validator that forbids a field to be present.
|
||||
*
|
||||
* If there is a value provided, it will forbid the field only when the value equals.
|
||||
* If there is no value provided, it will forbid the field when the field shows up.
|
||||
*/
|
||||
template <typename... T>
|
||||
class NotSupported;
|
||||
|
||||
/**
|
||||
* @brief A specialized NotSupported validator that forbids a field to be present when the value
|
||||
* equals the given value.
|
||||
*/
|
||||
template <typename T>
|
||||
class NotSupported<T> final {
|
||||
T value_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs a new NotSupported validator.
|
||||
*
|
||||
* @param val The value to store and verify against
|
||||
*/
|
||||
NotSupported(T val) : value_(val)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Verify whether the field is supported or not.
|
||||
*
|
||||
* @param value The JSON value representing the outer object
|
||||
* @param key The key used to retrieve the tested value from the outer object
|
||||
* @return `RippledError::RpcNotSupported` if the value matched; otherwise no error is returned
|
||||
*/
|
||||
[[nodiscard]] MaybeError
|
||||
verify(boost::json::value const& value, std::string_view key) const
|
||||
{
|
||||
if (value.is_object() and value.as_object().contains(key)) {
|
||||
using boost::json::value_to;
|
||||
auto const res = value_to<T>(value.as_object().at(key));
|
||||
if (value_ == res) {
|
||||
return Error{Status{
|
||||
RippledError::RpcNotSupported,
|
||||
fmt::format("Not supported field '{}'s value '{}'", std::string{key}, res)
|
||||
}};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A specialized NotSupported validator that forbids a field to be present.
|
||||
*/
|
||||
template <>
|
||||
class NotSupported<> final {
|
||||
public:
|
||||
/**
|
||||
* @brief Verify whether the field is supported or not.
|
||||
*
|
||||
* @param value The JSON value representing the outer object
|
||||
* @param key The key used to retrieve the tested value from the outer object
|
||||
* @return `RippledError::RpcNotSupported` if the field is found; otherwise no error is
|
||||
* returned
|
||||
*/
|
||||
[[nodiscard]] static MaybeError
|
||||
verify(boost::json::value const& value, std::string_view key)
|
||||
{
|
||||
if (value.is_object() and value.as_object().contains(key)) {
|
||||
return Error{Status{
|
||||
RippledError::RpcNotSupported, "Not supported field '" + std::string{key} + '\''
|
||||
}};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Deduction guide to avoid having to specify the template arguments.
|
||||
*/
|
||||
template <typename... T>
|
||||
NotSupported(T&&... t) -> NotSupported<T...>;
|
||||
|
||||
/**
|
||||
* @brief Validates that the type of the value is one of the given types.
|
||||
*/
|
||||
template <typename... Types>
|
||||
struct Type final {
|
||||
/**
|
||||
* @brief Verify that the JSON value is (one) of specified type(s).
|
||||
* @note The value itself can only change for integral types and only if the value is outside of
|
||||
* the range of the expected integer type (see checkTypeAndClamp).
|
||||
*
|
||||
* @param value The JSON value representing the outer object
|
||||
* @param key The key used to retrieve the tested value from the outer object
|
||||
* @return `RippledError::RpcInvalidParams` if validation failed; otherwise no error is
|
||||
* returned
|
||||
*/
|
||||
[[nodiscard]] MaybeError
|
||||
verify(boost::json::value& value, std::string_view key) const
|
||||
{
|
||||
if (not value.is_object() or not value.as_object().contains(key))
|
||||
return {}; // ignore. If field is supposed to exist, let 'required' fail instead
|
||||
|
||||
auto& res = value.as_object().at(key);
|
||||
auto const convertible = (checkTypeAndClamp<Types>(res) || ...);
|
||||
|
||||
if (not convertible)
|
||||
return Error{Status{RippledError::RpcInvalidParams}};
|
||||
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Validate that value is between specified min and max.
|
||||
*/
|
||||
template <typename Type>
|
||||
class Between final {
|
||||
Type min_;
|
||||
Type max_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct the validator storing min and max values.
|
||||
*
|
||||
* @param min
|
||||
* @param max
|
||||
*/
|
||||
explicit Between(Type min, Type max) : min_{min}, max_{max}
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Verify that the JSON value is within a certain range.
|
||||
*
|
||||
* @param value The JSON value representing the outer object
|
||||
* @param key The key used to retrieve the tested value from the outer object
|
||||
* @return `RippledError::RpcInvalidParams` if validation failed; otherwise no error is
|
||||
* returned
|
||||
*/
|
||||
[[nodiscard]] MaybeError
|
||||
verify(boost::json::value const& value, std::string_view key) const
|
||||
{
|
||||
using boost::json::value_to;
|
||||
|
||||
if (not value.is_object() or not value.as_object().contains(key))
|
||||
return {}; // ignore. field does not exist, let 'required' fail instead
|
||||
|
||||
auto const res = value_to<Type>(value.as_object().at(key));
|
||||
|
||||
// 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{Status{RippledError::RpcInvalidParams}};
|
||||
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Validate that value is equal or greater than the specified min.
|
||||
*/
|
||||
template <typename Type>
|
||||
class Min final {
|
||||
Type min_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct the validator storing min value.
|
||||
*
|
||||
* @param min
|
||||
*/
|
||||
explicit Min(Type min) : min_{min}
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Verify that the JSON value is not smaller than min
|
||||
*
|
||||
* @param value The JSON value representing the outer object
|
||||
* @param key The key used to retrieve the tested value from the outer object
|
||||
* @return `RippledError::RpcInvalidParams` if validation failed; otherwise no error is
|
||||
* returned
|
||||
*/
|
||||
[[nodiscard]] MaybeError
|
||||
verify(boost::json::value const& value, std::string_view key) const
|
||||
{
|
||||
using boost::json::value_to;
|
||||
|
||||
if (not value.is_object() or not value.as_object().contains(key))
|
||||
return {}; // ignore. field does not exist, let 'required' fail instead
|
||||
|
||||
auto const res = value_to<Type>(value.as_object().at(key));
|
||||
|
||||
if (res < min_)
|
||||
return Error{Status{RippledError::RpcInvalidParams}};
|
||||
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Validate that value is not greater than max.
|
||||
*/
|
||||
template <typename Type>
|
||||
class Max final {
|
||||
Type max_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct the validator storing max value.
|
||||
*
|
||||
* @param max
|
||||
*/
|
||||
explicit Max(Type max) : max_{max}
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Verify that the JSON value is not greater than max.
|
||||
*
|
||||
* @param value The JSON value representing the outer object
|
||||
* @param key The key used to retrieve the tested value from the outer object
|
||||
* @return `RippledError::RpcInvalidParams` if validation failed; otherwise no error is
|
||||
* returned
|
||||
*/
|
||||
[[nodiscard]] MaybeError
|
||||
verify(boost::json::value const& value, std::string_view key) const
|
||||
{
|
||||
using boost::json::value_to;
|
||||
|
||||
if (not value.is_object() or not value.as_object().contains(key))
|
||||
return {}; // ignore. field does not exist, let 'required' fail instead
|
||||
|
||||
auto const res = value_to<Type>(value.as_object().at(key));
|
||||
|
||||
if (res > max_)
|
||||
return Error{Status{RippledError::RpcInvalidParams}};
|
||||
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Validate that value can be converted to time according to the given format.
|
||||
*/
|
||||
class TimeFormatValidator final {
|
||||
std::string format_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct the validator storing format value.
|
||||
*
|
||||
* @param format The format to use for time conversion
|
||||
*/
|
||||
explicit TimeFormatValidator(std::string format) : format_{std::move(format)}
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Verify that the JSON value is valid formatted time.
|
||||
*
|
||||
* @param value The JSON value representing the outer object
|
||||
* @param key The key used to retrieve the tested value from the outer object
|
||||
* @return `RippledError::RpcInvalidParams` if validation failed; otherwise no error is
|
||||
* returned
|
||||
*/
|
||||
[[nodiscard]] MaybeError
|
||||
verify(boost::json::value const& value, std::string_view key) const;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Validates that the value is equal to the one passed in.
|
||||
*/
|
||||
template <typename Type>
|
||||
class EqualTo final {
|
||||
Type original_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct the validator with stored original value.
|
||||
*
|
||||
* @param original The original value to store
|
||||
*/
|
||||
explicit EqualTo(Type original) : original_{std::move(original)}
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Verify that the JSON value is equal to the stored original.
|
||||
*
|
||||
* @param value The JSON value representing the outer object
|
||||
* @param key The key used to retrieve the tested value from the outer object
|
||||
* @return `RippledError::RpcInvalidParams` if validation failed; otherwise no error is
|
||||
* returned
|
||||
*/
|
||||
[[nodiscard]] MaybeError
|
||||
verify(boost::json::value const& value, std::string_view key) const
|
||||
{
|
||||
using boost::json::value_to;
|
||||
|
||||
if (not value.is_object() or not value.as_object().contains(key))
|
||||
return {}; // ignore. field does not exist, let 'required' fail instead
|
||||
|
||||
auto const res = value_to<Type>(value.as_object().at(key));
|
||||
if (res != original_)
|
||||
return Error{Status{RippledError::RpcInvalidParams}};
|
||||
|
||||
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:
|
||||
/**
|
||||
* @brief Construct the validator with stored options of initializer list.
|
||||
*
|
||||
* @param options The list of allowed options
|
||||
*/
|
||||
explicit OneOf(std::initializer_list<Type> options) : options_{options}
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Construct the validator with stored options of other container.
|
||||
*
|
||||
* @param begin,end the range to copy the elements from
|
||||
*/
|
||||
template <typename InputIt>
|
||||
explicit OneOf(InputIt begin, InputIt end) : options_{begin, end}
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Verify that the JSON value is one of the stored options.
|
||||
*
|
||||
* @param value The JSON value representing the outer object
|
||||
* @param key The key used to retrieve the tested value from the outer object
|
||||
* @return `RippledError::RpcInvalidParams` if validation failed; otherwise no error is
|
||||
* returned
|
||||
*/
|
||||
[[nodiscard]] MaybeError
|
||||
verify(boost::json::value const& value, std::string_view key) const
|
||||
{
|
||||
using boost::json::value_to;
|
||||
|
||||
if (not value.is_object() or not value.as_object().contains(key))
|
||||
return {}; // ignore. field does not exist, let 'required' fail instead
|
||||
|
||||
auto const res = value_to<Type>(value.as_object().at(key));
|
||||
if (std::find(std::begin(options_), std::end(options_), res) == std::end(options_)) {
|
||||
return Error{
|
||||
Status{RippledError::RpcInvalidParams, fmt::format("Invalid field '{}'.", key)}
|
||||
};
|
||||
}
|
||||
|
||||
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 allows to specify a custom validation function.
|
||||
*/
|
||||
class CustomValidator final {
|
||||
std::function<MaybeError(boost::json::value const&, std::string_view)> validator_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs a custom validator from any supported callable.
|
||||
*
|
||||
* @tparam Fn The type of callable
|
||||
* @param fn The callable/function object
|
||||
*/
|
||||
template <typename Fn>
|
||||
requires std::invocable<Fn, boost::json::value const&, std::string_view>
|
||||
explicit CustomValidator(Fn&& fn) : validator_{std::forward<Fn>(fn)}
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Verify that the JSON value is valid according to the custom validation function
|
||||
* stored.
|
||||
*
|
||||
* @param value The JSON value representing the outer object
|
||||
* @param key The key used to retrieve the tested value from the outer object
|
||||
* @return Any compatible user-provided error if validation failed; otherwise no error is
|
||||
* returned
|
||||
*/
|
||||
[[nodiscard]] MaybeError
|
||||
verify(boost::json::value const& value, std::string_view key) const;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Helper function to check if input value is an uint32 number or not.
|
||||
*
|
||||
* @param sv The input value as a string_view
|
||||
* @return true if the string can be converted to a uint32; false otherwise
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
checkIsU32Numeric(std::string_view sv);
|
||||
|
||||
template <class HexType>
|
||||
requires(
|
||||
std::is_same_v<HexType, xrpl::uint160> || std::is_same_v<HexType, xrpl::uint192> ||
|
||||
std::is_same_v<HexType, xrpl::uint256>
|
||||
)
|
||||
MaybeError
|
||||
makeHexStringValidator(boost::json::value const& value, std::string_view key)
|
||||
{
|
||||
if (!value.is_string())
|
||||
return Error{Status{RippledError::RpcInvalidParams, std::string(key) + "NotString"}};
|
||||
|
||||
HexType parsedInt;
|
||||
if (!parsedInt.parseHex(value.as_string().c_str()))
|
||||
return Error{Status{RippledError::RpcInvalidParams, std::string(key) + "Malformed"}};
|
||||
|
||||
return MaybeError{};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A group of custom validation functions
|
||||
*/
|
||||
struct CustomValidators final {
|
||||
/**
|
||||
* @brief Provides a commonly used validator for ledger index.
|
||||
*
|
||||
* LedgerIndex must be a string or an int. If the specified LedgerIndex is a string, its value
|
||||
* must be either "validated" or a valid integer value represented as a string.
|
||||
*/
|
||||
static CustomValidator ledgerIndexValidator;
|
||||
|
||||
/**
|
||||
* @brief Provides a commonly used validator for accounts.
|
||||
*
|
||||
* Account must be a string and can convert to base58.
|
||||
*/
|
||||
static CustomValidator accountBase58Validator;
|
||||
|
||||
/**
|
||||
* @brief Provides a commonly used validator for markers.
|
||||
*
|
||||
* A marker is composed of a comma-separated index and a start hint.
|
||||
* The former will be read as hex, and the latter can be cast to uint64.
|
||||
*/
|
||||
static CustomValidator accountMarkerValidator;
|
||||
|
||||
/**
|
||||
* @brief Provides a validator for account type.
|
||||
*
|
||||
* A type accepts canonical names of owned ledger entry types (case insensitive) or short names.
|
||||
* Used by account_objects.
|
||||
*/
|
||||
static CustomValidator accountTypeValidator;
|
||||
|
||||
/**
|
||||
* @brief Provides a commonly used validator for uint160(AccountID) hex string.
|
||||
*
|
||||
* It must be a string and also a decodable hex.
|
||||
* AccountID uses this validator.
|
||||
*/
|
||||
static CustomValidator uint160HexStringValidator;
|
||||
|
||||
/**
|
||||
* @brief Provides a commonly used validator for uint192 hex string.
|
||||
*
|
||||
* It must be a string and also a decodable hex.
|
||||
* MPTIssuanceID uses this validator.
|
||||
*/
|
||||
static CustomValidator uint192HexStringValidator;
|
||||
|
||||
/**
|
||||
* @brief Provides a commonly used validator for uint256 hex string.
|
||||
*
|
||||
* It must be a string and also a decodable hex.
|
||||
* Transaction index, ledger hash all use this validator.
|
||||
*/
|
||||
static CustomValidator uint256HexStringValidator;
|
||||
|
||||
/**
|
||||
* @brief Provides a commonly used validator for currency, including standard currency code and
|
||||
* token code.
|
||||
*/
|
||||
static CustomValidator currencyValidator;
|
||||
|
||||
/**
|
||||
* @brief Validates an asset (xrpl::Issue).
|
||||
*
|
||||
* Used by amm_info.
|
||||
*/
|
||||
static CustomValidator currencyIssueValidator;
|
||||
|
||||
/**
|
||||
* @brief Validates a book taker object (`taker_gets`/`taker_pays`).
|
||||
*
|
||||
* The object must specify an asset as either a `currency` (optionally with an `issuer`) or an
|
||||
* `mpt_issuance_id`, but not both, and `mpt_issuance_id` must not be combined with `issuer`.
|
||||
* Mirrors `xrpld`'s `validateTakerJSON`: a missing asset yields `Missing field
|
||||
* '<field>.currency'.`, conflicting fields yield `Invalid field '<field>'.`, and a present but
|
||||
* non-string `currency`/`mpt_issuance_id` yields `Invalid field '<field>.currency', not
|
||||
* string.` (all `invalidParams`). The field name is taken from the validated key.
|
||||
*
|
||||
* Used by book_offers.
|
||||
*/
|
||||
static CustomValidator bookTakerValidator;
|
||||
|
||||
/**
|
||||
* @brief Provides a validator for validating authorized_credentials json array.
|
||||
*
|
||||
* Used by deposit_preauth.
|
||||
*/
|
||||
static CustomValidator authorizeCredentialValidator;
|
||||
|
||||
/**
|
||||
* @brief Provides a validator for validating credential_type.
|
||||
*
|
||||
* Used by AuthorizeCredentialValidator in deposit_preauth and by the credential
|
||||
* object lookup in ledger_entry.
|
||||
*/
|
||||
static CustomValidator credentialTypeValidator;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Validates that the elements of the array is of type Hex256 uint
|
||||
*/
|
||||
struct Hex256ItemType final {
|
||||
/**
|
||||
* @brief Validates given the prerequisite that the type of the json value is an array,
|
||||
* verifies all values within the array is of uint256 hash
|
||||
*
|
||||
* @param value the value to verify
|
||||
* @param key The key used to retrieve the tested value from the outer object
|
||||
* @return `RippledError::RpcInvalidParams` if validation failed; otherwise no error is
|
||||
* returned
|
||||
*/
|
||||
[[nodiscard]] static MaybeError
|
||||
verify(boost::json::value const& value, std::string_view key)
|
||||
{
|
||||
if (not value.is_object() or not value.as_object().contains(key))
|
||||
return {}; // ignore. If field is supposed to exist, let 'required' fail instead
|
||||
|
||||
auto const& res = value.as_object().at(key);
|
||||
|
||||
// loop through each item in the array and make sure it is uint256 hex string
|
||||
for (auto const& elem : res.as_array()) {
|
||||
xrpl::uint256 num;
|
||||
if (!elem.is_string() || !num.parseHex(elem.as_string())) {
|
||||
return Error{
|
||||
Status{RippledError::RpcInvalidParams, "Item is not a valid uint256 type."}
|
||||
};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace rpc::validation
|
||||
@@ -1,86 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "rpc/common/Checkers.hpp"
|
||||
#include "rpc/common/Concepts.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
#include "util/UnsupportedType.hpp"
|
||||
|
||||
#include <boost/json/array.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
|
||||
#include <expected>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace rpc::impl {
|
||||
|
||||
using FieldSpecProcessor = std::function<MaybeError(boost::json::value&)>;
|
||||
|
||||
static FieldSpecProcessor const kEmptyFieldProcessor = [](boost::json::value&) -> MaybeError {
|
||||
return {};
|
||||
};
|
||||
|
||||
template <SomeProcessor... Processors>
|
||||
[[nodiscard]] FieldSpecProcessor
|
||||
makeFieldProcessor(std::string const& key, Processors&&... procs)
|
||||
{
|
||||
return [key, ... proc = std::forward<Processors>(procs)](boost::json::value& j) -> MaybeError {
|
||||
std::optional<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 = &proc]() {
|
||||
if (firstFailure)
|
||||
return; // already failed earlier - skip
|
||||
|
||||
if constexpr (SomeRequirement<decltype(*req)>) {
|
||||
if (auto const res = req->verify(j, key); not res)
|
||||
firstFailure = res.error();
|
||||
} else if constexpr (SomeModifier<decltype(*req)>) {
|
||||
if (auto const res = req->modify(j, key); not res)
|
||||
firstFailure = res.error();
|
||||
} else {
|
||||
static_assert(util::Unsupported<decltype(*req)>);
|
||||
}
|
||||
}(),
|
||||
...);
|
||||
|
||||
if (firstFailure)
|
||||
return std::unexpected{std::move(firstFailure).value()};
|
||||
|
||||
return {};
|
||||
};
|
||||
}
|
||||
|
||||
using FieldChecker = std::function<check::Warnings(boost::json::value const&)>;
|
||||
|
||||
static FieldChecker const kEmptyFieldChecker = [](boost::json::value const&) -> check::Warnings {
|
||||
return {};
|
||||
};
|
||||
|
||||
template <SomeCheck... Checks>
|
||||
[[nodiscard]] FieldChecker
|
||||
makeFieldChecker(std::string const& key, Checks&&... checks)
|
||||
{
|
||||
return [key,
|
||||
... checks =
|
||||
std::forward<Checks>(checks)](boost::json::value const& j) -> check::Warnings {
|
||||
check::Warnings warnings;
|
||||
// This expands in order of Checks and collects all warnings into a WarningsCollection
|
||||
(
|
||||
[&j, &key, &warnings, req = &checks]() {
|
||||
if (auto res = req->check(j, key); res)
|
||||
warnings.push_back(*std::move(res));
|
||||
}(),
|
||||
...);
|
||||
return warnings;
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace rpc::impl
|
||||
@@ -20,20 +20,12 @@ struct DefaultProcessor final {
|
||||
) const
|
||||
{
|
||||
using boost::json::value_from;
|
||||
using boost::json::value_to;
|
||||
|
||||
static_assert(
|
||||
kIsSingleInputPath<HandlerType>,
|
||||
"handler satisfies both the legacy and the typed input path; dispatch would be "
|
||||
"decided by the order of the branches below rather than by the handler"
|
||||
);
|
||||
static_assert(
|
||||
SomeHandlerWithTypedInput<HandlerType> or SomeHandlerWithInput<HandlerType> or
|
||||
SomeHandlerWithoutInput<HandlerType>,
|
||||
SomeHandlerWithTypedInput<HandlerType> or SomeHandlerWithoutInput<HandlerType>,
|
||||
"handler matches none of the branches below"
|
||||
);
|
||||
|
||||
// New `rpc-spec`-based handler
|
||||
if constexpr (SomeHandlerWithTypedInput<HandlerType>) {
|
||||
auto input = HandlerType::parseInput(value, ctx.apiVersion);
|
||||
auto warnings = rpc::spec::toJsonArray(HandlerType::spec(ctx.apiVersion).check(value));
|
||||
@@ -49,26 +41,6 @@ struct DefaultProcessor final {
|
||||
return ReturnType{value_from(std::move(ret).value()), std::move(warnings)};
|
||||
}
|
||||
|
||||
if constexpr (SomeHandlerWithInput<HandlerType>) {
|
||||
// Old spec-based handler: first we run validation against specified API version
|
||||
// TODO: This will be eventually removed once fully migraded to new rpc-spec system.
|
||||
auto const spec = handler.spec(ctx.apiVersion);
|
||||
auto warnings = spec.check(value);
|
||||
auto input = value; // copy here, spec require mutable data
|
||||
|
||||
if (auto const ret = spec.process(input); not ret.has_value())
|
||||
return ReturnType{Error{ret.error()}, std::move(warnings)}; // forward Status
|
||||
|
||||
auto const inData = value_to<typename HandlerType::Input>(input);
|
||||
auto ret = handler.process(inData, ctx);
|
||||
|
||||
// real handler is given expected Input, not json
|
||||
if (not ret.has_value())
|
||||
return ReturnType{Error{std::move(ret).error()}, std::move(warnings)};
|
||||
|
||||
return ReturnType{value_from(std::move(ret).value()), std::move(warnings)};
|
||||
}
|
||||
|
||||
if constexpr (SomeHandlerWithoutInput<HandlerType>) {
|
||||
// no input to pass, ignore the value
|
||||
auto const ret = handler.process(ctx);
|
||||
|
||||
@@ -119,13 +119,12 @@ constexpr auto kHexLocators = std::to_array<HexLocator>({
|
||||
LocatorOrStatus
|
||||
directoryLocator(le::DirectoryEntry const& entry)
|
||||
{
|
||||
if (entry.dirRoot.has_value() && entry.owner.has_value()) {
|
||||
return std::unexpected{
|
||||
Status{RippledError::RpcInvalidParams, "mayNotSpecifyBothDirRootAndOwner"}
|
||||
};
|
||||
if (entry.dirRoot.has_value() == entry.owner.has_value()) {
|
||||
return std::unexpected{Status{
|
||||
RippledError::RpcInvalidParams,
|
||||
"Must have exactly one of `owner` and `dir_root` fields."
|
||||
}};
|
||||
}
|
||||
if (not entry.dirRoot.has_value() and not entry.owner.has_value())
|
||||
return std::unexpected{Status{RippledError::RpcInvalidParams, "missingOwnerOrDirRoot"}};
|
||||
|
||||
auto const subIndex = entry.subIndex.value_or(0);
|
||||
if (entry.dirRoot.has_value())
|
||||
@@ -140,7 +139,8 @@ depositPreauthLocator(le::DepositPreauthEntry const& entry)
|
||||
// Exactly one of authorized or authorized_credentials MUST exist.
|
||||
if (entry.authorized.has_value() == entry.authorizedCredentials.has_value()) {
|
||||
return std::unexpected{Status{
|
||||
ClioError::RpcMalformedRequest, "Must have one of authorized or authorized_credentials."
|
||||
ClioError::RpcMalformedRequest,
|
||||
"Must have exactly one of `authorized` and `authorized_credentials`."
|
||||
}};
|
||||
}
|
||||
|
||||
|
||||
@@ -58,9 +58,14 @@ VaultInfoHandler::VaultInfoHandler(std::shared_ptr<BackendInterface> sharedPtrBa
|
||||
VaultInfoHandler::Result
|
||||
VaultInfoHandler::process(VaultInfoHandler::Input const& input, Context const& ctx) const
|
||||
{
|
||||
// vault info input must either have owner and sequence, or vault_id only.
|
||||
if (not validate(input))
|
||||
return Error{ClioError::RpcMalformedRequest};
|
||||
// vault info input must either have owner and sequence, or vault_id only. Wording and code
|
||||
// match xrpld's VaultInfo.cpp parseVault().
|
||||
if (not validate(input)) {
|
||||
return Error{Status{
|
||||
RippledError::RpcInvalidParams,
|
||||
"Must specify either 'vault_id' or both 'owner' and 'seq'."
|
||||
}};
|
||||
}
|
||||
|
||||
auto const range = sharedPtrBackend_->fetchLedgerRange();
|
||||
ASSERT(range.has_value(), "VaultInfo's ledger range must be available");
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "rpc/common/Specs.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
#include "rpc/common/Validators.hpp"
|
||||
|
||||
#include <boost/json/conversion.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
@@ -24,30 +22,11 @@
|
||||
|
||||
namespace tests::common {
|
||||
|
||||
// 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 {
|
||||
.hello = boost::json::value_to<std::string>(jv.as_object().at("hello")), .limit = optLimit
|
||||
};
|
||||
}
|
||||
|
||||
// must be implemented as per rpc/common/Concepts.h
|
||||
inline void
|
||||
tag_invoke(boost::json::value_from_tag, boost::json::value& jv, TestOutput const& output)
|
||||
@@ -55,33 +34,6 @@ tag_invoke(boost::json::value_from_tag, boost::json::value& jv, TestOutput const
|
||||
jv = {{"computed", output.computed}};
|
||||
}
|
||||
|
||||
// example handler
|
||||
class HandlerFake {
|
||||
public:
|
||||
using Input = TestInput;
|
||||
using Output = TestOutput;
|
||||
using Result = rpc::HandlerReturnType<Output>;
|
||||
|
||||
static rpc::RpcSpecConstRef
|
||||
spec([[maybe_unused]] uint32_t apiVersion)
|
||||
{
|
||||
using namespace rpc::validation;
|
||||
|
||||
static auto const kRpcSpec = rpc::RpcSpec{
|
||||
{"hello", Required{}, Type<std::string>{}, EqualTo{"world"}},
|
||||
{"limit", Type<uint32_t>{}, Between<uint32_t>{0, 100}}, // optional field
|
||||
};
|
||||
|
||||
return kRpcSpec;
|
||||
}
|
||||
|
||||
static Result
|
||||
process(Input input, [[maybe_unused]] rpc::Context const& ctx)
|
||||
{
|
||||
return Output{input.hello + '_' + std::to_string(input.limit.value_or(0))};
|
||||
}
|
||||
};
|
||||
|
||||
class NoInputHandlerFake {
|
||||
public:
|
||||
using Output = TestOutput;
|
||||
@@ -94,34 +46,6 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
// example handler that returns custom error
|
||||
class FailingHandlerFake {
|
||||
public:
|
||||
using Input = TestInput;
|
||||
using Output = TestOutput;
|
||||
using Result = rpc::HandlerReturnType<Output>;
|
||||
|
||||
static rpc::RpcSpecConstRef
|
||||
spec([[maybe_unused]] uint32_t apiVersion)
|
||||
{
|
||||
using namespace rpc::validation;
|
||||
|
||||
static auto const kRpcSpec = rpc::RpcSpec{
|
||||
{"hello", Required{}, Type<std::string>{}, EqualTo{"world"}},
|
||||
{"limit", Type<uint32_t>{}, Between<uint32_t>{0u, 100u}}, // optional field
|
||||
};
|
||||
|
||||
return kRpcSpec;
|
||||
}
|
||||
|
||||
static Result
|
||||
process([[maybe_unused]] Input input, [[maybe_unused]] rpc::Context const& ctx)
|
||||
{
|
||||
// always fail
|
||||
return rpc::Error{rpc::Status{"Very custom error"}};
|
||||
}
|
||||
};
|
||||
|
||||
struct InOutFake {
|
||||
std::string something;
|
||||
|
||||
@@ -144,15 +68,6 @@ tag_invoke(boost::json::value_from_tag, boost::json::value& jv, InOutFake const&
|
||||
jv = {{"something", output.something}};
|
||||
}
|
||||
|
||||
struct HandlerMock {
|
||||
using Input = InOutFake;
|
||||
using Output = InOutFake;
|
||||
using Result = rpc::HandlerReturnType<Output>;
|
||||
|
||||
MOCK_METHOD(rpc::RpcSpecConstRef, spec, (uint32_t), (const));
|
||||
MOCK_METHOD(Result, process, (Input, rpc::Context const&), (const));
|
||||
};
|
||||
|
||||
struct HandlerWithoutInputMock {
|
||||
using Output = InOutFake;
|
||||
using Result = rpc::HandlerReturnType<Output>;
|
||||
@@ -164,15 +79,26 @@ struct HandlerWithoutInputMock {
|
||||
// `specFor` hook, so the fake Input below needs its own namespace to host that hook.
|
||||
namespace typed_fake {
|
||||
|
||||
// input data for TypedHandlerFake; mirrors TestInput so the two paths stay comparable
|
||||
// input data for TypedHandlerFake
|
||||
struct TypedInput {
|
||||
std::string hello;
|
||||
std::optional<uint32_t> limit;
|
||||
};
|
||||
|
||||
inline constexpr auto kInputSpec = rpc::spec::spec<TypedInput>(
|
||||
rpc::spec::field("hello", &TypedInput::hello, rpc::spec::required, rpc::spec::asString),
|
||||
rpc::spec::field("limit", &TypedInput::limit, rpc::spec::asUint32),
|
||||
rpc::spec::field(
|
||||
"hello",
|
||||
&TypedInput::hello,
|
||||
rpc::spec::required,
|
||||
rpc::spec::oneOf("world"),
|
||||
rpc::spec::asString
|
||||
),
|
||||
rpc::spec::field(
|
||||
"limit",
|
||||
&TypedInput::limit,
|
||||
rpc::spec::between(uint32_t{0}, uint32_t{100}),
|
||||
rpc::spec::asUint32
|
||||
),
|
||||
rpc::spec::field("old_field", rpc::spec::deprecated)
|
||||
);
|
||||
|
||||
|
||||
@@ -94,12 +94,9 @@ target_sources(
|
||||
migration/SpecTests.cpp
|
||||
# RPC
|
||||
rpc/APIVersionTests.cpp
|
||||
rpc/BaseTests.cpp
|
||||
rpc/CountersTests.cpp
|
||||
rpc/ErrorTests.cpp
|
||||
rpc/ForwardingProxyTests.cpp
|
||||
rpc/common/CheckersTests.cpp
|
||||
rpc/common/SpecsTests.cpp
|
||||
rpc/common/TypesTests.cpp
|
||||
rpc/common/impl/HandlerProviderTests.cpp
|
||||
rpc/filters/impl/DelegateTransactionsFilterTests.cpp
|
||||
@@ -146,7 +143,6 @@ target_sources(
|
||||
rpc/handlers/UnsubscribeTests.cpp
|
||||
rpc/handlers/VersionHandlerTests.cpp
|
||||
rpc/handlers/VaultInfoTests.cpp
|
||||
rpc/JsonBoolTests.cpp
|
||||
rpc/RPCEngineTests.cpp
|
||||
rpc/RPCHelpersTests.cpp
|
||||
rpc/WorkQueueTests.cpp
|
||||
|
||||
@@ -1,838 +0,0 @@
|
||||
#include "rpc/common/MetaProcessors.hpp"
|
||||
#include "rpc/common/Modifiers.hpp"
|
||||
#include "rpc/common/Specs.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
#include "rpc/common/ValidationHelpers.hpp"
|
||||
#include "rpc/common/Validators.hpp"
|
||||
|
||||
#include <boost/json/array.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
#include <fmt/format.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <rpcspec/Errors.hpp>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
using namespace std;
|
||||
|
||||
using namespace rpc;
|
||||
using namespace rpc::validation;
|
||||
using namespace rpc::meta;
|
||||
using namespace rpc::modifiers;
|
||||
|
||||
class RPCBaseTest : public virtual ::testing::Test {};
|
||||
|
||||
TEST_F(RPCBaseTest, CheckTypeString)
|
||||
{
|
||||
auto const jString = boost::json::value("a string");
|
||||
ASSERT_TRUE(checkType<string>(jString));
|
||||
ASSERT_FALSE(checkType<int>(jString));
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, CheckTypeUint)
|
||||
{
|
||||
auto const jUint = boost::json::value(123u);
|
||||
ASSERT_TRUE(checkType<uint32_t>(jUint));
|
||||
ASSERT_TRUE(checkType<int32_t>(jUint));
|
||||
ASSERT_FALSE(checkType<bool>(jUint));
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, CheckTypeInt)
|
||||
{
|
||||
auto jInt = boost::json::value(123);
|
||||
ASSERT_TRUE(checkType<int32_t>(jInt));
|
||||
ASSERT_TRUE(checkType<uint32_t>(jInt));
|
||||
ASSERT_FALSE(checkType<bool>(jInt));
|
||||
|
||||
jInt = boost::json::value(-123);
|
||||
ASSERT_TRUE(checkType<int32_t>(jInt));
|
||||
ASSERT_FALSE(checkType<uint32_t>(jInt)); // Unsigned can't be negative
|
||||
ASSERT_FALSE(checkType<bool>(jInt));
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, CheckTypeBool)
|
||||
{
|
||||
auto const jBool = boost::json::value(true);
|
||||
ASSERT_TRUE(checkType<bool>(jBool));
|
||||
ASSERT_FALSE(checkType<int>(jBool));
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, CheckTypeDouble)
|
||||
{
|
||||
auto const jDouble = boost::json::value(0.123);
|
||||
ASSERT_TRUE(checkType<double>(jDouble));
|
||||
ASSERT_TRUE(checkType<float>(jDouble));
|
||||
ASSERT_FALSE(checkType<bool>(jDouble));
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, CheckTypeArray)
|
||||
{
|
||||
auto const jArr = boost::json::value({1, 2, 3});
|
||||
ASSERT_TRUE(checkType<boost::json::array>(jArr));
|
||||
ASSERT_FALSE(checkType<int>(jArr));
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, CheckTypeAndClampValueUnchanged)
|
||||
{
|
||||
auto jUint = boost::json::value(123u);
|
||||
ASSERT_TRUE(checkTypeAndClamp<uint32_t>(jUint));
|
||||
ASSERT_EQ(jUint.as_uint64(), 123u);
|
||||
ASSERT_TRUE(checkTypeAndClamp<int32_t>(jUint));
|
||||
ASSERT_EQ(jUint.as_uint64(), 123u);
|
||||
|
||||
auto jInt = boost::json::value(123);
|
||||
ASSERT_TRUE(checkTypeAndClamp<int32_t>(jInt));
|
||||
ASSERT_EQ(jInt.as_int64(), 123);
|
||||
ASSERT_TRUE(checkTypeAndClamp<uint32_t>(jInt));
|
||||
ASSERT_EQ(jInt.as_int64(), 123);
|
||||
|
||||
jInt = boost::json::value(-123);
|
||||
ASSERT_TRUE(checkTypeAndClamp<int32_t>(jInt));
|
||||
ASSERT_EQ(jInt.as_int64(), -123);
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, CheckTypeAndClampInvalidValues)
|
||||
{
|
||||
auto jInt = boost::json::value(-123);
|
||||
ASSERT_FALSE(checkTypeAndClamp<uint32_t>(jInt)); // Unsigned can't be negative
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, CheckTypeAndClampOverflow)
|
||||
{
|
||||
auto jBigUint = boost::json::value(std::numeric_limits<uint64_t>::max());
|
||||
ASSERT_TRUE(checkTypeAndClamp<uint32_t>(jBigUint));
|
||||
ASSERT_EQ(jBigUint.as_uint64(), std::numeric_limits<uint32_t>::max());
|
||||
|
||||
auto jBigInt = boost::json::value(std::numeric_limits<int64_t>::max());
|
||||
ASSERT_TRUE(checkTypeAndClamp<int32_t>(jBigInt));
|
||||
ASSERT_EQ(jBigInt.as_int64(), std::numeric_limits<int32_t>::max());
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, CheckTypeAndClampUnderflow)
|
||||
{
|
||||
auto jLowInt = boost::json::value(std::numeric_limits<int64_t>::min());
|
||||
ASSERT_TRUE(checkTypeAndClamp<int32_t>(jLowInt));
|
||||
ASSERT_EQ(jLowInt.as_int64(), std::numeric_limits<int32_t>::min());
|
||||
}
|
||||
|
||||
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<boost::json::array>{}},
|
||||
};
|
||||
|
||||
auto passingInput = boost::json::parse(R"JSON({
|
||||
"uint": 123,
|
||||
"int": 321,
|
||||
"str": "a string",
|
||||
"double": 1.0,
|
||||
"bool": true,
|
||||
"arr": []
|
||||
})JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
|
||||
{
|
||||
auto failingInput = boost::json::parse(R"JSON({ "uint": "a string" })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
}
|
||||
{
|
||||
auto failingInput = boost::json::parse(R"JSON({ "int": "a string" })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
}
|
||||
{
|
||||
auto failingInput = boost::json::parse(R"JSON({ "str": 1234 })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
}
|
||||
{
|
||||
auto failingInput = boost::json::parse(R"JSON({ "double": "a string" })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
}
|
||||
{
|
||||
auto failingInput = boost::json::parse(R"JSON({ "bool": "a string" })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
}
|
||||
{
|
||||
auto failingInput = boost::json::parse(R"JSON({ "arr": "a string" })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, TypeValidatorMultipleTypes)
|
||||
{
|
||||
auto spec = RpcSpec{
|
||||
// either int or string
|
||||
{"test", Type<uint32_t, string>{}},
|
||||
};
|
||||
|
||||
auto passingInput = boost::json::parse(R"JSON({ "test": "1234" })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
|
||||
auto passingInput2 = boost::json::parse(R"JSON({ "test": 1234 })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput2));
|
||||
|
||||
auto failingInput = boost::json::parse(R"JSON({ "test": true })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, RequiredValidator)
|
||||
{
|
||||
auto spec = RpcSpec{
|
||||
{"required", Required{}},
|
||||
};
|
||||
|
||||
auto passingInput = boost::json::parse(R"JSON({ "required": "present" })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
|
||||
auto passingInput2 = boost::json::parse(R"JSON({ "required": true })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput2));
|
||||
|
||||
auto failingInput = boost::json::parse(R"JSON({})JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, BetweenValidator)
|
||||
{
|
||||
auto spec = RpcSpec{
|
||||
{"amount", Between<uint32_t>{10u, 20u}},
|
||||
};
|
||||
|
||||
auto passingInput = boost::json::parse(R"JSON({ "amount": 15 })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
|
||||
auto passingInput2 = boost::json::parse(R"JSON({ "amount": 10 })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput2));
|
||||
|
||||
auto passingInput3 = boost::json::parse(R"JSON({ "amount": 20 })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput3));
|
||||
|
||||
auto failingInput = boost::json::parse(R"JSON({ "amount": 9 })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
|
||||
auto failingInput2 = boost::json::parse(R"JSON({ "amount": 21 })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput2));
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, MinValidator)
|
||||
{
|
||||
auto spec = RpcSpec{
|
||||
{"amount", Min{6}},
|
||||
};
|
||||
|
||||
auto passingInput = boost::json::parse(R"JSON({ "amount": 7 })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
|
||||
auto passingInput2 = boost::json::parse(R"JSON({ "amount": 6 })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput2));
|
||||
|
||||
auto failingInput = boost::json::parse(R"JSON({ "amount": 5 })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, MinValidatorAfterType)
|
||||
{
|
||||
auto spec = RpcSpec{
|
||||
{"amount", Type<std::uint32_t>{}, Min{std::numeric_limits<uint32_t>::max()}},
|
||||
{"amount2", Type<std::int32_t>{}, Min{std::numeric_limits<int32_t>::max()}},
|
||||
{"amount3", Type<std::int32_t>{}, Min{std::numeric_limits<int32_t>::min()}},
|
||||
};
|
||||
|
||||
auto bigInput = boost::json::parse(
|
||||
R"JSON({ "amount": 9999999999, "amount2": 9999999999, "amount3": -9999999999 })JSON"
|
||||
);
|
||||
ASSERT_TRUE(spec.process(bigInput)); // type check clamps to type's max/min value
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, MaxValidator)
|
||||
{
|
||||
auto spec = RpcSpec{
|
||||
{"amount", Max{6}},
|
||||
};
|
||||
|
||||
auto passingInput = boost::json::parse(R"JSON({ "amount": 5 })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
|
||||
auto passingInput2 = boost::json::parse(R"JSON({ "amount": 6 })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput2));
|
||||
|
||||
auto failingInput = boost::json::parse(R"JSON({ "amount": 7 })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, MaxValidatorAfterType)
|
||||
{
|
||||
auto spec = RpcSpec{
|
||||
{"amount", Type<std::uint32_t>{}, Max{std::numeric_limits<uint32_t>::max()}},
|
||||
{"amount2", Type<std::int32_t>{}, Max{std::numeric_limits<int32_t>::max()}},
|
||||
{"amount3", Type<std::int32_t>{}, Max{std::numeric_limits<int32_t>::min()}},
|
||||
};
|
||||
|
||||
auto bigInput = boost::json::parse(
|
||||
R"JSON({ "amount": 9999999999, "amount2": 9999999999, "amount3": -9999999999 })JSON"
|
||||
);
|
||||
ASSERT_TRUE(spec.process(bigInput)); // type check clamps to type's min/max value
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, OneOfValidator)
|
||||
{
|
||||
auto spec = RpcSpec{
|
||||
{"currency", OneOf{"XRP", "USD"}},
|
||||
};
|
||||
|
||||
auto passingInput = boost::json::parse(R"JSON({ "currency": "XRP" })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
|
||||
auto passingInput2 = boost::json::parse(R"JSON({ "currency": "USD" })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput2));
|
||||
|
||||
auto failingInput = boost::json::parse(R"JSON({ "currency": "PRX" })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, EqualToValidator)
|
||||
{
|
||||
auto spec = RpcSpec{
|
||||
{"exact", EqualTo{"CaseSensitive"}},
|
||||
};
|
||||
|
||||
auto passingInput = boost::json::parse(R"JSON({ "exact": "CaseSensitive" })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
|
||||
auto failingInput = boost::json::parse(R"JSON({ "exact": "Different" })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, ArrayAtValidator)
|
||||
{
|
||||
auto spec = RpcSpec{
|
||||
{"arr",
|
||||
Required{},
|
||||
Type<boost::json::array>{},
|
||||
ValidateArrayAt{
|
||||
0,
|
||||
{
|
||||
{"limit", Required{}, Type<uint32_t>{}, Between<uint32_t>{0, 100}},
|
||||
}
|
||||
}},
|
||||
{"arr2",
|
||||
ValidateArrayAt{
|
||||
0,
|
||||
{
|
||||
{"limit", Required{}, Type<uint32_t>{}, Between<uint32_t>{0, 100}},
|
||||
}
|
||||
}},
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
auto passingInput = boost::json::parse(R"JSON({ "arr": [{"limit": 42}] })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
|
||||
auto failingInput = boost::json::parse(R"JSON({ "arr": [{"limit": "not int"}] })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
|
||||
failingInput =
|
||||
boost::json::parse(R"JSON({ "arr": [{"limit": 42}], "arr2": "not array type" })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
|
||||
failingInput = boost::json::parse(R"JSON({ "arr": [] })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, IfTypeValidator)
|
||||
{
|
||||
auto spec = RpcSpec{
|
||||
{"mix",
|
||||
Required{},
|
||||
Type<std::string, boost::json::object>{},
|
||||
IfType<boost::json::object>{
|
||||
Section{{"limit", Required{}, Type<uint32_t>{}, Between<uint32_t>{0, 100}}},
|
||||
Section{{"limit2", Required{}, Type<uint32_t>{}, Between<uint32_t>{0, 100}}}
|
||||
},
|
||||
IfType<std::string>{CustomValidators::uint256HexStringValidator}},
|
||||
{"mix2",
|
||||
Section{{"limit", Required{}, Type<uint32_t>{}, Between<uint32_t>{0, 100}}},
|
||||
Type<std::string, boost::json::object>{}},
|
||||
};
|
||||
|
||||
// if json object pass
|
||||
auto passingInput = boost::json::parse(R"JSON({ "mix": {"limit": 42, "limit2": 22} })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
// if string pass
|
||||
passingInput = boost::json::parse(
|
||||
R"JSON({ "mix": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC" })JSON"
|
||||
);
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
|
||||
// if json object fail at first requirement
|
||||
auto failingInput = boost::json::parse(R"JSON({ "mix": {"limit": "not int"} })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
// if json object fail at second requirement
|
||||
failingInput = boost::json::parse(R"JSON({ "mix": {"limit": 22, "limit2": "y"} })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
|
||||
// if string fail
|
||||
failingInput = boost::json::parse(R"JSON({ "mix": "not hash" })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
|
||||
// type check fail
|
||||
failingInput = boost::json::parse(R"JSON({ "mix": 1213 })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
|
||||
failingInput =
|
||||
boost::json::parse(R"JSON({ "mix": {"limit": 42, "limit2": 22}, "mix2": 1213 })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, WithCustomError)
|
||||
{
|
||||
auto const spec = RpcSpec{
|
||||
{"transaction",
|
||||
WithCustomError{
|
||||
CustomValidators::uint256HexStringValidator,
|
||||
rpc::Status{xrpl::RpcBadFeature, "MyCustomError"}
|
||||
}},
|
||||
{"other",
|
||||
WithCustomError{
|
||||
Type<std::string>{}, rpc::Status{xrpl::RpcAlreadyMultisig, "MyCustomError2"}
|
||||
}}
|
||||
};
|
||||
|
||||
auto passingInput = boost::json::parse(
|
||||
R"JSON({ "transaction": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC", "other": "1"})JSON"
|
||||
);
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
|
||||
auto failingInput = boost::json::parse(
|
||||
R"JSON({ "transaction": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515B"})JSON"
|
||||
);
|
||||
auto err = spec.process(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
ASSERT_EQ(err.error().message, "MyCustomError");
|
||||
ASSERT_EQ(err.error(), xrpl::RpcBadFeature);
|
||||
|
||||
failingInput = boost::json::parse(R"JSON({ "other": 1})JSON");
|
||||
err = spec.process(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
ASSERT_EQ(err.error().message, "MyCustomError2");
|
||||
ASSERT_EQ(err.error(), xrpl::RpcAlreadyMultisig);
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, TimeFormatValidator)
|
||||
{
|
||||
auto const spec = RpcSpec{
|
||||
{"date", TimeFormatValidator{"%Y-%m-%dT%H:%M:%SZ"}},
|
||||
};
|
||||
|
||||
auto passingInput = boost::json::parse(R"JSON({ "date": "2023-01-01T00:00:00Z" })JSON");
|
||||
EXPECT_TRUE(spec.process(passingInput));
|
||||
|
||||
passingInput = boost::json::parse("123");
|
||||
EXPECT_TRUE(spec.process(passingInput));
|
||||
|
||||
// key not exists
|
||||
passingInput = boost::json::parse(R"JSON({ "date1": "2023-01-01T00:00:00Z" })JSON");
|
||||
EXPECT_TRUE(spec.process(passingInput));
|
||||
|
||||
auto failingInput = boost::json::parse(R"JSON({ "date": "2023-01-01-00:00:00" })JSON");
|
||||
auto err = spec.process(failingInput);
|
||||
EXPECT_FALSE(err);
|
||||
EXPECT_EQ(err.error(), xrpl::RpcInvalidParams);
|
||||
|
||||
failingInput = boost::json::parse(R"JSON({ "date": "01-01-2024T00:00:00" })JSON");
|
||||
EXPECT_FALSE(spec.process(failingInput));
|
||||
|
||||
failingInput = boost::json::parse(R"JSON({ "date": "2024-01-01T29:00:00" })JSON");
|
||||
EXPECT_FALSE(spec.process(failingInput));
|
||||
|
||||
failingInput = boost::json::parse(R"JSON({ "date": "" })JSON");
|
||||
EXPECT_FALSE(spec.process(failingInput));
|
||||
|
||||
failingInput = boost::json::parse(R"JSON({ "date": 1 })JSON");
|
||||
err = spec.process(failingInput);
|
||||
EXPECT_FALSE(err);
|
||||
EXPECT_EQ(err.error(), xrpl::RpcInvalidParams);
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, CustomValidator)
|
||||
{
|
||||
auto customFormatCheck = CustomValidator{
|
||||
[](boost::json::value const& value, std::string_view /* key */) -> MaybeError {
|
||||
return value.as_string().size() == 34 ? MaybeError{} : Error{rpc::Status{"Uh oh"}};
|
||||
}
|
||||
};
|
||||
|
||||
auto spec = RpcSpec{
|
||||
{"taker", customFormatCheck},
|
||||
};
|
||||
|
||||
auto passingInput =
|
||||
boost::json::parse(R"JSON({ "taker": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59" })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
|
||||
auto failingInput = boost::json::parse(R"JSON({ "taker": "wrongformat" })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, NotSupported)
|
||||
{
|
||||
auto spec = RpcSpec{
|
||||
{"taker", Type<uint32_t>{}, NotSupported{123}},
|
||||
{"getter", NotSupported{}},
|
||||
};
|
||||
|
||||
auto passingInput = boost::json::parse(R"JSON({ "taker": 2 })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
|
||||
auto failingInput = boost::json::parse(R"JSON({ "taker": 123 })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
|
||||
failingInput = boost::json::parse(R"JSON({ "taker": 2, "getter": 2 })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, LedgerIndexValidator)
|
||||
{
|
||||
auto spec = RpcSpec{
|
||||
{"ledgerIndex", CustomValidators::ledgerIndexValidator},
|
||||
};
|
||||
auto passingInput = boost::json::parse(R"JSON({ "ledgerIndex": "validated" })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
|
||||
passingInput = boost::json::parse(R"JSON({ "ledgerIndex": "256" })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
|
||||
passingInput = boost::json::parse(R"JSON({ "ledgerIndex": 256 })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
|
||||
auto failingInput = boost::json::parse(R"JSON({ "ledgerIndex": "wrongformat" })JSON");
|
||||
auto err = spec.process(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
ASSERT_EQ(err.error().message, "ledgerIndexMalformed");
|
||||
|
||||
failingInput = boost::json::parse(R"JSON({ "ledgerIndex": true })JSON");
|
||||
err = spec.process(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
ASSERT_EQ(err.error().message, "ledgerIndexMalformed");
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, AccountBase58Validator)
|
||||
{
|
||||
auto spec = RpcSpec{
|
||||
{"account", CustomValidators::accountBase58Validator},
|
||||
};
|
||||
auto failingInput = boost::json::parse(R"JSON({ "account": 256 })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
|
||||
failingInput =
|
||||
boost::json::parse(R"JSON({ "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jp" })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
|
||||
failingInput = boost::json::parse(
|
||||
R"JSON({ "account": "020000000000000000000000000000000000000000000000000000000000000000" })JSON"
|
||||
);
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
|
||||
failingInput =
|
||||
boost::json::parse(R"JSON({ "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jp?" })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
|
||||
auto passingInput =
|
||||
boost::json::parse(R"JSON({ "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, AccountMarkerValidator)
|
||||
{
|
||||
auto spec = RpcSpec{
|
||||
{"marker", CustomValidators::accountMarkerValidator},
|
||||
};
|
||||
auto failingInput = boost::json::parse(R"JSON({ "marker": 256 })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
|
||||
failingInput = boost::json::parse(R"JSON({ "marker": "testtest" })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
|
||||
failingInput = boost::json::parse(R"JSON({ "marker": "ABAB1234:1H" })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
|
||||
auto passingInput = boost::json::parse(R"JSON({ "account": "ABAB1234:123" })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, Uint160HexStringValidator)
|
||||
{
|
||||
auto const spec = RpcSpec{{"marker", CustomValidators::uint160HexStringValidator}};
|
||||
auto passingInput =
|
||||
boost::json::parse(R"JSON({ "marker": "F609A18102218C75767209946A77523CBD97E225"})JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
|
||||
auto failingInput = boost::json::parse(R"JSON({ "marker": 160})JSON");
|
||||
auto err = spec.process(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
ASSERT_EQ(err.error().message, "markerNotString");
|
||||
|
||||
failingInput = boost::json::parse(
|
||||
R"JSON({ "marker": "F609A18102218C75767209946A77523CBD97E2253515BC"})JSON"
|
||||
);
|
||||
err = spec.process(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
ASSERT_EQ(err.error().message, "markerMalformed");
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, Uint192HexStringValidator)
|
||||
{
|
||||
auto const spec = RpcSpec{{"mpt_issuance_id", CustomValidators::uint192HexStringValidator}};
|
||||
auto passingInput = boost::json::parse(
|
||||
R"JSON({ "mpt_issuance_id": "0000012F27A9DE73EAA1E8831FA253E19030A17E2D038198"})JSON"
|
||||
);
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
|
||||
auto failingInput = boost::json::parse(R"JSON({ "mpt_issuance_id": 192})JSON");
|
||||
auto err = spec.process(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
ASSERT_EQ(err.error().message, "mpt_issuance_idNotString");
|
||||
|
||||
failingInput = boost::json::parse(
|
||||
R"JSON({ "mpt_issuance_id": "0000012F27A9DE73EAA1E8831FA253E19030A17E2D038198983515BC"})JSON"
|
||||
);
|
||||
err = spec.process(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
ASSERT_EQ(err.error().message, "mpt_issuance_idMalformed");
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, Uint256HexStringValidator)
|
||||
{
|
||||
auto const spec = RpcSpec{{"transaction", CustomValidators::uint256HexStringValidator}};
|
||||
auto passingInput = boost::json::parse(
|
||||
R"JSON({ "transaction": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC"})JSON"
|
||||
);
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
|
||||
auto failingInput = boost::json::parse(R"JSON({ "transaction": 256})JSON");
|
||||
auto err = spec.process(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
ASSERT_EQ(err.error().message, "transactionNotString");
|
||||
|
||||
failingInput = boost::json::parse(
|
||||
R"JSON({ "transaction": "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC"})JSON"
|
||||
);
|
||||
err = spec.process(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
ASSERT_EQ(err.error().message, "transactionMalformed");
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, CurrencyValidator)
|
||||
{
|
||||
auto const spec = RpcSpec{{"currency", CustomValidators::currencyValidator}};
|
||||
auto passingInput = boost::json::parse(R"JSON({ "currency": "GBP"})JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
|
||||
passingInput =
|
||||
boost::json::parse(R"JSON({ "currency": "0158415500000000C1F76FF6ECB0BAC600000000"})JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
|
||||
passingInput =
|
||||
boost::json::parse(R"JSON({ "currency": "0158415500000000c1f76ff6ecb0bac600000000"})JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
|
||||
for (auto const& currency : {"[]<", ">()", "{}|", "?!@", "#$%", "^&*"}) {
|
||||
passingInput =
|
||||
boost::json::parse(fmt::format(R"JSON({{ "currency": "{}" }})JSON", currency));
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
}
|
||||
|
||||
auto failingInput = boost::json::parse(R"JSON({ "currency": 256})JSON");
|
||||
auto err = spec.process(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
ASSERT_EQ(err.error().message, "currencyNotString");
|
||||
|
||||
failingInput = boost::json::parse(R"JSON({ "currency": "12314"})JSON");
|
||||
err = spec.process(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
ASSERT_EQ(err.error().message, "malformedCurrency");
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, BookTakerValidator)
|
||||
{
|
||||
auto const spec = RpcSpec{{"taker_gets", CustomValidators::bookTakerValidator}};
|
||||
|
||||
// An asset specified by currency, optionally with an issuer, is valid.
|
||||
auto passingInput = boost::json::parse(R"JSON({ "taker_gets": { "currency": "XRP" }})JSON");
|
||||
EXPECT_TRUE(spec.process(passingInput));
|
||||
|
||||
passingInput = boost::json::parse(
|
||||
R"JSON({ "taker_gets": { "currency": "USD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" }})JSON"
|
||||
);
|
||||
EXPECT_TRUE(spec.process(passingInput));
|
||||
|
||||
// An asset specified by mpt_issuance_id alone is valid.
|
||||
passingInput = boost::json::parse(
|
||||
R"JSON({ "taker_gets": { "mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405DBADD" }})JSON"
|
||||
);
|
||||
EXPECT_TRUE(spec.process(passingInput));
|
||||
|
||||
// Non-object values are deferred to the type validator; the field being absent is deferred to
|
||||
// the required validator. Both pass here.
|
||||
passingInput = boost::json::parse(R"JSON({ "taker_gets": "not_an_object" })JSON");
|
||||
EXPECT_TRUE(spec.process(passingInput));
|
||||
|
||||
passingInput = boost::json::parse(R"JSON({ })JSON");
|
||||
EXPECT_TRUE(spec.process(passingInput));
|
||||
|
||||
// Neither currency nor mpt_issuance_id -> invalidParams, reporting the currency field.
|
||||
auto failingInput = boost::json::parse(R"JSON({ "taker_gets": { }})JSON");
|
||||
auto err = spec.process(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
EXPECT_TRUE(err.error().code == CombinedError{RippledError::RpcInvalidParams});
|
||||
EXPECT_EQ(err.error().message, "Missing field 'taker_gets.currency'.");
|
||||
|
||||
// currency and mpt_issuance_id are mutually exclusive.
|
||||
failingInput = boost::json::parse(
|
||||
R"JSON({ "taker_gets": { "currency": "USD", "mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405DBADD" }})JSON"
|
||||
);
|
||||
err = spec.process(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
EXPECT_TRUE(err.error().code == CombinedError{RippledError::RpcInvalidParams});
|
||||
EXPECT_EQ(err.error().message, "Invalid field 'taker_gets'.");
|
||||
|
||||
// mpt_issuance_id must not be combined with an issuer.
|
||||
failingInput = boost::json::parse(
|
||||
R"JSON({ "taker_gets": { "mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405DBADD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" }})JSON"
|
||||
);
|
||||
err = spec.process(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
EXPECT_TRUE(err.error().code == CombinedError{RippledError::RpcInvalidParams});
|
||||
EXPECT_EQ(err.error().message, "Invalid field 'taker_gets'.");
|
||||
|
||||
// A present-but-non-string currency -> expectedFieldError against '<key>.currency'.
|
||||
failingInput = boost::json::parse(R"JSON({ "taker_gets": { "currency": 123 }})JSON");
|
||||
err = spec.process(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
EXPECT_TRUE(err.error().code == CombinedError{RippledError::RpcInvalidParams});
|
||||
EXPECT_EQ(err.error().message, "Invalid field 'taker_gets.currency', not string.");
|
||||
|
||||
// A present-but-non-string mpt_issuance_id is reported the same way (against '.currency',
|
||||
// matching xrpld's validateTakerJSON).
|
||||
failingInput = boost::json::parse(R"JSON({ "taker_gets": { "mpt_issuance_id": 123 }})JSON");
|
||||
err = spec.process(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
EXPECT_TRUE(err.error().code == CombinedError{RippledError::RpcInvalidParams});
|
||||
EXPECT_EQ(err.error().message, "Invalid field 'taker_gets.currency', not string.");
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, BookTakerValidatorUsesFieldKey)
|
||||
{
|
||||
// The error messages must reflect the validated field's key (taker_pays vs taker_gets).
|
||||
auto const spec = RpcSpec{{"taker_pays", CustomValidators::bookTakerValidator}};
|
||||
|
||||
auto failingInput = boost::json::parse(R"JSON({ "taker_pays": { }})JSON");
|
||||
auto err = spec.process(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
EXPECT_EQ(err.error().message, "Missing field 'taker_pays.currency'.");
|
||||
|
||||
failingInput = boost::json::parse(
|
||||
R"JSON({ "taker_pays": { "mpt_issuance_id": "000004C463C52827307480341125DA0577DEFC38405DBADD", "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn" }})JSON"
|
||||
);
|
||||
err = spec.process(failingInput);
|
||||
ASSERT_FALSE(err);
|
||||
EXPECT_EQ(err.error().message, "Invalid field 'taker_pays'.");
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, ClampingModifier)
|
||||
{
|
||||
auto spec = RpcSpec{
|
||||
{"amount", Clamp<uint32_t>{10u, 20u}},
|
||||
};
|
||||
|
||||
auto passingInput = boost::json::parse(R"JSON({ "amount": 15 })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
|
||||
auto passingInput2 = boost::json::parse(R"JSON({ "amount": 5 })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput2));
|
||||
ASSERT_EQ(passingInput2.at("amount").as_uint64(), 10u); // clamped
|
||||
|
||||
auto passingInput3 = boost::json::parse(R"JSON({ "amount": 25 })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput3));
|
||||
ASSERT_EQ(passingInput3.at("amount").as_uint64(), 20u); // clamped
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, ToLowerModifier)
|
||||
{
|
||||
auto spec = RpcSpec{
|
||||
{"str", ToLower{}},
|
||||
};
|
||||
|
||||
auto passingInput = boost::json::parse(R"JSON({ "str": "TesT" })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
ASSERT_EQ(passingInput.at("str").as_string(), "test");
|
||||
|
||||
auto passingInput2 = boost::json::parse(R"JSON({ "str2": "TesT" })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput2)); // no str no problem
|
||||
|
||||
auto passingInput3 = boost::json::parse(R"JSON({ "str": "already lower case" })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput3));
|
||||
ASSERT_EQ(passingInput3.at("str").as_string(), "already lower case");
|
||||
|
||||
auto passingInput4 = boost::json::parse(R"JSON({ "str": "" })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput4)); // empty str no problem
|
||||
ASSERT_EQ(passingInput4.at("str").as_string(), "");
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, ToNumberModifier)
|
||||
{
|
||||
auto const spec = RpcSpec{
|
||||
{"str", ToNumber{}},
|
||||
};
|
||||
|
||||
auto passingInput = boost::json::parse(R"JSON({ "str": [] })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
|
||||
passingInput = boost::json::parse(R"JSON({ "str2": "TesT" })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
|
||||
passingInput = boost::json::parse(R"JSON([])JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
|
||||
passingInput = boost::json::parse(R"JSON({ "str": "123" })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
ASSERT_EQ(passingInput.at("str").as_int64(), 123);
|
||||
|
||||
auto failingInput = boost::json::parse(R"JSON({ "str": "ok" })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
|
||||
failingInput = boost::json::parse(R"JSON({ "str": "123.123" })JSON");
|
||||
ASSERT_FALSE(spec.process(failingInput));
|
||||
}
|
||||
|
||||
TEST_F(RPCBaseTest, CustomModifier)
|
||||
{
|
||||
testing::StrictMock<
|
||||
testing::MockFunction<MaybeError(boost::json::value & value, std::string_view)>>
|
||||
mockModifier;
|
||||
auto const customModifier = CustomModifier{mockModifier.AsStdFunction()};
|
||||
auto const spec = RpcSpec{
|
||||
{"str", customModifier},
|
||||
};
|
||||
|
||||
EXPECT_CALL(mockModifier, Call).WillOnce(testing::Return(MaybeError{}));
|
||||
auto passingInput = boost::json::parse(R"JSON({ "str": "sss" })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
|
||||
passingInput = boost::json::parse(R"JSON({ "strNotExist": 123 })JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
|
||||
// not a json object
|
||||
passingInput = boost::json::parse(R"JSON([])JSON");
|
||||
ASSERT_TRUE(spec.process(passingInput));
|
||||
}
|
||||
@@ -119,6 +119,17 @@ generateTestValuesForParametersTest()
|
||||
.called = 1,
|
||||
.isAdmin = isAdmin,
|
||||
.expected = shouldForward},
|
||||
// The isClioOnly check short-circuits before the current/closed check, so a Clio-only
|
||||
// method is NOT forwarded even when it names a ledger Clio does not hold. Its handler has
|
||||
// to reject the shortcut itself - see getLedgerHeaderFromLedgerSpecifier.
|
||||
{.testName = "ShouldForwardReturnsFalseIfClioOnlyEvenWithCurrentLedger",
|
||||
.apiVersion = 2u,
|
||||
.method = "nft_info",
|
||||
.testJson = R"JSON({"ledger_index": "current"})JSON",
|
||||
.mockedIsClioOnly = isClioOnly,
|
||||
.called = 1,
|
||||
.isAdmin = !isAdmin,
|
||||
.expected = !shouldForward},
|
||||
{.testName = "ShouldForwardReturnsTrueIfCurrentLedgerSpecified",
|
||||
.apiVersion = 2u,
|
||||
.method = "anymethod",
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
#include "util/NameGenerator.hpp"
|
||||
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <boost/json/value_to.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
#include <rpcspec/JsonBool.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace rpc;
|
||||
using namespace testing;
|
||||
|
||||
struct JsonBoolTestsCaseBundle {
|
||||
std::string testName;
|
||||
std::string json;
|
||||
bool expectedBool;
|
||||
};
|
||||
|
||||
class JsonBoolTests : public TestWithParam<JsonBoolTestsCaseBundle> {
|
||||
public:
|
||||
static auto
|
||||
generateTestValuesForParametersTest()
|
||||
{
|
||||
return std::vector<JsonBoolTestsCaseBundle>{
|
||||
{.testName = "NullValue",
|
||||
.json = R"JSON({ "test_bool": null })JSON",
|
||||
.expectedBool = false},
|
||||
{.testName = "BoolTrueValue",
|
||||
.json = R"JSON({ "test_bool": true })JSON",
|
||||
.expectedBool = true},
|
||||
{.testName = "BoolFalseValue",
|
||||
.json = R"JSON({ "test_bool": false })JSON",
|
||||
.expectedBool = false},
|
||||
{.testName = "IntTrueValue",
|
||||
.json = R"JSON({ "test_bool": 1 })JSON",
|
||||
.expectedBool = true},
|
||||
{.testName = "IntFalseValue",
|
||||
.json = R"JSON({ "test_bool": 0 })JSON",
|
||||
.expectedBool = false},
|
||||
{.testName = "DoubleTrueValue",
|
||||
.json = R"JSON({ "test_bool": 0.1 })JSON",
|
||||
.expectedBool = true},
|
||||
{.testName = "DoubleFalseValue",
|
||||
.json = R"JSON({ "test_bool": 0.0 })JSON",
|
||||
.expectedBool = false},
|
||||
{.testName = "StringTrueValue",
|
||||
.json = R"JSON({ "test_bool": "true" })JSON",
|
||||
.expectedBool = true},
|
||||
{.testName = "StringFalseValue",
|
||||
.json = R"JSON({ "test_bool": "false" })JSON",
|
||||
.expectedBool = true},
|
||||
{.testName = "ArrayTrueValue",
|
||||
.json = R"JSON({ "test_bool": [0] })JSON",
|
||||
.expectedBool = true},
|
||||
{.testName = "ArrayFalseValue",
|
||||
.json = R"JSON({ "test_bool": [] })JSON",
|
||||
.expectedBool = false},
|
||||
{.testName = "ObjectTrueValue",
|
||||
.json = R"JSON({ "test_bool": { "key": null } })JSON",
|
||||
.expectedBool = true},
|
||||
{.testName = "ObjectFalseValue",
|
||||
.json = R"JSON({ "test_bool": {} })JSON",
|
||||
.expectedBool = false}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
JsonBoolCheckGroup,
|
||||
JsonBoolTests,
|
||||
ValuesIn(JsonBoolTests::generateTestValuesForParametersTest()),
|
||||
tests::util::kNameGenerator
|
||||
);
|
||||
|
||||
TEST_P(JsonBoolTests, Parse)
|
||||
{
|
||||
auto const testBundle = GetParam();
|
||||
auto const jv = boost::json::parse(testBundle.json).as_object();
|
||||
ASSERT_TRUE(jv.contains("test_bool"));
|
||||
EXPECT_EQ(testBundle.expectedBool, value_to<rpc::spec::JsonBool>(jv.at("test_bool")).value);
|
||||
}
|
||||
@@ -220,12 +220,12 @@ TEST_P(RPCEngineFlowParameterTest, Test)
|
||||
} else {
|
||||
if (testBundle.handlerReturnError) {
|
||||
EXPECT_CALL(*handlerProvider, getHandler)
|
||||
.WillOnce(Return(AnyHandler{tests::common::FailingHandlerFake{}}));
|
||||
.WillOnce(Return(AnyHandler{tests::common::FailingTypedHandlerFake{}}));
|
||||
EXPECT_CALL(*mockCountersPtr_, rpcErrored(testBundle.method));
|
||||
EXPECT_CALL(*handlerProvider, contains(testBundle.method)).WillOnce(Return(true));
|
||||
} else {
|
||||
EXPECT_CALL(*handlerProvider, getHandler(testBundle.method))
|
||||
.WillOnce(Return(AnyHandler{tests::common::HandlerFake{}}));
|
||||
.WillOnce(Return(AnyHandler{tests::common::TypedHandlerFake{}}));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -261,7 +261,7 @@ TEST_F(RPCEngineTest, ThrowDatabaseError)
|
||||
);
|
||||
EXPECT_CALL(*backend_, isTooBusy).WillOnce(Return(false));
|
||||
EXPECT_CALL(*handlerProvider, getHandler(method))
|
||||
.WillOnce(Return(AnyHandler{tests::common::FailingHandlerFake{}}));
|
||||
.WillOnce(Return(AnyHandler{tests::common::FailingTypedHandlerFake{}}));
|
||||
EXPECT_CALL(*mockCountersPtr_, rpcErrored(method)).WillOnce(Throw(data::DatabaseError{}));
|
||||
EXPECT_CALL(*handlerProvider, contains(method)).WillOnce(Return(true));
|
||||
EXPECT_CALL(*mockCountersPtr_, onTooBusy());
|
||||
@@ -293,7 +293,7 @@ TEST_F(RPCEngineTest, ThrowException)
|
||||
);
|
||||
EXPECT_CALL(*backend_, isTooBusy).WillOnce(Return(false));
|
||||
EXPECT_CALL(*handlerProvider, getHandler(method))
|
||||
.WillOnce(Return(AnyHandler{tests::common::FailingHandlerFake{}}));
|
||||
.WillOnce(Return(AnyHandler{tests::common::FailingTypedHandlerFake{}}));
|
||||
EXPECT_CALL(*mockCountersPtr_, rpcErrored(method)).WillOnce(Throw(std::exception{}));
|
||||
EXPECT_CALL(*handlerProvider, contains(method)).WillOnce(Return(true));
|
||||
EXPECT_CALL(*mockCountersPtr_, onInternalError());
|
||||
@@ -486,7 +486,7 @@ TEST_F(RPCEngineTest, NonBareRequestBypassesCache)
|
||||
EXPECT_CALL(*backend_, isTooBusy).Times(callTime).WillRepeatedly(Return(false));
|
||||
EXPECT_CALL(*handlerProvider, getHandler)
|
||||
.Times(callTime)
|
||||
.WillRepeatedly(Return(AnyHandler{tests::common::HandlerFake{}}));
|
||||
.WillRepeatedly(Return(AnyHandler{tests::common::TypedHandlerFake{}}));
|
||||
|
||||
while (callTime-- != 0) {
|
||||
runSpawn([&](auto yield) {
|
||||
@@ -538,7 +538,7 @@ TEST_F(RPCEngineTest, NotCacheIfErrorHappen)
|
||||
EXPECT_CALL(*backend_, isTooBusy).Times(callTime).WillRepeatedly(Return(false));
|
||||
EXPECT_CALL(*handlerProvider, getHandler)
|
||||
.Times(callTime)
|
||||
.WillRepeatedly(Return(AnyHandler{tests::common::FailingHandlerFake{}}));
|
||||
.WillRepeatedly(Return(AnyHandler{tests::common::FailingTypedHandlerFake{}}));
|
||||
EXPECT_CALL(*mockCountersPtr_, rpcErrored(method)).Times(callTime);
|
||||
EXPECT_CALL(*handlerProvider, isClioOnly).Times(callTime).WillRepeatedly(Return(false));
|
||||
EXPECT_CALL(*handlerProvider, contains).Times(callTime).WillRepeatedly(Return(true));
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include "util/AsioContextTestFixture.hpp"
|
||||
#include "util/LoggerFixtures.hpp"
|
||||
#include "util/MockAmendmentCenter.hpp"
|
||||
#include "util/MockAssert.hpp"
|
||||
#include "util/MockBackendTestFixture.hpp"
|
||||
#include "util/MockPrometheus.hpp"
|
||||
#include "util/NameGenerator.hpp"
|
||||
@@ -2051,43 +2050,40 @@ TEST_F(RPCHelpersTest, LedgerHeaderFromSpecifierValidatedUsesMaxSeq)
|
||||
});
|
||||
}
|
||||
|
||||
struct RPCHelpersAssertTest : RPCHelpersTest, common::util::WithMockAssert {};
|
||||
|
||||
TEST_F(RPCHelpersAssertTest, LedgerHeaderFromSpecifierCurrentAsserts)
|
||||
// `current` and `closed` name ledgers Clio does not hold. Most methods never reach here because
|
||||
// ForwardingProxy diverts them to xrpld, but it skips that check for Clio-only methods, so the
|
||||
// resolver has to reject the shortcut rather than treat it as unreachable.
|
||||
TEST_F(RPCHelpersTest, LedgerHeaderFromSpecifierCurrentRejected)
|
||||
{
|
||||
EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(0);
|
||||
|
||||
runSpawn([&, this](auto yield) {
|
||||
EXPECT_CLIO_ASSERT_FAIL_WITH_MESSAGE(
|
||||
{
|
||||
[[maybe_unused]] auto const res = getLedgerHeaderFromLedgerSpecifier(
|
||||
*backend_,
|
||||
yield,
|
||||
rpc::spec::LedgerSpecifier{rpc::spec::LedgerShortcut::Current},
|
||||
kSpecifierRangeMax
|
||||
);
|
||||
},
|
||||
"must be forwarded before dispatch"
|
||||
auto const res = getLedgerHeaderFromLedgerSpecifier(
|
||||
*backend_,
|
||||
yield,
|
||||
rpc::spec::LedgerSpecifier{rpc::spec::LedgerShortcut::Current},
|
||||
kSpecifierRangeMax
|
||||
);
|
||||
ASSERT_FALSE(res.has_value());
|
||||
EXPECT_EQ(res.error().code, rpc::CombinedError{rpc::RippledError::RpcInvalidParams});
|
||||
EXPECT_EQ(res.error().message, "ledgerIndexMalformed");
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCHelpersAssertTest, LedgerHeaderFromSpecifierClosedAsserts)
|
||||
TEST_F(RPCHelpersTest, LedgerHeaderFromSpecifierClosedRejected)
|
||||
{
|
||||
EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(0);
|
||||
|
||||
runSpawn([&, this](auto yield) {
|
||||
EXPECT_CLIO_ASSERT_FAIL_WITH_MESSAGE(
|
||||
{
|
||||
[[maybe_unused]] auto const res = getLedgerHeaderFromLedgerSpecifier(
|
||||
*backend_,
|
||||
yield,
|
||||
rpc::spec::LedgerSpecifier{rpc::spec::LedgerShortcut::Closed},
|
||||
kSpecifierRangeMax
|
||||
);
|
||||
},
|
||||
"must be forwarded before dispatch"
|
||||
auto const res = getLedgerHeaderFromLedgerSpecifier(
|
||||
*backend_,
|
||||
yield,
|
||||
rpc::spec::LedgerSpecifier{rpc::spec::LedgerShortcut::Closed},
|
||||
kSpecifierRangeMax
|
||||
);
|
||||
ASSERT_FALSE(res.has_value());
|
||||
EXPECT_EQ(res.error().code, rpc::CombinedError{rpc::RippledError::RpcInvalidParams});
|
||||
EXPECT_EQ(res.error().message, "ledgerIndexMalformed");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
#include "rpc/common/Checkers.hpp"
|
||||
|
||||
#include <boost/json/value.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
#include <rpcspec/Errors.hpp>
|
||||
|
||||
#include <string>
|
||||
|
||||
using namespace rpc;
|
||||
using namespace rpc::check;
|
||||
|
||||
struct DeprecatedTests : ::testing::Test {
|
||||
boost::json::value const json{
|
||||
{"some_string", "some_value"},
|
||||
{"some_number", 42},
|
||||
{"some_bool", false},
|
||||
{"some_float", 3.14}
|
||||
};
|
||||
};
|
||||
|
||||
TEST_F(DeprecatedTests, Field)
|
||||
{
|
||||
auto warning = Deprecated<>::check(json, "some_string");
|
||||
ASSERT_TRUE(warning.has_value());
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
EXPECT_EQ(warning->warningCode, WarningCode::WarnRpcDeprecated);
|
||||
|
||||
warning = Deprecated<>::check(json, "other");
|
||||
EXPECT_FALSE(warning.has_value());
|
||||
}
|
||||
|
||||
TEST_F(DeprecatedTests, FieldWithStringValue)
|
||||
{
|
||||
Deprecated<std::string> const checker{"some_value"};
|
||||
auto warning = checker.check(json, "some_string");
|
||||
ASSERT_TRUE(warning.has_value());
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
EXPECT_EQ(warning->warningCode, WarningCode::WarnRpcDeprecated);
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
EXPECT_EQ(warning->extraMessage, "Value 'some_value' for field 'some_string' is deprecated");
|
||||
EXPECT_FALSE(Deprecated<std::string>{"other"}.check(json, "some_string"));
|
||||
}
|
||||
|
||||
TEST_F(DeprecatedTests, FieldWithIntValue)
|
||||
{
|
||||
Deprecated<int> const checker{42};
|
||||
auto warning = checker.check(json, "some_number");
|
||||
ASSERT_TRUE(warning.has_value());
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
EXPECT_EQ(warning->warningCode, WarningCode::WarnRpcDeprecated);
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
EXPECT_EQ(warning->extraMessage, "Value '42' for field 'some_number' is deprecated");
|
||||
EXPECT_FALSE(Deprecated<int>{43}.check(json, "some_number"));
|
||||
}
|
||||
|
||||
TEST_F(DeprecatedTests, FieldWithBoolValue)
|
||||
{
|
||||
Deprecated<bool> const checker{false};
|
||||
auto warning = checker.check(json, "some_bool");
|
||||
ASSERT_TRUE(warning.has_value());
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
EXPECT_EQ(warning->warningCode, WarningCode::WarnRpcDeprecated);
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
EXPECT_EQ(warning->extraMessage, "Value 'false' for field 'some_bool' is deprecated");
|
||||
EXPECT_FALSE(Deprecated<bool>{true}.check(json, "some_bool"));
|
||||
}
|
||||
|
||||
TEST_F(DeprecatedTests, FieldWithFloatValue)
|
||||
{
|
||||
Deprecated<float> const checker{3.14};
|
||||
auto warning = checker.check(json, "some_float");
|
||||
ASSERT_TRUE(warning.has_value());
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
EXPECT_EQ(warning->warningCode, WarningCode::WarnRpcDeprecated);
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
EXPECT_EQ(warning->extraMessage, "Value '3.14' for field 'some_float' is deprecated");
|
||||
EXPECT_FALSE(Deprecated<float>{3.15}.check(json, "some_float"));
|
||||
}
|
||||
@@ -1,284 +0,0 @@
|
||||
#include "rpc/common/Checkers.hpp"
|
||||
#include "rpc/common/Specs.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
|
||||
#include <boost/json/array.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <rpcspec/Errors.hpp>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
using namespace rpc;
|
||||
|
||||
using testing::StrictMock;
|
||||
|
||||
struct SpecsTests : testing::Test {
|
||||
struct RequirementMock {
|
||||
MOCK_METHOD(MaybeError, verify, (boost::json::value const&, std::string));
|
||||
};
|
||||
|
||||
struct RequirementMockRef {
|
||||
RequirementMockRef(StrictMock<RequirementMock>& ref) : ref(ref)
|
||||
{
|
||||
}
|
||||
MaybeError
|
||||
verify(boost::json::value& value, std::string key) const
|
||||
{
|
||||
return ref.verify(value, key);
|
||||
}
|
||||
|
||||
StrictMock<RequirementMock>& ref;
|
||||
};
|
||||
|
||||
struct CheckMock {
|
||||
MOCK_METHOD(std::optional<check::Warning>, check, (boost::json::value const&, std::string));
|
||||
};
|
||||
|
||||
struct CheckMockRef {
|
||||
CheckMockRef(StrictMock<CheckMock>& ref) : ref(ref)
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<check::Warning>
|
||||
check(boost::json::value const& value, std::string key) const
|
||||
{
|
||||
return ref.check(value, key);
|
||||
}
|
||||
|
||||
StrictMock<CheckMock>& ref;
|
||||
};
|
||||
|
||||
StrictMock<RequirementMock> requirementMock;
|
||||
StrictMock<RequirementMock> anotherRequirementMock;
|
||||
StrictMock<CheckMock> checkMock;
|
||||
StrictMock<CheckMock> anotherCheckMock;
|
||||
};
|
||||
|
||||
struct ProcessorTestBundle {
|
||||
std::string name;
|
||||
MaybeError requirementResult;
|
||||
std::optional<MaybeError> otherRequirementResult;
|
||||
MaybeError expectedResult;
|
||||
};
|
||||
|
||||
struct FieldProcessorTests : SpecsTests, testing::WithParamInterface<ProcessorTestBundle> {
|
||||
protected:
|
||||
FieldSpec spec_{
|
||||
"key",
|
||||
RequirementMockRef(requirementMock),
|
||||
RequirementMockRef(anotherRequirementMock)
|
||||
};
|
||||
boost::json::value json_;
|
||||
};
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
FieldSpecTestGroup,
|
||||
FieldProcessorTests,
|
||||
testing::Values(
|
||||
ProcessorTestBundle{"NoErrors", MaybeError{}, MaybeError{}, MaybeError{}},
|
||||
ProcessorTestBundle{
|
||||
"FirstError",
|
||||
Error{Status{"error1"}},
|
||||
std::nullopt,
|
||||
Error{Status{"error1"}},
|
||||
},
|
||||
ProcessorTestBundle{
|
||||
"SecondError",
|
||||
MaybeError{},
|
||||
Error{Status{"error2"}},
|
||||
Error{Status{"error2"}},
|
||||
}
|
||||
),
|
||||
[](testing::TestParamInfo<ProcessorTestBundle> const& info) { return info.param.name; }
|
||||
);
|
||||
|
||||
TEST_P(FieldProcessorTests, FieldSpecWithRequirementProcess)
|
||||
{
|
||||
EXPECT_CALL(requirementMock, verify).WillOnce(testing::Return(GetParam().requirementResult));
|
||||
if (GetParam().otherRequirementResult.has_value()) {
|
||||
EXPECT_CALL(anotherRequirementMock, verify)
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
.WillOnce(testing::Return(GetParam().otherRequirementResult.value()));
|
||||
}
|
||||
|
||||
auto const result = spec_.process(json_);
|
||||
EXPECT_EQ(result, GetParam().expectedResult);
|
||||
}
|
||||
|
||||
TEST_F(FieldProcessorTests, FieldSpecWithRequirementCheck)
|
||||
{
|
||||
auto const result = spec_.check(json_);
|
||||
EXPECT_EQ(result, check::Warnings{});
|
||||
}
|
||||
|
||||
struct FieldCheckerTestBundle {
|
||||
std::string name;
|
||||
std::optional<check::Warning> checkResult;
|
||||
std::optional<check::Warning> otherCheckResult;
|
||||
check::Warnings expectedWarnings;
|
||||
};
|
||||
|
||||
struct FieldCheckerTests : SpecsTests, testing::WithParamInterface<FieldCheckerTestBundle> {
|
||||
protected:
|
||||
FieldSpec spec_{"key", CheckMockRef(checkMock), CheckMockRef(anotherCheckMock)};
|
||||
boost::json::value json_;
|
||||
};
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
FieldSpecTestGroup,
|
||||
FieldCheckerTests,
|
||||
testing::Values(
|
||||
FieldCheckerTestBundle{"NoWarnings", std::nullopt, std::nullopt, check::Warnings{}},
|
||||
FieldCheckerTestBundle{
|
||||
"FirstWarning",
|
||||
check::Warning{WarningCode::WarnUnknown, "error1"},
|
||||
std::nullopt,
|
||||
check::Warnings{check::Warning{WarningCode::WarnUnknown, "error1"}}
|
||||
},
|
||||
FieldCheckerTestBundle{
|
||||
"SecondWarning",
|
||||
std::nullopt,
|
||||
check::Warning{WarningCode::WarnUnknown, "error2"},
|
||||
check::Warnings{check::Warning{WarningCode::WarnUnknown, "error2"}}
|
||||
},
|
||||
FieldCheckerTestBundle{
|
||||
"BothWarnings",
|
||||
check::Warning{WarningCode::WarnUnknown, "error1"},
|
||||
check::Warning{WarningCode::WarnUnknown, "error2"},
|
||||
check::Warnings{
|
||||
check::Warning{WarningCode::WarnUnknown, "error1"},
|
||||
check::Warning{WarningCode::WarnUnknown, "error2"}
|
||||
}
|
||||
}
|
||||
),
|
||||
[](testing::TestParamInfo<FieldCheckerTestBundle> const& info) { return info.param.name; }
|
||||
);
|
||||
|
||||
TEST_F(FieldCheckerTests, FieldSpecWithCheckProcess)
|
||||
{
|
||||
auto const result = spec_.process(json_);
|
||||
EXPECT_EQ(result, MaybeError{});
|
||||
}
|
||||
|
||||
TEST_P(FieldCheckerTests, FieldSpecWithCheck)
|
||||
{
|
||||
EXPECT_CALL(checkMock, check).WillOnce(testing::Return(GetParam().checkResult));
|
||||
EXPECT_CALL(anotherCheckMock, check).WillOnce(testing::Return(GetParam().otherCheckResult));
|
||||
auto const result = spec_.check(json_);
|
||||
EXPECT_EQ(result, GetParam().expectedWarnings);
|
||||
}
|
||||
|
||||
struct RpcSpecProcessTests : SpecsTests, testing::WithParamInterface<ProcessorTestBundle> {
|
||||
RpcSpec spec{
|
||||
{"key1", RequirementMockRef(requirementMock)},
|
||||
{"key2", RequirementMockRef(anotherRequirementMock)}
|
||||
};
|
||||
boost::json::value json;
|
||||
};
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
RpcSpecProcessTestGroup,
|
||||
RpcSpecProcessTests,
|
||||
testing::Values(
|
||||
ProcessorTestBundle{"NoErrors", MaybeError{}, MaybeError{}, MaybeError{}},
|
||||
ProcessorTestBundle{
|
||||
"FirstError",
|
||||
Error{Status{"error1"}},
|
||||
std::nullopt,
|
||||
Error{Status{"error1"}},
|
||||
},
|
||||
ProcessorTestBundle{
|
||||
"SecondError",
|
||||
MaybeError{},
|
||||
Error{Status{"error2"}},
|
||||
Error{Status{"error2"}},
|
||||
}
|
||||
),
|
||||
[](testing::TestParamInfo<ProcessorTestBundle> const& info) { return info.param.name; }
|
||||
);
|
||||
|
||||
TEST_P(RpcSpecProcessTests, Process)
|
||||
{
|
||||
EXPECT_CALL(requirementMock, verify).WillOnce(testing::Return(GetParam().requirementResult));
|
||||
if (GetParam().otherRequirementResult.has_value()) {
|
||||
EXPECT_CALL(anotherRequirementMock, verify)
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
.WillOnce(testing::Return(GetParam().otherRequirementResult.value()));
|
||||
}
|
||||
auto const result = spec.process(json);
|
||||
EXPECT_EQ(result, GetParam().expectedResult);
|
||||
}
|
||||
|
||||
struct RpcSpecCheckTestBundle {
|
||||
std::string name;
|
||||
std::optional<check::Warning> checkResult;
|
||||
std::optional<check::Warning> otherCheckResult;
|
||||
std::unordered_map<int, std::vector<std::string>> expectedWarnings;
|
||||
};
|
||||
|
||||
struct RpcSpecCheckTests : SpecsTests, testing::WithParamInterface<RpcSpecCheckTestBundle> {
|
||||
protected:
|
||||
RpcSpec spec_{{"key1", CheckMockRef(checkMock)}, {"key2", CheckMockRef(anotherCheckMock)}};
|
||||
boost::json::value json_;
|
||||
};
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
RpcSpecCheckTestGroup,
|
||||
RpcSpecCheckTests,
|
||||
testing::Values(
|
||||
RpcSpecCheckTestBundle{"NoWarnings", std::nullopt, std::nullopt, {}},
|
||||
RpcSpecCheckTestBundle{
|
||||
"FirstWarning",
|
||||
check::Warning{WarningCode::WarnUnknown, "error1"},
|
||||
std::nullopt,
|
||||
{{WarningCode::WarnUnknown, {"error1"}}}
|
||||
},
|
||||
RpcSpecCheckTestBundle{
|
||||
"SecondWarning",
|
||||
std::nullopt,
|
||||
check::Warning{WarningCode::WarnUnknown, "error2"},
|
||||
{{WarningCode::WarnUnknown, {"error2"}}}
|
||||
},
|
||||
RpcSpecCheckTestBundle{
|
||||
"BothWarnings",
|
||||
check::Warning{WarningCode::WarnUnknown, "error1"},
|
||||
check::Warning{WarningCode::WarnUnknown, "error2"},
|
||||
{{WarningCode::WarnUnknown, {"error1", "error2"}}}
|
||||
},
|
||||
RpcSpecCheckTestBundle{
|
||||
"DifferentWarningCodes",
|
||||
check::Warning{WarningCode::WarnUnknown, "error1"},
|
||||
check::Warning{WarningCode::WarnRpcClio, "error2"},
|
||||
{{WarningCode::WarnUnknown, {"error1"}}, {WarningCode::WarnRpcClio, {"error2"}}}
|
||||
}
|
||||
|
||||
),
|
||||
[](testing::TestParamInfo<RpcSpecCheckTestBundle> const& info) { return info.param.name; }
|
||||
);
|
||||
|
||||
TEST_P(RpcSpecCheckTests, Check)
|
||||
{
|
||||
EXPECT_CALL(checkMock, check).WillOnce(testing::Return(GetParam().checkResult));
|
||||
EXPECT_CALL(anotherCheckMock, check).WillOnce(testing::Return(GetParam().otherCheckResult));
|
||||
|
||||
auto const result = spec_.check(json_);
|
||||
ASSERT_EQ(result.size(), GetParam().expectedWarnings.size());
|
||||
for (auto const& entry : result) {
|
||||
ASSERT_TRUE(entry.is_object());
|
||||
auto const& object = entry.as_object();
|
||||
ASSERT_TRUE(object.contains("id"));
|
||||
ASSERT_TRUE(object.at("id").is_int64());
|
||||
ASSERT_TRUE(object.contains("message"));
|
||||
ASSERT_TRUE(object.at("message").is_string());
|
||||
auto it = GetParam().expectedWarnings.find(object.at("id").as_int64());
|
||||
ASSERT_NE(it, GetParam().expectedWarnings.end());
|
||||
for (auto const& message : it->second) {
|
||||
EXPECT_NE(object.at("message").as_string().find(message), std::string::npos);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "rpc/common/AnyHandler.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
#include "rpc/common/Validators.hpp"
|
||||
#include "rpc/handlers/AccountMPTokenIssuances.hpp"
|
||||
#include "util/HandlerBaseTestFixture.hpp"
|
||||
#include "util/NameGenerator.hpp"
|
||||
@@ -1213,11 +1212,8 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, MPTokenIssuanceIdIsDerivedIdNotLed
|
||||
EXPECT_EQ(mptIssuanceId, expectedMptIssuanceId(kSequence));
|
||||
EXPECT_NE(mptIssuanceId, kIssuanceIndex1);
|
||||
|
||||
auto const asOuterObject = boost::json::value{{"mpt_issuance_id", mptIssuanceId}};
|
||||
auto const validated = validation::CustomValidators::uint192HexStringValidator.verify(
|
||||
asOuterObject, "mpt_issuance_id"
|
||||
);
|
||||
EXPECT_TRUE(validated.has_value());
|
||||
xrpl::uint192 parsedMptIssuanceId;
|
||||
EXPECT_TRUE(parsedMptIssuanceId.parseHex(mptIssuanceId));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "rpc/FakesAndMocks.hpp"
|
||||
#include "rpc/common/Concepts.hpp"
|
||||
#include "rpc/common/Specs.hpp"
|
||||
#include "rpc/common/Types.hpp"
|
||||
#include "rpc/common/Validators.hpp"
|
||||
#include "rpc/common/impl/Processors.hpp"
|
||||
#include "util/HandlerBaseTestFixture.hpp"
|
||||
|
||||
@@ -15,30 +13,15 @@ using namespace testing;
|
||||
using namespace std;
|
||||
|
||||
using namespace rpc;
|
||||
using namespace rpc::validation;
|
||||
using namespace tests::common;
|
||||
|
||||
class RPCDefaultProcessorTest : public HandlerBaseTest {};
|
||||
|
||||
TEST_F(RPCDefaultProcessorTest, ValidInput)
|
||||
{
|
||||
runSpawn([](auto yield) {
|
||||
HandlerMock const handler;
|
||||
rpc::impl::DefaultProcessor<HandlerMock> const processor;
|
||||
static_assert(SomeHandlerWithTypedInput<TypedHandlerFake>);
|
||||
static_assert(SomeHandlerWithTypedInput<FailingTypedHandlerFake>);
|
||||
static_assert(SomeHandlerWithoutInput<HandlerWithoutInputMock>);
|
||||
|
||||
auto const input = boost::json::parse(R"JSON({ "something": "works" })JSON");
|
||||
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, Context{yield});
|
||||
ASSERT_TRUE(ret); // no error
|
||||
EXPECT_TRUE(ret.warnings.empty());
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCDefaultProcessorTest, NoInputValidCall)
|
||||
TEST_F(RPCDefaultProcessorTest, NoInputHandler_ValidCall)
|
||||
{
|
||||
runSpawn([](auto yield) {
|
||||
HandlerWithoutInputMock const handler;
|
||||
@@ -54,34 +37,10 @@ TEST_F(RPCDefaultProcessorTest, NoInputValidCall)
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCDefaultProcessorTest, InvalidInput)
|
||||
{
|
||||
runSpawn([](auto yield) {
|
||||
HandlerMock const handler;
|
||||
rpc::impl::DefaultProcessor<HandlerMock> const processor;
|
||||
// These exercise the input path of a handler whose spec, validation and
|
||||
// deserialization all come from the shared consteval spec via HandlerFor<Input>.
|
||||
|
||||
auto const input = boost::json::parse(R"JSON({ "other": "nope" })JSON");
|
||||
auto const spec = RpcSpec{{"something", Required{}}};
|
||||
EXPECT_CALL(handler, spec(_)).WillOnce(ReturnRef(spec));
|
||||
|
||||
auto const ret = processor(handler, input, Context{yield});
|
||||
ASSERT_FALSE(ret); // returns error
|
||||
EXPECT_TRUE(ret.warnings.empty());
|
||||
});
|
||||
}
|
||||
|
||||
static_assert(SomeHandlerWithTypedInput<TypedHandlerFake>);
|
||||
static_assert(not SomeHandlerWithInput<TypedHandlerFake>);
|
||||
static_assert(SomeHandlerWithTypedInput<FailingTypedHandlerFake>);
|
||||
static_assert(SomeHandlerWithInput<HandlerMock>);
|
||||
static_assert(not SomeHandlerWithTypedInput<HandlerMock>);
|
||||
|
||||
// The four tests below exercise the typed path — a handler whose spec, validation and
|
||||
// deserialization all come from the shared consteval spec via HandlerFor<Input>. They run
|
||||
// against the same DefaultProcessor as the legacy tests above, which is the point: the
|
||||
// dual path is a dispatch detail, not a second processor.
|
||||
|
||||
TEST_F(RPCDefaultProcessorTest, NewSpecHandler_HappyPath)
|
||||
TEST_F(RPCDefaultProcessorTest, SpecHandler_HappyPath)
|
||||
{
|
||||
runSpawn([](auto yield) {
|
||||
TypedHandlerFake const handler;
|
||||
@@ -96,7 +55,7 @@ TEST_F(RPCDefaultProcessorTest, NewSpecHandler_HappyPath)
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCDefaultProcessorTest, NewSpecHandler_MissingRequiredField_ReturnsError)
|
||||
TEST_F(RPCDefaultProcessorTest, SpecHandler_MissingRequiredField_ReturnsError)
|
||||
{
|
||||
runSpawn([](auto yield) {
|
||||
TypedHandlerFake const handler;
|
||||
@@ -110,7 +69,7 @@ TEST_F(RPCDefaultProcessorTest, NewSpecHandler_MissingRequiredField_ReturnsError
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCDefaultProcessorTest, NewSpecHandler_DeprecatedField_WarningsForwarded)
|
||||
TEST_F(RPCDefaultProcessorTest, SpecHandler_DeprecatedField_WarningsForwarded)
|
||||
{
|
||||
runSpawn([](auto yield) {
|
||||
TypedHandlerFake const handler;
|
||||
@@ -124,7 +83,7 @@ TEST_F(RPCDefaultProcessorTest, NewSpecHandler_DeprecatedField_WarningsForwarded
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCDefaultProcessorTest, NewSpecHandler_HandlerReturnsError_ForwardsError)
|
||||
TEST_F(RPCDefaultProcessorTest, SpecHandler_HandlerReturnsError_ForwardsError)
|
||||
{
|
||||
runSpawn([](auto yield) {
|
||||
FailingTypedHandlerFake const handler;
|
||||
@@ -139,7 +98,7 @@ TEST_F(RPCDefaultProcessorTest, NewSpecHandler_HandlerReturnsError_ForwardsError
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCDefaultProcessorTest, NewSpecHandler_HandlerReturnsError_StillForwardsWarnings)
|
||||
TEST_F(RPCDefaultProcessorTest, SpecHandler_HandlerReturnsError_StillForwardsWarnings)
|
||||
{
|
||||
runSpawn([](auto yield) {
|
||||
FailingTypedHandlerFake const handler;
|
||||
@@ -154,7 +113,7 @@ TEST_F(RPCDefaultProcessorTest, NewSpecHandler_HandlerReturnsError_StillForwards
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCDefaultProcessorTest, NewSpecHandler_DeprecatedFieldAbsent_NoWarnings)
|
||||
TEST_F(RPCDefaultProcessorTest, SpecHandler_DeprecatedFieldAbsent_NoWarnings)
|
||||
{
|
||||
runSpawn([](auto yield) {
|
||||
TypedHandlerFake const handler;
|
||||
|
||||
@@ -246,7 +246,8 @@ generateTestValuesForParametersTest()
|
||||
kAccount
|
||||
),
|
||||
.expectedError = "malformedRequest",
|
||||
.expectedErrorMessage = "Must have one of authorized or authorized_credentials."
|
||||
.expectedErrorMessage =
|
||||
"Must have exactly one of `authorized` and `authorized_credentials`."
|
||||
},
|
||||
|
||||
ParamTestCaseBundle{
|
||||
@@ -270,7 +271,8 @@ generateTestValuesForParametersTest()
|
||||
kCredentialType
|
||||
),
|
||||
.expectedError = "malformedRequest",
|
||||
.expectedErrorMessage = "Must have one of authorized or authorized_credentials."
|
||||
.expectedErrorMessage =
|
||||
"Must have exactly one of `authorized` and `authorized_credentials`."
|
||||
},
|
||||
|
||||
ParamTestCaseBundle{
|
||||
@@ -805,7 +807,7 @@ generateTestValuesForParametersTest()
|
||||
"directory": {}
|
||||
})JSON",
|
||||
.expectedError = "invalidParams",
|
||||
.expectedErrorMessage = "missingOwnerOrDirRoot"
|
||||
.expectedErrorMessage = "Must have exactly one of `owner` and `dir_root` fields."
|
||||
},
|
||||
|
||||
ParamTestCaseBundle{
|
||||
@@ -865,7 +867,7 @@ generateTestValuesForParametersTest()
|
||||
kAccount
|
||||
),
|
||||
.expectedError = "invalidParams",
|
||||
.expectedErrorMessage = "mayNotSpecifyBothDirRootAndOwner"
|
||||
.expectedErrorMessage = "Must have exactly one of `owner` and `dir_root` fields."
|
||||
},
|
||||
|
||||
ParamTestCaseBundle{
|
||||
|
||||
@@ -59,6 +59,53 @@ TEST_F(RPCNFTInfoHandlerTest, NonHexLedgerHash)
|
||||
});
|
||||
}
|
||||
|
||||
// nft_info is Clio-only, so ForwardingProxy::shouldForward returns false before it gets to the
|
||||
// current/closed check and the request is dispatched here rather than sent to xrpld. Clio holds
|
||||
// neither ledger, so the resolver has to reject them itself.
|
||||
TEST_F(RPCNFTInfoHandlerTest, CurrentLedgerIndexRejected)
|
||||
{
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
auto const handler = AnyHandler{NFTInfoHandler{backend_}};
|
||||
auto const input = boost::json::parse(
|
||||
fmt::format(
|
||||
R"JSON({{
|
||||
"nft_id": "{}",
|
||||
"ledger_index": "current"
|
||||
}})JSON",
|
||||
kNftId
|
||||
)
|
||||
);
|
||||
auto const output = handler.process(input, Context{.yield = yield});
|
||||
ASSERT_FALSE(output);
|
||||
|
||||
auto const err = rpc::makeError(output.result.error());
|
||||
EXPECT_EQ(err.at("error").as_string(), "invalidParams");
|
||||
EXPECT_EQ(err.at("error_message").as_string(), "ledgerIndexMalformed");
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCNFTInfoHandlerTest, ClosedLedgerIndexRejected)
|
||||
{
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
auto const handler = AnyHandler{NFTInfoHandler{backend_}};
|
||||
auto const input = boost::json::parse(
|
||||
fmt::format(
|
||||
R"JSON({{
|
||||
"nft_id": "{}",
|
||||
"ledger_index": "closed"
|
||||
}})JSON",
|
||||
kNftId
|
||||
)
|
||||
);
|
||||
auto const output = handler.process(input, Context{.yield = yield});
|
||||
ASSERT_FALSE(output);
|
||||
|
||||
auto const err = rpc::makeError(output.result.error());
|
||||
EXPECT_EQ(err.at("error").as_string(), "invalidParams");
|
||||
EXPECT_EQ(err.at("error_message").as_string(), "ledgerIndexMalformed");
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(RPCNFTInfoHandlerTest, NonStringLedgerHash)
|
||||
{
|
||||
runSpawn([this](boost::asio::yield_context yield) {
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
using namespace std;
|
||||
using namespace rpc;
|
||||
using namespace rpc::validation;
|
||||
using namespace tests::common;
|
||||
|
||||
class RPCTestHandlerTest : public HandlerBaseTest {};
|
||||
@@ -19,7 +18,7 @@ class RPCTestHandlerTest : public HandlerBaseTest {};
|
||||
TEST_F(RPCTestHandlerTest, HandlerSuccess)
|
||||
{
|
||||
runSpawn([](auto yield) {
|
||||
auto const handler = AnyHandler{HandlerFake{}};
|
||||
auto const handler = AnyHandler{TypedHandlerFake{}};
|
||||
auto const input = boost::json::parse(R"JSON({
|
||||
"hello": "world",
|
||||
"limit": 10
|
||||
@@ -48,7 +47,7 @@ TEST_F(RPCTestHandlerTest, NoInputHandlerSuccess)
|
||||
TEST_F(RPCTestHandlerTest, HandlerErrorHandling)
|
||||
{
|
||||
runSpawn([](auto yield) {
|
||||
auto const handler = AnyHandler{HandlerFake{}};
|
||||
auto const handler = AnyHandler{TypedHandlerFake{}};
|
||||
auto const input = boost::json::parse(R"JSON({
|
||||
"hello": "not world",
|
||||
"limit": 10
|
||||
@@ -67,7 +66,7 @@ TEST_F(RPCTestHandlerTest, HandlerErrorHandling)
|
||||
TEST_F(RPCTestHandlerTest, HandlerInnerErrorHandling)
|
||||
{
|
||||
runSpawn([](auto yield) {
|
||||
auto const handler = AnyHandler{FailingHandlerFake{}};
|
||||
auto const handler = AnyHandler{FailingTypedHandlerFake{}};
|
||||
auto const input = boost::json::parse(R"JSON({
|
||||
"hello": "world",
|
||||
"limit": 10
|
||||
|
||||
@@ -70,27 +70,27 @@ generateTestValuesForParametersTest()
|
||||
.testJson = R"JSON({
|
||||
"idk": "idk"
|
||||
})JSON",
|
||||
.expectedError = "malformedRequest",
|
||||
.expectedErrorCode = ClioError::RpcMalformedRequest,
|
||||
.expectedErrorMessage = "Malformed request."
|
||||
.expectedError = "invalidParams",
|
||||
.expectedErrorCode = RippledError::RpcInvalidParams,
|
||||
.expectedErrorMessage = "Must specify either 'vault_id' or both 'owner' and 'seq'."
|
||||
},
|
||||
VaultInfoParamTestCaseBundle{
|
||||
.testName = "MissingOwnerInVault",
|
||||
.testJson = R"JSON({
|
||||
"seq": 4
|
||||
})JSON",
|
||||
.expectedError = "malformedRequest",
|
||||
.expectedErrorCode = ClioError::RpcMalformedRequest,
|
||||
.expectedErrorMessage = "Malformed request."
|
||||
.expectedError = "invalidParams",
|
||||
.expectedErrorCode = RippledError::RpcInvalidParams,
|
||||
.expectedErrorMessage = "Must specify either 'vault_id' or both 'owner' and 'seq'."
|
||||
},
|
||||
VaultInfoParamTestCaseBundle{
|
||||
.testName = "MissingSeqInVault",
|
||||
.testJson = R"JSON({
|
||||
"owner": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"
|
||||
})JSON",
|
||||
.expectedError = "malformedRequest",
|
||||
.expectedErrorCode = ClioError::RpcMalformedRequest,
|
||||
.expectedErrorMessage = "Malformed request."
|
||||
.expectedError = "invalidParams",
|
||||
.expectedErrorCode = RippledError::RpcInvalidParams,
|
||||
.expectedErrorMessage = "Must specify either 'vault_id' or both 'owner' and 'seq'."
|
||||
},
|
||||
VaultInfoParamTestCaseBundle{
|
||||
.testName = "SeqNotAnInteger",
|
||||
@@ -150,9 +150,9 @@ generateTestValuesForParametersTest()
|
||||
kVaultId,
|
||||
kAccount
|
||||
),
|
||||
.expectedError = "malformedRequest",
|
||||
.expectedErrorCode = ClioError::RpcMalformedRequest,
|
||||
.expectedErrorMessage = "Malformed request."
|
||||
.expectedError = "invalidParams",
|
||||
.expectedErrorCode = RippledError::RpcInvalidParams,
|
||||
.expectedErrorMessage = "Must specify either 'vault_id' or both 'owner' and 'seq'."
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user