refactor: Migrate handlers to rpc-spec (G) (#3213)

This commit is contained in:
Alex Kremer
2026-09-17 16:33:22 +01:00
committed by GitHub
parent 0e8ee75caf
commit 1cb6f29b40
33 changed files with 221 additions and 3486 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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;

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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);

View File

@@ -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`."
}};
}

View File

@@ -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");

View File

@@ -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)
);

View File

@@ -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

View File

@@ -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));
}

View File

@@ -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",

View File

@@ -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);
}

View File

@@ -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));

View File

@@ -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");
});
}

View File

@@ -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"));
}

View File

@@ -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);
}
}
}

View File

@@ -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));
});
}

View File

@@ -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;

View File

@@ -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{

View File

@@ -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) {

View File

@@ -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

View File

@@ -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'."
}
};
}