From 023e02da1533d91b8af2c0f2c2bf3bb1e45d00cc Mon Sep 17 00:00:00 2001 From: Alex Kremer Date: Thu, 2 Feb 2023 13:16:01 +0000 Subject: [PATCH] Implement base for nextgen rpc subsystem (#487) Fixes #494 --- CMakeLists.txt | 19 +- src/rpc/Errors.h | 2 +- src/rpc/common/AnyHandler.h | 103 +++++++ src/rpc/common/Concepts.h | 60 ++++ src/rpc/common/Specs.cpp | 42 +++ src/rpc/common/Specs.h | 70 +++++ src/rpc/common/Types.h | 56 ++++ src/rpc/common/Validators.cpp | 89 ++++++ src/rpc/common/Validators.h | 259 +++++++++++++++++ src/rpc/common/impl/Factories.h | 59 ++++ src/rpc/common/impl/Processors.h | 52 ++++ src/util/Expected.h | 264 ++++++++++++++++++ unittests/Playground.cpp | 31 ++ unittests/rpc/BaseTests.cpp | 245 ++++++++++++++++ .../{RPCErrors.cpp => rpc/ErrorTests.cpp} | 0 .../rpc/handlers/DefaultProcessorTests.cpp | 66 +++++ unittests/rpc/handlers/TestHandlerTests.cpp | 84 ++++++ unittests/rpc/handlers/impl/FakesAndMocks.h | 168 +++++++++++ 18 files changed, 1662 insertions(+), 7 deletions(-) create mode 100644 src/rpc/common/AnyHandler.h create mode 100644 src/rpc/common/Concepts.h create mode 100644 src/rpc/common/Specs.cpp create mode 100644 src/rpc/common/Specs.h create mode 100644 src/rpc/common/Types.h create mode 100644 src/rpc/common/Validators.cpp create mode 100644 src/rpc/common/Validators.h create mode 100644 src/rpc/common/impl/Factories.h create mode 100644 src/rpc/common/impl/Processors.h create mode 100644 src/util/Expected.h create mode 100644 unittests/Playground.cpp create mode 100644 unittests/rpc/BaseTests.cpp rename unittests/{RPCErrors.cpp => rpc/ErrorTests.cpp} (100%) create mode 100644 unittests/rpc/handlers/DefaultProcessorTests.cpp create mode 100644 unittests/rpc/handlers/TestHandlerTests.cpp create mode 100644 unittests/rpc/handlers/impl/FakesAndMocks.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 15fc4169..f8ba0525 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,6 +52,9 @@ target_sources(clio PRIVATE src/rpc/RPCHelpers.cpp src/rpc/Counters.cpp src/rpc/WorkQueue.cpp + ## NextGen RPC + src/rpc/common/Specs.cpp + src/rpc/common/Validators.cpp ## RPC Methods # Account src/rpc/handlers/AccountChannels.cpp @@ -97,12 +100,16 @@ target_link_libraries(clio_server PUBLIC clio) if(BUILD_TESTS) set(TEST_TARGET clio_tests) add_executable(${TEST_TARGET} - unittests/RPCErrors.cpp - unittests/Backend.cpp - unittests/Logger.cpp - unittests/Config.cpp - unittests/ProfilerTest.cpp - unittests/DOSGuard.cpp) + unittests/Playground.cpp + unittests/Backend.cpp + unittests/Logger.cpp + unittests/Config.cpp + unittests/ProfilerTest.cpp + unittests/DOSGuard.cpp + unittests/rpc/ErrorTests.cpp + unittests/rpc/BaseTests.cpp + unittests/rpc/handlers/TestHandlerTests.cpp + unittests/rpc/handlers/DefaultProcessorTests.cpp) include(CMake/deps/gtest.cmake) # if CODE_COVERAGE enable, add clio_test-ccov diff --git a/src/rpc/Errors.h b/src/rpc/Errors.h index 708ddc21..5e7caf17 100644 --- a/src/rpc/Errors.h +++ b/src/rpc/Errors.h @@ -21,7 +21,7 @@ #include -#include +#include #include #include diff --git a/src/rpc/common/AnyHandler.h b/src/rpc/common/AnyHandler.h new file mode 100644 index 00000000..1dcb44d9 --- /dev/null +++ b/src/rpc/common/AnyHandler.h @@ -0,0 +1,103 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include +#include +#include + +namespace RPCng { + +/** + * @brief A type-erased Handler that can contain any (NextGen) RPC handler class + */ +class AnyHandler final +{ +public: + template < + Handler HandlerType, + typename ProcessingStrategy = detail::DefaultProcessor> + /* implicit */ AnyHandler(HandlerType&& handler) + : pimpl_{std::make_unique>( + std::forward(handler))} + { + } + + ~AnyHandler() = default; + AnyHandler(AnyHandler const& other) : pimpl_{other.pimpl_->clone()} + { + } + AnyHandler& + operator=(AnyHandler const& rhs) + { + AnyHandler copy{rhs}; + pimpl_.swap(copy.pimpl_); + return *this; + } + AnyHandler(AnyHandler&&) = default; + AnyHandler& + operator=(AnyHandler&&) = default; + + [[nodiscard]] ReturnType + process(boost::json::value const& value) const + { + return pimpl_->process(value); + } + +private: + struct Concept + { + virtual ~Concept() = default; + + [[nodiscard]] virtual ReturnType + process(boost::json::value const& value) const = 0; + + [[nodiscard]] virtual std::unique_ptr + clone() const = 0; + }; + + template + struct Model : Concept + { + HandlerType handler; + ProcessorType processor; + + Model(HandlerType&& handler) : handler{std::move(handler)} + { + } + + [[nodiscard]] ReturnType + process(boost::json::value const& value) const override + { + return processor(handler, value); + } + + [[nodiscard]] std::unique_ptr + clone() const override + { + return std::make_unique(*this); + } + }; + +private: + std::unique_ptr pimpl_; +}; + +} // namespace RPCng diff --git a/src/rpc/common/Concepts.h b/src/rpc/common/Concepts.h new file mode 100644 index 00000000..bbfed99e --- /dev/null +++ b/src/rpc/common/Concepts.h @@ -0,0 +1,60 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include + +#include +#include + +#include + +namespace RPCng { + +struct RpcSpec; + +/** + * @brief A concept that specifies what a requirement used with @ref FieldSpec + * must provide + */ +// clang-format off +template +concept Requirement = requires(T a) { + { a.verify(boost::json::value{}, std::string{}) } -> std::same_as; +}; +// clang-format on + +/** + * @brief A concept that specifies what a Handler type must provide + * + * Note that value_from and value_to should be implemented using tag_invoke + * as per boost::json documentation for these functions. + */ +// clang-format off +template +concept Handler = requires(T a, typename T::Input in, typename T::Output out) { + { a.spec() } -> std::same_as; + { a.process(in) } -> std::same_as>; +} +&& boost::json::has_value_from::value +&& boost::json::has_value_to::value; +// clang-format on + +} // namespace RPCng diff --git a/src/rpc/common/Specs.cpp b/src/rpc/common/Specs.cpp new file mode 100644 index 00000000..9fdc5d32 --- /dev/null +++ b/src/rpc/common/Specs.cpp @@ -0,0 +1,42 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include + +namespace RPCng { + +[[nodiscard]] MaybeError +FieldSpec::validate(boost::json::value const& value) const +{ + return validator_(value); +} + +[[nodiscard]] MaybeError +RpcSpec::validate(boost::json::value const& value) const +{ + for (auto const& field : fields_) + if (auto ret = field.validate(value); not ret) + return Error{ret.error()}; + + return {}; +} + +} // namespace RPCng diff --git a/src/rpc/common/Specs.h b/src/rpc/common/Specs.h new file mode 100644 index 00000000..5fa3ed83 --- /dev/null +++ b/src/rpc/common/Specs.h @@ -0,0 +1,70 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include +#include +#include + +#include +#include + +namespace RPCng { + +/** + * @brief Represents a Specification for one field of an RPC command + */ +struct FieldSpec final +{ + template + FieldSpec(std::string const& key, Requirements&&... requirements) + : validator_{detail::makeFieldValidator( + key, + std::forward(requirements)...)} + { + } + + [[nodiscard]] MaybeError + validate(boost::json::value const& value) const; + +private: + std::function validator_; +}; + +/** + * @brief Represents a Specification of an entire RPC command + * + * Note: this should really be all constexpr and handlers would expose + * static constexpr RpcSpec spec instead. Maybe some day in the future. + */ +struct RpcSpec final +{ + RpcSpec(std::initializer_list fields) : fields_{fields} + { + } + + [[nodiscard]] MaybeError + validate(boost::json::value const& value) const; + +private: + std::vector fields_; +}; + +} // namespace RPCng diff --git a/src/rpc/common/Types.h b/src/rpc/common/Types.h new file mode 100644 index 00000000..b62a8ad7 --- /dev/null +++ b/src/rpc/common/Types.h @@ -0,0 +1,56 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include +#include + +#include + +namespace RPCng { + +/** + * @brief Return type used for Validators that can return error but don't have + * specific value to return + */ +using MaybeError = util::Expected; + +/** + * @brief The type that represents just the error part of @ref MaybeError + */ +using Error = util::Unexpected; + +/** + * @brief Return type for each individual handler + */ +template +using HandlerReturnType = util::Expected; + +/** + * @brief The final return type out of RPC engine + */ +using ReturnType = util::Expected; + +struct RpcSpec; +struct FieldSpec; + +using RpcSpecConstRef = RpcSpec const&; + +} // namespace RPCng diff --git a/src/rpc/common/Validators.cpp b/src/rpc/common/Validators.cpp new file mode 100644 index 00000000..1ff1edf6 --- /dev/null +++ b/src/rpc/common/Validators.cpp @@ -0,0 +1,89 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include + +#include + +namespace RPCng::validation { + +[[nodiscard]] MaybeError +Section::verify(boost::json::value const& value, std::string_view key) const +{ + if (not value.is_object() or not value.as_object().contains(key.data())) + return {}; // ignore. field does not exist, let 'required' fail + // instead + + auto const& res = value.at(key.data()); + for (auto const& spec : specs) + { + if (auto const ret = spec.validate(res); not ret) + return Error{ret.error()}; + } + return {}; +} + +[[nodiscard]] MaybeError +Required::verify(boost::json::value const& value, std::string_view key) const +{ + if (not value.is_object() or not value.as_object().contains(key.data())) + return Error{RPC::Status{ + RPC::RippledError::rpcINVALID_PARAMS, + "Required field '" + std::string{key} + "' missing"}}; + + return {}; +} + +[[nodiscard]] MaybeError +ValidateArrayAt::verify(boost::json::value const& value, std::string_view key) + const +{ + if (not value.is_object() or not value.as_object().contains(key.data())) + return {}; // ignore. field does not exist, let 'required' fail + // instead + + if (not value.as_object().at(key.data()).is_array()) + return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS}}; + + auto const& arr = value.as_object().at(key.data()).as_array(); + if (idx_ >= arr.size()) + return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS}}; + + auto const& res = arr.at(idx_); + for (auto const& spec : specs_) + if (auto const ret = spec.validate(res); not ret) + return Error{ret.error()}; + + return {}; +} + +[[nodiscard]] MaybeError +CustomValidator::verify(boost::json::value const& value, std::string_view key) + const +{ + if (not value.is_object() or not value.as_object().contains(key.data())) + return {}; // ignore. field does not exist, let 'required' fail + // instead + + return validator_(value.as_object().at(key.data()), key); +} + +} // namespace RPCng::validation diff --git a/src/rpc/common/Validators.h b/src/rpc/common/Validators.h new file mode 100644 index 00000000..b4443e85 --- /dev/null +++ b/src/rpc/common/Validators.h @@ -0,0 +1,259 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include +#include +#include + +namespace RPCng::validation { + +/** + * @brief Check that the type is the same as what was expected + * + * @tparam Expected The expected type that value should be convertible to + * @param value The json value to check the type of + * @return true if convertible; false otherwise + */ +template +[[nodiscard]] bool static checkType(boost::json::value const& value) +{ + auto hasError = false; + if constexpr (std::is_same_v) + { + if (not value.is_bool()) + hasError = true; + } + else if constexpr (std::is_same_v) + { + if (not value.is_string()) + hasError = true; + } + else if constexpr ( + std::is_same_v or std::is_same_v) + { + if (not value.is_double()) + hasError = true; + } + else if constexpr ( + std::is_convertible_v or + std::is_convertible_v) + { + if (not value.is_int64() && not value.is_uint64()) + hasError = true; + } + else if constexpr (std::is_same_v) + { + if (not value.is_array()) + hasError = true; + } + + return not hasError; +} + +/** + * @brief A meta-validator that acts as a spec for a sub-object/section + */ +class Section final +{ + std::vector specs; + +public: + explicit Section(std::initializer_list specs) : specs{specs} + { + } + + [[nodiscard]] MaybeError + verify(boost::json::value const& value, std::string_view key) const; +}; + +/** + * @brief A validator that simply requires a field to be present + */ +struct Required final +{ + [[nodiscard]] MaybeError + verify(boost::json::value const& value, std::string_view key) const; +}; + +/** + * @brief Validates that the type of the value is one of the given types + */ +template +struct Type final +{ + [[nodiscard]] MaybeError + verify(boost::json::value const& value, std::string_view key) const + { + if (not value.is_object() or not value.as_object().contains(key.data())) + return {}; // ignore. field does not exist, let 'required' fail + // instead + + auto const& res = value.as_object().at(key.data()); + auto const convertible = (checkType(res) || ...); + + if (not convertible) + return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS}}; + + return {}; + } +}; + +/** + * @brief Validate that value is between specified min and max + */ +template +class Between final +{ + Type min_; + Type max_; + +public: + explicit Between(Type min, Type max) : min_{min}, max_{max} + { + } + + [[nodiscard]] MaybeError + verify(boost::json::value const& value, std::string_view key) const + { + if (not value.is_object() or not value.as_object().contains(key.data())) + return {}; // ignore. field does not exist, let 'required' fail + // instead + + using boost::json::value_to; + auto const res = value_to(value.as_object().at(key.data())); + // todo: may want a way to make this code more generic (e.g. use a free + // function that can be overridden for this comparison) + if (res < min_ || res > max_) + return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS}}; + + return {}; + } +}; + +/** + * @brief Validates that the value is equal to the one passed in + */ +template +class EqualTo final +{ + Type original_; + +public: + explicit EqualTo(Type original) : original_{original} + { + } + + [[nodiscard]] MaybeError + verify(boost::json::value const& value, std::string_view key) const + { + if (not value.is_object() or not value.as_object().contains(key.data())) + return {}; // ignore. field does not exist, let 'required' fail + // instead + + using boost::json::value_to; + auto const res = value_to(value.as_object().at(key.data())); + if (res != original_) + return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS}}; + + return {}; + } +}; + +/** + * @brief Deduction guide to help disambiguate what it means to EqualTo a + * "string" without specifying the type. + */ +EqualTo(char const*)->EqualTo; + +/** + * @brief Validates that the value is one of the values passed in + */ +template +class OneOf final +{ + std::vector options_; + +public: + explicit OneOf(std::initializer_list options) : options_{options} + { + } + + [[nodiscard]] MaybeError + verify(boost::json::value const& value, std::string_view key) const + { + if (not value.is_object() or not value.as_object().contains(key.data())) + return {}; // ignore. field does not exist, let 'required' fail + // instead + + using boost::json::value_to; + auto const res = value_to(value.as_object().at(key.data())); + if (std::find(std::begin(options_), std::end(options_), res) == + std::end(options_)) + return Error{RPC::Status{RPC::RippledError::rpcINVALID_PARAMS}}; + + return {}; + } +}; + +/** + * @brief Deduction guide to help disambiguate what it means to OneOf a + * few "strings" without specifying the type. + */ +OneOf(std::initializer_list)->OneOf; + +/** + * @brief A meta-validator that specifies a list of specs to run against the + * object at the given index in the array + */ +class ValidateArrayAt final +{ + std::size_t idx_; + std::vector specs_; + +public: + ValidateArrayAt(std::size_t idx, std::initializer_list specs) + : idx_{idx}, specs_{specs} + { + } + + [[nodiscard]] MaybeError + verify(boost::json::value const& value, std::string_view key) const; +}; + +/** + * @brief A meta-validator that allows to specify a custom validation function + */ +class CustomValidator final +{ + std::function + validator_; + +public: + template + explicit CustomValidator(Fn&& fn) : validator_{std::forward(fn)} + { + } + + [[nodiscard]] MaybeError + verify(boost::json::value const& value, std::string_view key) const; +}; + +} // namespace RPCng::validation diff --git a/src/rpc/common/impl/Factories.h b/src/rpc/common/impl/Factories.h new file mode 100644 index 00000000..c1fcd7e9 --- /dev/null +++ b/src/rpc/common/impl/Factories.h @@ -0,0 +1,59 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include +#include + +#include + +#include + +namespace RPCng::detail { + +template +[[nodiscard]] auto +makeFieldValidator(std::string const& key, Requirements&&... requirements) +{ + return [key, ... r = std::forward(requirements)]( + boost::json::value const& j) -> MaybeError { + // clang-format off + std::optional firstFailure = std::nullopt; + + // This expands in order of Requirements and stops evaluating after + // first failure which is stored in `firstFailure` and can be checked + // later on to see whether the verification failed as a whole or not. + ([&j, &key, &firstFailure, req = &r]() { + if (firstFailure) + return; // already failed earlier - skip + + if (auto const res = req->verify(j, key); not res) + firstFailure = res.error(); + }(), ...); + // clang-format on + + if (firstFailure) + return Error{firstFailure.value()}; + + return {}; + }; +} + +} // namespace RPCng::detail diff --git a/src/rpc/common/impl/Processors.h b/src/rpc/common/impl/Processors.h new file mode 100644 index 00000000..d0724a3e --- /dev/null +++ b/src/rpc/common/impl/Processors.h @@ -0,0 +1,52 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include +#include + +namespace RPCng::detail { + +template +struct DefaultProcessor final +{ + [[nodiscard]] ReturnType + operator()(HandlerType const& handler, boost::json::value const& value) + const + { + using boost::json::value_from; + using boost::json::value_to; + + // first we run validation + auto const spec = handler.spec(); + if (auto const ret = spec.validate(value); not ret) + return Error{ret.error()}; // forward Status + + auto const inData = value_to(value); + + // real handler is given expected Input, not json + if (auto const ret = handler.process(inData); not ret) + return Error{ret.error()}; // forward Status + else + return value_from(ret.value()); + } +}; + +} // namespace RPCng::detail diff --git a/src/util/Expected.h b/src/util/Expected.h new file mode 100644 index 00000000..09f707ae --- /dev/null +++ b/src/util/Expected.h @@ -0,0 +1,264 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + Copyright (c) 2021 Ripple Labs Inc. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +/* + * NOTE: + * + * This entire file is taken from rippled and modified slightly to fit this + * codebase as well as fixing the original issue that made this necessary. + * + * The reason is that currently there is no easy way to injest the fix that is + * required to make this implementation correctly work with boost::json::value. + * Since this will be replaced by `std::expected` as soon as possible there is + * not much harm done in doing it this way. + */ + +#pragma once + +#include +#include +#include +#include + +namespace util { + +/** Expected is an approximation of std::expected (hoped for in C++23) + + See: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p0323r10.html + + The implementation is entirely based on boost::outcome_v2::result. +*/ + +// Exception thrown by an invalid access to Expected. +struct bad_expected_access : public std::runtime_error +{ + bad_expected_access() : runtime_error("bad expected access") + { + } +}; + +namespace detail { + +// Custom policy for Expected. Always throw on an invalid access. +struct throw_policy : public boost::outcome_v2::policy::base +{ + template + static constexpr void + wide_value_check(Impl&& self) + { + if (!base::_has_value(std::forward(self))) + ripple::Throw(); + } + + template + static constexpr void + wide_error_check(Impl&& self) + { + if (!base::_has_error(std::forward(self))) + ripple::Throw(); + } + + template + static constexpr void + wide_exception_check(Impl&& self) + { + if (!base::_has_exception(std::forward(self))) + ripple::Throw(); + } +}; + +} // namespace detail + +// Definition of Unexpected, which is used to construct the unexpected +// return type of an Expected. +template +class Unexpected +{ +public: + static_assert(!std::is_same::value, "E must not be void"); + + Unexpected() = delete; + + constexpr explicit Unexpected(E const& e) : val_(e) + { + } + + constexpr explicit Unexpected(E&& e) : val_(std::move(e)) + { + } + + constexpr const E& + value() const& + { + return val_; + } + + constexpr E& + value() & + { + return val_; + } + + constexpr E&& + value() && + { + return std::move(val_); + } + + constexpr const E&& + value() const&& + { + return std::move(val_); + } + +private: + E val_; +}; + +// Unexpected deduction guide that converts array to const*. +template +Unexpected(E (&)[N]) -> Unexpected; + +// Definition of Expected. All of the machinery comes from boost::result. +template +class [[nodiscard]] Expected + : private boost::outcome_v2::result +{ + using Base = boost::outcome_v2::result; + +public: + template < + typename U, + typename = std::enable_if_t>> + constexpr Expected(U r) : Base(T(std::forward(r))) + { + } + + template < + typename U, + typename = std::enable_if_t>> + constexpr Expected(Unexpected e) : Base(E(std::forward(e.value()))) + { + } + + constexpr bool + has_value() const + { + return Base::has_value(); + } + + constexpr T const& + value() const + { + return Base::value(); + } + + constexpr T& + value() + { + return Base::value(); + } + + constexpr E const& + error() const + { + return Base::error(); + } + + constexpr E& + error() + { + return Base::error(); + } + + constexpr explicit operator bool() const + { + return has_value(); + } + + // Add operator* and operator-> so the Expected API looks a bit more like + // what std::expected is likely to look like. See: + // http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p0323r10.html + [[nodiscard]] constexpr T& + operator*() + { + return this->value(); + } + + [[nodiscard]] constexpr T const& + operator*() const + { + return this->value(); + } + + [[nodiscard]] constexpr T* + operator->() + { + return &this->value(); + } + + [[nodiscard]] constexpr T const* + operator->() const + { + return &this->value(); + } +}; + +// Specialization of Expected. Allows returning either success +// (without a value) or the reason for the failure. +template +class [[nodiscard]] Expected + : private boost::outcome_v2::result +{ + using Base = boost::outcome_v2::result; + +public: + // The default constructor makes a successful Expected. + // This aligns with std::expected behavior proposed in P0323R10. + constexpr Expected() : Base(boost::outcome_v2::success()) + { + } + + template < + typename U, + typename = std::enable_if_t>> + constexpr Expected(Unexpected e) : Base(E(std::forward(e.value()))) + { + } + + constexpr E const& + error() const + { + return Base::error(); + } + + constexpr E& + error() + { + return Base::error(); + } + + constexpr explicit operator bool() const + { + return Base::has_value(); + } +}; + +} // namespace util diff --git a/unittests/Playground.cpp b/unittests/Playground.cpp new file mode 100644 index 00000000..99d5be89 --- /dev/null +++ b/unittests/Playground.cpp @@ -0,0 +1,31 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +using namespace clio; + +/* + * Use this file for temporary tests and implementations. + * Note: Please don't push your temporary work to the repo. + */ + +// TEST(PlaygroundTest, MyTest) +// { +// } diff --git a/unittests/rpc/BaseTests.cpp b/unittests/rpc/BaseTests.cpp new file mode 100644 index 00000000..9d203d1b --- /dev/null +++ b/unittests/rpc/BaseTests.cpp @@ -0,0 +1,245 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include + +using namespace testing; +using namespace clio; +using namespace std; + +using namespace RPCng; +using namespace RPCng::validation; + +namespace json = boost::json; + +class RPCBaseTest : public NoLoggerFixture +{ +}; + +TEST_F(RPCBaseTest, CheckType) +{ + auto const jstr = json::value("a string"); + ASSERT_TRUE(checkType(jstr)); + ASSERT_FALSE(checkType(jstr)); + + auto const juint = json::value(123u); + ASSERT_TRUE(checkType(juint)); + ASSERT_TRUE(checkType(juint)); + ASSERT_FALSE(checkType(juint)); + + auto const jint = json::value(123); + ASSERT_TRUE(checkType(jint)); + ASSERT_TRUE(checkType(jint)); + ASSERT_FALSE(checkType(jint)); + + auto const jbool = json::value(true); + ASSERT_TRUE(checkType(jbool)); + ASSERT_FALSE(checkType(jbool)); + + auto const jdouble = json::value(0.123); + ASSERT_TRUE(checkType(jdouble)); + ASSERT_TRUE(checkType(jdouble)); + ASSERT_FALSE(checkType(jdouble)); + + auto const jarr = json::value({1, 2, 3}); + ASSERT_TRUE(checkType(jarr)); + ASSERT_FALSE(checkType(jarr)); +} + +TEST_F(RPCBaseTest, TypeValidator) +{ + auto spec = RpcSpec{ + {"uint", Type{}}, + {"int", Type{}}, + {"str", Type{}}, + {"double", Type{}}, + {"bool", Type{}}, + {"arr", Type{}}, + }; + + auto passingInput = json::parse(R"({ + "uint": 123, + "int": 321, + "str": "a string", + "double": 1.0, + "bool": true, + "arr": [] + })"); + ASSERT_TRUE(spec.validate(passingInput)); + + { + auto failingInput = json::parse(R"({ "uint": "a string" })"); + ASSERT_FALSE(spec.validate(failingInput)); + } + { + auto failingInput = json::parse(R"({ "int": "a string" })"); + ASSERT_FALSE(spec.validate(failingInput)); + } + { + auto failingInput = json::parse(R"({ "str": 1234 })"); + ASSERT_FALSE(spec.validate(failingInput)); + } + { + auto failingInput = json::parse(R"({ "double": "a string" })"); + ASSERT_FALSE(spec.validate(failingInput)); + } + { + auto failingInput = json::parse(R"({ "bool": "a string" })"); + ASSERT_FALSE(spec.validate(failingInput)); + } + { + auto failingInput = json::parse(R"({ "arr": "a string" })"); + ASSERT_FALSE(spec.validate(failingInput)); + } +} + +TEST_F(RPCBaseTest, TypeValidatorMultipleTypes) +{ + auto spec = RpcSpec{ + // either int or string + {"test", Type{}}, + }; + + auto passingInput = json::parse(R"({ "test": "1234" })"); + ASSERT_TRUE(spec.validate(passingInput)); + + auto passingInput2 = json::parse(R"({ "test": 1234 })"); + ASSERT_TRUE(spec.validate(passingInput2)); + + auto failingInput = json::parse(R"({ "test": true })"); + ASSERT_FALSE(spec.validate(failingInput)); +} + +TEST_F(RPCBaseTest, RequiredValidator) +{ + auto spec = RpcSpec{ + {"required", Required{}}, + }; + + auto passingInput = json::parse(R"({ "required": "present" })"); + ASSERT_TRUE(spec.validate(passingInput)); + + auto passingInput2 = json::parse(R"({ "required": true })"); + ASSERT_TRUE(spec.validate(passingInput2)); + + auto failingInput = json::parse(R"({})"); + ASSERT_FALSE(spec.validate(failingInput)); +} + +TEST_F(RPCBaseTest, BetweenValidator) +{ + auto spec = RpcSpec{ + {"amount", Between{10u, 20u}}, + }; + + auto passingInput = json::parse(R"({ "amount": 15 })"); + ASSERT_TRUE(spec.validate(passingInput)); + + auto passingInput2 = json::parse(R"({ "amount": 10 })"); + ASSERT_TRUE(spec.validate(passingInput2)); + + auto passingInput3 = json::parse(R"({ "amount": 20 })"); + ASSERT_TRUE(spec.validate(passingInput3)); + + auto failingInput = json::parse(R"({ "amount": 9 })"); + ASSERT_FALSE(spec.validate(failingInput)); + + auto failingInput2 = json::parse(R"({ "amount": 21 })"); + ASSERT_FALSE(spec.validate(failingInput2)); +} + +TEST_F(RPCBaseTest, OneOfValidator) +{ + auto spec = RpcSpec{ + {"currency", OneOf{"XRP", "USD"}}, + }; + + auto passingInput = json::parse(R"({ "currency": "XRP" })"); + ASSERT_TRUE(spec.validate(passingInput)); + + auto passingInput2 = json::parse(R"({ "currency": "USD" })"); + ASSERT_TRUE(spec.validate(passingInput2)); + + auto failingInput = json::parse(R"({ "currency": "PRX" })"); + ASSERT_FALSE(spec.validate(failingInput)); +} + +TEST_F(RPCBaseTest, EqualToValidator) +{ + auto spec = RpcSpec{ + {"exact", EqualTo{"CaseSensitive"}}, + }; + + auto passingInput = json::parse(R"({ "exact": "CaseSensitive" })"); + ASSERT_TRUE(spec.validate(passingInput)); + + auto failingInput = json::parse(R"({ "exact": "Different" })"); + ASSERT_FALSE(spec.validate(failingInput)); +} + +TEST_F(RPCBaseTest, ArrayAtValidator) +{ + // clang-format off + auto spec = RpcSpec{ + {"arr", Required{}, Type{}, ValidateArrayAt{0, { + {"limit", Required{}, Type{}, Between{0, 100}}, + }}}, + }; + // clang-format on + + auto passingInput = json::parse(R"({ "arr": [{"limit": 42}] })"); + ASSERT_TRUE(spec.validate(passingInput)); + + auto failingInput = json::parse(R"({ "arr": [{"limit": "not int"}] })"); + ASSERT_FALSE(spec.validate(failingInput)); +} + +TEST_F(RPCBaseTest, CustomValidator) +{ + // clang-format off + auto customFormatCheck = CustomValidator{ + [](json::value const& value, std::string_view key) -> MaybeError { + return value.as_string().size() == 34 ? + MaybeError{} : Error{RPC::Status{"Uh oh"}}; + } + }; + // clang-format on + + auto spec = RpcSpec{ + {"taker", customFormatCheck}, + }; + + auto passingInput = + json::parse(R"({ "taker": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59" })"); + ASSERT_TRUE(spec.validate(passingInput)); + + auto failingInput = json::parse(R"({ "taker": "wrongformat" })"); + ASSERT_FALSE(spec.validate(failingInput)); +} diff --git a/unittests/RPCErrors.cpp b/unittests/rpc/ErrorTests.cpp similarity index 100% rename from unittests/RPCErrors.cpp rename to unittests/rpc/ErrorTests.cpp diff --git a/unittests/rpc/handlers/DefaultProcessorTests.cpp b/unittests/rpc/handlers/DefaultProcessorTests.cpp new file mode 100644 index 00000000..a79f6305 --- /dev/null +++ b/unittests/rpc/handlers/DefaultProcessorTests.cpp @@ -0,0 +1,66 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include + +#include + +#include + +using namespace testing; +using namespace std; + +using namespace RPCng; +using namespace RPCng::validation; +using namespace unittests::detail; + +namespace json = boost::json; + +class RPCDefaultProcessorTest : public NoLoggerFixture +{ +}; + +TEST_F(RPCDefaultProcessorTest, ValidInput) +{ + HandlerMock handler; + RPCng::detail::DefaultProcessor processor; + + auto const input = json::parse(R"({ "something": "works" })"); + auto const spec = RpcSpec{{"something", Required{}}}; + auto const data = InOutFake{"works"}; + EXPECT_CALL(handler, spec()).WillOnce(ReturnRef(spec)); + EXPECT_CALL(handler, process(Eq(data))).WillOnce(Return(data)); + + auto const ret = processor(handler, input); + ASSERT_TRUE(ret); // no error +} + +TEST_F(RPCDefaultProcessorTest, InvalidInput) +{ + HandlerMock handler; + RPCng::detail::DefaultProcessor processor; + + auto const input = json::parse(R"({ "other": "nope" })"); + auto const spec = RpcSpec{{"something", Required{}}}; + EXPECT_CALL(handler, spec()).WillOnce(ReturnRef(spec)); + + auto const ret = processor(handler, input); + ASSERT_FALSE(ret); // returns error +} diff --git a/unittests/rpc/handlers/TestHandlerTests.cpp b/unittests/rpc/handlers/TestHandlerTests.cpp new file mode 100644 index 00000000..adceb39a --- /dev/null +++ b/unittests/rpc/handlers/TestHandlerTests.cpp @@ -0,0 +1,84 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include + +#include + +using namespace std; +using namespace RPCng; +using namespace RPCng::validation; +using namespace unittests::detail; + +namespace json = boost::json; + +class RPCTestHandlerTest : public NoLoggerFixture +{ +}; + +// example handler tests +TEST_F(RPCTestHandlerTest, HandlerSuccess) +{ + auto const handler = AnyHandler{HandlerFake{}}; + auto const input = json::parse(R"({ + "hello": "world", + "limit": 10 + })"); + + auto const output = handler.process(input); + ASSERT_TRUE(output); + + auto const val = output.value(); + EXPECT_EQ(val.as_object().at("computed").as_string(), "world_10"); +} + +TEST_F(RPCTestHandlerTest, HandlerErrorHandling) +{ + auto const handler = AnyHandler{HandlerFake{}}; + auto const input = json::parse(R"({ + "hello": "not world", + "limit": 10 + })"); + + auto const output = handler.process(input); + ASSERT_FALSE(output); + + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "invalidParams"); + EXPECT_EQ(err.at("error_message").as_string(), "Invalid parameters."); + EXPECT_EQ(err.at("error_code").as_uint64(), 31); +} + +TEST_F(RPCTestHandlerTest, HandlerInnerErrorHandling) +{ + auto const handler = AnyHandler{FailingHandlerFake{}}; + auto const input = json::parse(R"({ + "hello": "world", + "limit": 10 + })"); + + // validation succeeds but handler itself returns error + auto const output = handler.process(input); + ASSERT_FALSE(output); + + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "Very custom error"); +} diff --git a/unittests/rpc/handlers/impl/FakesAndMocks.h b/unittests/rpc/handlers/impl/FakesAndMocks.h new file mode 100644 index 00000000..ae04e3b5 --- /dev/null +++ b/unittests/rpc/handlers/impl/FakesAndMocks.h @@ -0,0 +1,168 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2023, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +namespace unittests::detail { + +// input data for the test handlers below +struct TestInput +{ + std::string hello; + std::optional 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, boost::json::value const& jv) +{ + std::optional optLimit; + if (jv.as_object().contains("limit")) + optLimit = jv.at("limit").as_int64(); + + return {jv.as_object().at("hello").as_string().c_str(), optLimit}; +} + +// must be implemented as per rpc/common/Concepts.h +inline void +tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + TestOutput output) +{ + jv = {{"computed", output.computed}}; +} + +// example handler +class HandlerFake +{ +public: + using Input = TestInput; + using Output = TestOutput; + using Result = RPCng::HandlerReturnType; + + RPCng::RpcSpecConstRef + spec() const + { + using namespace RPCng::validation; + + // clang-format off + static const RPCng::RpcSpec rpcSpec = { + {"hello", Required{}, Type{}, EqualTo{"world"}}, + {"limit", Type{}, Between{0, 100}} // optional field + }; + // clang-format on + + return rpcSpec; + } + + Result + process(Input input) const + { + return Output{ + input.hello + '_' + std::to_string(input.limit.value_or(0))}; + } +}; + +// example handler that returns custom error +class FailingHandlerFake +{ +public: + using Input = TestInput; + using Output = TestOutput; + using Result = RPCng::HandlerReturnType; + + RPCng::RpcSpecConstRef + spec() const + { + using namespace RPCng::validation; + + // clang-format off + static const RPCng::RpcSpec rpcSpec = { + {"hello", Required{}, Type{}, EqualTo{"world"}}, + {"limit", Type{}, Between{0u, 100u}} // optional field + }; + // clang-format on + + return rpcSpec; + } + + Result + process([[maybe_unused]] Input input) const + { + // always fail + return RPCng::Error{RPC::Status{"Very custom error"}}; + } +}; + +struct InOutFake +{ + std::string something; + + // Note: no spaceship comparison possible for std::string + friend bool + operator==(InOutFake const& lhs, InOutFake const& rhs) = default; +}; + +// must be implemented as per rpc/common/Concepts.h +inline InOutFake +tag_invoke(boost::json::value_to_tag, boost::json::value const& jv) +{ + return {jv.as_object().at("something").as_string().c_str()}; +} + +// must be implemented as per rpc/common/Concepts.h +inline void +tag_invoke( + boost::json::value_from_tag, + boost::json::value& jv, + InOutFake output) +{ + jv = {{"something", output.something}}; +} + +struct HandlerMock +{ + using Input = InOutFake; + using Output = InOutFake; + using Result = RPCng::HandlerReturnType; + + MOCK_METHOD(RPCng::RpcSpecConstRef, spec, (), (const)); + MOCK_METHOD(Result, process, (Input), (const)); +}; + +} // namespace unittests::detail