Skip to content

Commit

Permalink
Support string type integer for oracle_document_id (#1448)
Browse files Browse the repository at this point in the history
Fix #1420
  • Loading branch information
cindyyan317 authored Jun 12, 2024
1 parent 56ab943 commit 49b80c7
Show file tree
Hide file tree
Showing 9 changed files with 246 additions and 21 deletions.
39 changes: 29 additions & 10 deletions src/rpc/common/MetaProcessors.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,8 @@
#pragma once

#include "rpc/Errors.hpp"
#include "rpc/common/Concepts.hpp"
#include "rpc/common/Specs.hpp"
#include "rpc/common/Types.hpp"
#include "rpc/common/Validators.hpp"

#include <boost/json/value.hpp>
#include <fmt/core.h>
Expand Down Expand Up @@ -146,10 +144,10 @@ class IfType final {
[[nodiscard]] MaybeError
verify(boost::json::value& value, std::string_view key) const
{
if (not value.is_object() or not value.as_object().contains(key.data()))
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.data())))
if (not rpc::validation::checkType<Type>(value.as_object().at(key)))
return {}; // ignore if type does not match

return processor_(value, key);
Expand All @@ -162,20 +160,22 @@ class IfType final {
/**
* @brief A meta-processor that wraps a validator and produces a custom error in case the wrapped validator fails.
*/
template <typename SomeRequirement>
template <typename RequirementOrModifierType>
requires SomeRequirement<RequirementOrModifierType> or SomeModifier<RequirementOrModifierType>
class WithCustomError final {
SomeRequirement requirement;
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 req The requirement to validate against
* @param reqOrModifier The requirement to validate against
* @param err The custom error to return in case `req` fails
*/
WithCustomError(SomeRequirement req, Status err) : requirement{std::move(req)}, error{std::move(err)}
WithCustomError(RequirementOrModifierType reqOrModifier, Status err)
: reqOrModifier{std::move(reqOrModifier)}, error{std::move(err)}
{
}

Expand All @@ -188,8 +188,9 @@ class WithCustomError final {
*/
[[nodiscard]] MaybeError
verify(boost::json::value const& value, std::string_view key) const
requires SomeRequirement<RequirementOrModifierType>
{
if (auto const res = requirement.verify(value, key); not res)
if (auto const res = reqOrModifier.verify(value, key); not res)
return Error{error};

return {};
Expand All @@ -205,12 +206,30 @@ class WithCustomError final {
*/
[[nodiscard]] MaybeError
verify(boost::json::value& value, std::string_view key) const
requires SomeRequirement<RequirementOrModifierType>
{
if (auto const res = requirement.verify(value, key); not res)
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
75 changes: 75 additions & 0 deletions src/rpc/common/Modifiers.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@

#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 <ripple/protocol/ErrorCodes.h>

#include <exception>
#include <functional>
#include <string>
#include <string_view>

Expand Down Expand Up @@ -100,4 +104,75 @@ struct ToLower final {
}
};

/**
* @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.find('.') != std::string::npos)
return Error{Status{RippledError::rpcINVALID_PARAMS}}; // maybe a float

try {
value.as_object()[key.data()] = std::stoi(strInt);
} catch (std::exception& e) {
return Error{Status{RippledError::rpcINVALID_PARAMS}};
}
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.data()), key);
};
};

} // namespace rpc::modifiers
7 changes: 4 additions & 3 deletions src/rpc/common/Validators.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include <fmt/core.h>
#include <ripple/basics/base_uint.h>
#include <ripple/protocol/AccountID.h>
#include <ripple/protocol/ErrorCodes.h>
#include <ripple/protocol/UintTypes.h>
#include <ripple/protocol/tokens.h>

Expand All @@ -45,7 +46,7 @@ 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.data()))
if (not value.is_object() or not value.as_object().contains(key))
return Error{Status{RippledError::rpcINVALID_PARAMS, "Required field '" + std::string{key} + "' missing"}};

return {};
Expand All @@ -54,10 +55,10 @@ Required::verify(boost::json::value const& value, std::string_view key)
[[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()))
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.data()), key);
return validator_(value.as_object().at(key), key);
}

[[nodiscard]] bool
Expand Down
1 change: 1 addition & 0 deletions src/rpc/common/Validators.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ class CustomValidator final {
* @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)}
{
}
Expand Down
14 changes: 9 additions & 5 deletions src/rpc/handlers/GetAggregatePrice.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include "data/BackendInterface.hpp"
#include "rpc/Errors.hpp"
#include "rpc/JS.hpp"
#include "rpc/common/Modifiers.hpp"
#include "rpc/common/Specs.hpp"
#include "rpc/common/Types.hpp"
#include "rpc/common/Validators.hpp"
Expand Down Expand Up @@ -131,23 +132,26 @@ class GetAggregatePriceHandler {
static auto constexpr ORACLES_MAX = 200;

static auto const oraclesValidator =
validation::CustomValidator{[](boost::json::value const& value, std::string_view) -> MaybeError {
modifiers::CustomModifier{[](boost::json::value& value, std::string_view) -> MaybeError {
if (!value.is_array() or value.as_array().empty() or value.as_array().size() > ORACLES_MAX)
return Error{Status{RippledError::rpcORACLE_MALFORMED}};

for (auto oracle : value.as_array()) {
for (auto& oracle : value.as_array()) {
if (!oracle.is_object() or !oracle.as_object().contains(JS(oracle_document_id)) or
!oracle.as_object().contains(JS(account)))
return Error{Status{RippledError::rpcORACLE_MALFORMED}};

auto maybeError =
validation::Type<std::uint32_t>{}.verify(oracle.as_object(), JS(oracle_document_id));
auto maybeError = validation::Type<std::uint32_t, std::string>{}.verify(
oracle.as_object(), JS(oracle_document_id)
);
if (!maybeError)
return maybeError;

maybeError = modifiers::ToNumber::modify(oracle, JS(oracle_document_id));
if (!maybeError)
return maybeError;

maybeError = validation::AccountBase58Validator.verify(oracle.as_object(), JS(account));

if (!maybeError)
return Error{Status{RippledError::rpcINVALID_PARAMS}};
};
Expand Down
6 changes: 4 additions & 2 deletions src/rpc/handlers/LedgerEntry.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include "rpc/JS.hpp"
#include "rpc/common/Checkers.hpp"
#include "rpc/common/MetaProcessors.hpp"
#include "rpc/common/Modifiers.hpp"
#include "rpc/common/Specs.hpp"
#include "rpc/common/Types.hpp"
#include "rpc/common/Validators.hpp"
Expand Down Expand Up @@ -296,8 +297,9 @@ class LedgerEntryHandler {
{JS(oracle_document_id),
meta::WithCustomError{validation::Required{}, Status(ClioError::rpcMALFORMED_REQUEST)},
meta::WithCustomError{
validation::Type<uint32_t>{}, Status(ClioError::rpcMALFORMED_ORACLE_DOCUMENT_ID)
}},
validation::Type<uint32_t, std::string>{}, Status(ClioError::rpcMALFORMED_ORACLE_DOCUMENT_ID)
},
meta::WithCustomError{modifiers::ToNumber{}, Status(ClioError::rpcMALFORMED_ORACLE_DOCUMENT_ID)}},
}}},
{JS(ledger), check::Deprecated{}},
};
Expand Down
47 changes: 47 additions & 0 deletions tests/unit/rpc/BaseTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#include <boost/json/parse.hpp>
#include <boost/json/value.hpp>
#include <fmt/core.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <ripple/protocol/AccountID.h>
#include <ripple/protocol/ErrorCodes.h>
Expand Down Expand Up @@ -604,3 +605,49 @@ TEST_F(RPCBaseTest, ToLowerModifier)
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 = json::parse(R"({ "str": [] })");
ASSERT_TRUE(spec.process(passingInput));

passingInput = json::parse(R"({ "str2": "TesT" })");
ASSERT_TRUE(spec.process(passingInput));

passingInput = json::parse(R"([])");
ASSERT_TRUE(spec.process(passingInput));

passingInput = json::parse(R"({ "str": "123" })");
ASSERT_TRUE(spec.process(passingInput));
ASSERT_EQ(passingInput.at("str").as_int64(), 123);

auto failingInput = json::parse(R"({ "str": "ok" })");
ASSERT_FALSE(spec.process(failingInput));

failingInput = json::parse(R"({ "str": "123.123" })");
ASSERT_FALSE(spec.process(failingInput));
}

TEST_F(RPCBaseTest, CustomModifier)
{
testing::StrictMock<testing::MockFunction<MaybeError(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 = json::parse(R"({ "str": "sss" })");
ASSERT_TRUE(spec.process(passingInput));

passingInput = json::parse(R"({ "strNotExist": 123 })");
ASSERT_TRUE(spec.process(passingInput));

// not a json object
passingInput = json::parse(R"([])");
ASSERT_TRUE(spec.process(passingInput));
}
49 changes: 49 additions & 0 deletions tests/unit/rpc/handlers/GetAggregatePriceTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,55 @@ TEST_F(RPCGetAggregatePriceHandlerTest, OracleLedgerEntrySinglePriceData)
});
}

TEST_F(RPCGetAggregatePriceHandlerTest, OracleLedgerEntryStrOracleDocumentId)
{
EXPECT_CALL(*backend, fetchLedgerBySequence(RANGEMAX, _))
.WillOnce(Return(CreateLedgerHeader(LEDGERHASH, RANGEMAX)));

auto constexpr documentId = 1;
mockLedgerObject(*backend, ACCOUNT, documentId, TX1, 1e3, 2); // 10

auto const handler = AnyHandler{GetAggregatePriceHandler{backend}};
auto const req = json::parse(fmt::format(
R"({{
"base_asset": "USD",
"quote_asset": "XRP",
"oracles":
[
{{
"account": "{}",
"oracle_document_id": "{}"
}}
]
}})",
ACCOUNT,
documentId
));

auto const expected = json::parse(fmt::format(
R"({{
"entire_set":
{{
"mean": "10",
"size": 1,
"standard_deviation": "0"
}},
"median": "10",
"time": 4321,
"ledger_index": {},
"ledger_hash": "{}",
"validated": true
}})",
RANGEMAX,
LEDGERHASH
));
runSpawn([&](auto yield) {
auto const output = handler.process(req, Context{yield});
ASSERT_TRUE(output);
EXPECT_EQ(output.result.value(), expected);
});
}

TEST_F(RPCGetAggregatePriceHandlerTest, PreviousTxNotFound)
{
EXPECT_CALL(*backend, fetchLedgerBySequence(RANGEMAX, _))
Expand Down
Loading

0 comments on commit 49b80c7

Please sign in to comment.