mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-09 05:08:27 +00:00
refactor: Add initial integration of rpc-spec (#8284)
This commit is contained in:
@@ -194,6 +194,9 @@ tests.libxrpl > xrpl.resource
|
||||
tests.libxrpl > xrpl.server
|
||||
tests.libxrpl > xrpl.shamap
|
||||
tests.libxrpl > xrpl.tx
|
||||
tests.xrpld > xrpld.rpc
|
||||
tests.xrpld > xrpl.json
|
||||
tests.xrpld > xrpl.protocol
|
||||
xrpl.conditions > xrpl.basics
|
||||
xrpl.conditions > xrpl.protocol
|
||||
xrpl.config > xrpl.basics
|
||||
|
||||
@@ -257,6 +257,7 @@ jobs:
|
||||
run: |
|
||||
./bin/check-nix-store-refs.sh "${BUILD_DIR}/xrpld"
|
||||
./bin/check-nix-store-refs.sh "${BUILD_DIR}/xrpl_tests"
|
||||
./bin/check-nix-store-refs.sh "${BUILD_DIR}/xrpld_tests"
|
||||
|
||||
- name: Show ccache statistics
|
||||
if: ${{ inputs.ccache_enabled }}
|
||||
@@ -338,7 +339,9 @@ jobs:
|
||||
- name: Run the separate tests
|
||||
if: ${{ !inputs.build_only }}
|
||||
working-directory: ${{ runner.os == 'Windows' && format('{0}/{1}', env.BUILD_DIR, inputs.build_type) || env.BUILD_DIR }}
|
||||
run: ./xrpl_tests
|
||||
run: |
|
||||
./xrpl_tests
|
||||
./xrpld_tests
|
||||
|
||||
- name: Run the embedded tests
|
||||
if: ${{ !inputs.build_only }}
|
||||
|
||||
@@ -115,6 +115,7 @@ find_package(secp256k1 REQUIRED)
|
||||
find_package(SOCI REQUIRED)
|
||||
find_package(SQLite3 REQUIRED)
|
||||
find_package(xxHash REQUIRED)
|
||||
find_package(xrpl-rpc-spec REQUIRED)
|
||||
|
||||
target_link_libraries(
|
||||
xrpl_libs
|
||||
@@ -175,6 +176,7 @@ include(XrplPackaging)
|
||||
if(tests)
|
||||
include(CTest)
|
||||
add_subdirectory(src/tests/libxrpl)
|
||||
add_subdirectory(src/tests/xrpld)
|
||||
endif()
|
||||
|
||||
if(benchmark)
|
||||
|
||||
@@ -296,7 +296,14 @@ if(xrpld)
|
||||
target_sources(xrpld PRIVATE ${sources})
|
||||
endif()
|
||||
|
||||
target_link_libraries(xrpld Xrpl::boost Xrpl::opts Xrpl::libs xrpl.libxrpl)
|
||||
target_link_libraries(
|
||||
xrpld
|
||||
Xrpl::boost
|
||||
Xrpl::opts
|
||||
Xrpl::libs
|
||||
xrpl.libxrpl
|
||||
rpcspec::rpcspec
|
||||
)
|
||||
exclude_if_included(xrpld)
|
||||
# define a macro for tests that might need to
|
||||
# be excluded or run differently in CI environment
|
||||
|
||||
@@ -48,7 +48,7 @@ setup_target_for_coverage_gcovr(
|
||||
"include/xrpl/beast/test"
|
||||
"include/xrpl/beast/unit_test"
|
||||
"${CMAKE_BINARY_DIR}/pb-xrpl.libpb"
|
||||
DEPENDENCIES xrpld xrpl_tests
|
||||
DEPENDENCIES xrpld xrpl_tests xrpld_tests
|
||||
)
|
||||
|
||||
add_code_coverage_to_target(opts INTERFACE)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"requires": [
|
||||
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708",
|
||||
"xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1782392402.420688",
|
||||
"xrpl-rpc-spec/0.1.19#870b2d3abcfbbf13b61c2d3c69495060%1790348286.187549",
|
||||
"sqlite3/3.53.0#324ada52333108388a9a6108bfa96734%1782392403.185447",
|
||||
"soci/4.0.3#e726491a03468795453f7c83fc924a96%1782392402.679521",
|
||||
"snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1782307151.633168",
|
||||
@@ -19,8 +20,8 @@
|
||||
"libarchive/3.8.7#c446109bd1f1d8ba7936c94189bc50e6%1782392403.066892",
|
||||
"jemalloc/5.3.1#1fc58d55316041f10fbc1e8a2eae632a%1776700028.228",
|
||||
"gtest/1.17.0#5224b3b3ff3b4ce1133cbdd27d53ee7d%1782392402.791979",
|
||||
"grpc/1.81.1#f729f6d75992d20f9c72828e9142d62f%1783945160.094135",
|
||||
"fast_float/8.2.10#f6f28d6bb22112078e7dbda611caf681%1782494504.298",
|
||||
"grpc/1.81.1#b87796a4269034856cbc1a2522db16eb%1788275071.530512",
|
||||
"fast_float/8.2.10#f6f28d6bb22112078e7dbda611caf681%1785888854.601666",
|
||||
"ed25519/2015.03#ae761bdc52730a843f0809bdf6c1b1f6%1782307148.15562",
|
||||
"date/3.0.4#862e11e80030356b53c2c38599ceb32b%1782392402.538492",
|
||||
"corrosion/0.6.1#bfa292df0a957bc70a450ff316cd9435%1786119416.131296",
|
||||
@@ -45,6 +46,10 @@
|
||||
],
|
||||
"python_requires": [],
|
||||
"overrides": {
|
||||
"boost/1.91.0#ea540ca2133d831b560036aa24dece3c": [
|
||||
null,
|
||||
"boost/1.91.0#ea540ca2133d831b560036aa24dece3c"
|
||||
],
|
||||
"protobuf/[>=5.27.0 <7]": [
|
||||
"protobuf/6.33.5"
|
||||
],
|
||||
|
||||
@@ -36,6 +36,7 @@ class Xrpl(ConanFile):
|
||||
"nudb/2.0.9",
|
||||
"openssl/3.6.3",
|
||||
"soci/4.0.3",
|
||||
"xrpl-rpc-spec/0.1.19",
|
||||
"zlib/1.3.2",
|
||||
]
|
||||
|
||||
@@ -114,6 +115,7 @@ class Xrpl(ConanFile):
|
||||
"soci/*:shared": False,
|
||||
"soci/*:with_sqlite3": True,
|
||||
"soci/*:with_boost": True,
|
||||
"xrpl-rpc-spec/*:server": "xrpld",
|
||||
"xxhash/*:shared": False,
|
||||
}
|
||||
|
||||
|
||||
33
src/tests/xrpld/CMakeLists.txt
Normal file
33
src/tests/xrpld/CMakeLists.txt
Normal file
@@ -0,0 +1,33 @@
|
||||
include(GoogleTest)
|
||||
|
||||
find_package(GTest REQUIRED)
|
||||
|
||||
# kept separate from xrpl_tests so that the libxrpl test binary stays free of server code.
|
||||
add_executable(xrpld_tests main.cpp)
|
||||
patch_nix_binary(xrpld_tests)
|
||||
set_target_properties(
|
||||
xrpld_tests
|
||||
PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
|
||||
)
|
||||
target_include_directories(
|
||||
xrpld_tests
|
||||
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} "${CMAKE_SOURCE_DIR}/src"
|
||||
)
|
||||
target_link_libraries(
|
||||
xrpld_tests
|
||||
PRIVATE GTest::gtest GTest::gmock xrpl.libxrpl rpcspec::rpcspec
|
||||
)
|
||||
|
||||
set(test_modules rpc)
|
||||
|
||||
foreach(module IN LISTS test_modules)
|
||||
file(
|
||||
GLOB_RECURSE sources
|
||||
CONFIGURE_DEPENDS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/${module}/*.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/${module}.cpp"
|
||||
)
|
||||
target_sources(xrpld_tests PRIVATE ${sources})
|
||||
endforeach()
|
||||
|
||||
gtest_discover_tests(xrpld_tests DISCOVERY_TIMEOUT 60)
|
||||
9
src/tests/xrpld/main.cpp
Normal file
9
src/tests/xrpld/main.cpp
Normal file
@@ -0,0 +1,9 @@
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
int
|
||||
main(int argc, char** argv)
|
||||
{
|
||||
::testing::InitGoogleMock(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
412
src/tests/xrpld/rpc/SpecBridge.cpp
Normal file
412
src/tests/xrpld/rpc/SpecBridge.cpp
Normal file
@@ -0,0 +1,412 @@
|
||||
#include <xrpld/rpc/detail/SpecBridge.hpp>
|
||||
|
||||
#include <xrpld/rpc/detail/JsonObjectView.hpp>
|
||||
|
||||
#include <xrpl/json/json_reader.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <rpcspec/Errors.hpp>
|
||||
#include <rpcspec/Types.hpp>
|
||||
#include <rpcspec/handlers/ledger/Spec.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <variant>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
namespace {
|
||||
|
||||
using ObjectView = rpc::JsonObjectView;
|
||||
|
||||
json::Value
|
||||
makeParams()
|
||||
{
|
||||
static constexpr auto kParams = R"JSON({
|
||||
"str": "hello",
|
||||
"num": 42,
|
||||
"big": 3000000000,
|
||||
"neg": -7,
|
||||
"real": 1.5,
|
||||
"flag": true,
|
||||
"nothing": null,
|
||||
"obj": {"inner": "deep"},
|
||||
"arr": ["a", "b"]
|
||||
})JSON";
|
||||
|
||||
json::Value params;
|
||||
EXPECT_TRUE(json::Reader{}.parse(kParams, params));
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(JsonView, field_types)
|
||||
{
|
||||
auto params = makeParams();
|
||||
ObjectView const root{params};
|
||||
|
||||
auto const str = root.child("str");
|
||||
EXPECT_TRUE(str.present());
|
||||
EXPECT_EQ(str.key(), "str");
|
||||
EXPECT_TRUE(str.isString());
|
||||
EXPECT_EQ(str.asString(), "hello");
|
||||
EXPECT_TRUE(str.is<std::string>());
|
||||
EXPECT_FALSE(str.isBool());
|
||||
EXPECT_FALSE(str.isInt64());
|
||||
EXPECT_FALSE(str.isObject());
|
||||
EXPECT_FALSE(str.isArray());
|
||||
|
||||
// uint32-ness is about the value, not the storage: a non-negative Int answers true.
|
||||
auto const num = root.child("num");
|
||||
EXPECT_TRUE(num.isInt64());
|
||||
EXPECT_EQ(num.asInt64(), 42);
|
||||
EXPECT_TRUE(num.isUint32());
|
||||
EXPECT_EQ(num.asUint32(), 42U);
|
||||
EXPECT_TRUE(num.is<std::int64_t>());
|
||||
EXPECT_TRUE(num.is<std::uint32_t>());
|
||||
|
||||
auto const big = root.child("big");
|
||||
EXPECT_TRUE(big.isInt64());
|
||||
EXPECT_EQ(big.asInt64(), 3000000000LL);
|
||||
EXPECT_TRUE(big.isUint32());
|
||||
EXPECT_EQ(big.asUint32(), 3000000000U);
|
||||
|
||||
auto const neg = root.child("neg");
|
||||
EXPECT_TRUE(neg.isInt64());
|
||||
EXPECT_EQ(neg.asInt64(), -7);
|
||||
EXPECT_FALSE(neg.isUint32());
|
||||
EXPECT_FALSE(neg.is<std::uint32_t>());
|
||||
|
||||
auto const real = root.child("real");
|
||||
EXPECT_TRUE(real.isDouble());
|
||||
EXPECT_EQ(real.asDouble(), 1.5);
|
||||
EXPECT_TRUE(real.is<double>());
|
||||
EXPECT_FALSE(real.isInt64());
|
||||
|
||||
auto const flag = root.child("flag");
|
||||
EXPECT_TRUE(flag.isBool());
|
||||
EXPECT_TRUE(flag.asBool());
|
||||
EXPECT_TRUE(flag.is<bool>());
|
||||
|
||||
// A boolean is not a number, though json::Value::isIntegral() says otherwise.
|
||||
EXPECT_FALSE(flag.isInt64());
|
||||
EXPECT_FALSE(flag.isUint32());
|
||||
|
||||
// Present, but null: no predicate claims it.
|
||||
auto const nothing = root.child("nothing");
|
||||
EXPECT_TRUE(nothing.present());
|
||||
EXPECT_FALSE(nothing.isString());
|
||||
EXPECT_FALSE(nothing.isInt64());
|
||||
EXPECT_FALSE(nothing.isBool());
|
||||
EXPECT_FALSE(nothing.isDouble());
|
||||
EXPECT_FALSE(nothing.isObject());
|
||||
EXPECT_FALSE(nothing.isArray());
|
||||
|
||||
auto const obj = root.child("obj");
|
||||
EXPECT_TRUE(obj.isObject());
|
||||
EXPECT_TRUE(obj.is<::rpc::spec::JsonObject>());
|
||||
EXPECT_EQ(obj.objectSize(), 1U);
|
||||
EXPECT_EQ(obj.arraySize(), 0U);
|
||||
|
||||
auto const arr = root.child("arr");
|
||||
EXPECT_TRUE(arr.isArray());
|
||||
EXPECT_TRUE(arr.is<::rpc::spec::JsonArray>());
|
||||
EXPECT_EQ(arr.arraySize(), 2U);
|
||||
EXPECT_EQ(arr.objectSize(), 0U);
|
||||
}
|
||||
|
||||
TEST(JsonView, field_absent)
|
||||
{
|
||||
auto params = makeParams();
|
||||
ObjectView const root{params};
|
||||
|
||||
// Every predicate has to answer for an absent field: that is how a spec asks whether
|
||||
// an optional field was supplied.
|
||||
auto const missing = root.child("nope");
|
||||
EXPECT_FALSE(missing.present());
|
||||
EXPECT_EQ(missing.key(), "nope");
|
||||
EXPECT_FALSE(missing.isString());
|
||||
EXPECT_FALSE(missing.isInt64());
|
||||
EXPECT_FALSE(missing.isUint32());
|
||||
EXPECT_FALSE(missing.isBool());
|
||||
EXPECT_FALSE(missing.isDouble());
|
||||
EXPECT_FALSE(missing.isObject());
|
||||
EXPECT_FALSE(missing.isArray());
|
||||
EXPECT_EQ(missing.arraySize(), 0U);
|
||||
EXPECT_EQ(missing.objectSize(), 0U);
|
||||
EXPECT_FALSE(missing.is<std::string>());
|
||||
EXPECT_FALSE(missing.is<::rpc::spec::JsonObject>());
|
||||
|
||||
// Walking down from an absent field stays absent rather than faulting.
|
||||
EXPECT_FALSE(missing.child("deeper").present());
|
||||
EXPECT_FALSE(missing.element(0).present());
|
||||
|
||||
// Same for a non-object asked for a child, or a non-array for an element.
|
||||
EXPECT_FALSE(root.child("str").child("inner").present());
|
||||
EXPECT_FALSE(root.child("str").element(0).present());
|
||||
}
|
||||
|
||||
TEST(JsonView, field_navigation)
|
||||
{
|
||||
auto params = makeParams();
|
||||
ObjectView const root{params};
|
||||
|
||||
auto const inner = root.child("obj").child("inner");
|
||||
EXPECT_TRUE(inner.present());
|
||||
EXPECT_EQ(inner.asString(), "deep");
|
||||
EXPECT_EQ(inner.key(), "inner");
|
||||
|
||||
// An element borrows its parent's key.
|
||||
auto const first = root.child("arr").element(0);
|
||||
EXPECT_TRUE(first.present());
|
||||
EXPECT_EQ(first.asString(), "a");
|
||||
EXPECT_EQ(first.key(), "arr");
|
||||
EXPECT_EQ(root.child("arr").element(1).asString(), "b");
|
||||
EXPECT_FALSE(root.child("arr").element(2).present());
|
||||
}
|
||||
|
||||
TEST(JsonView, field_set)
|
||||
{
|
||||
auto params = makeParams();
|
||||
ObjectView root{params};
|
||||
|
||||
// What a modifier does: normalise in place, so the converter after it sees that.
|
||||
root.child("str").set(std::string_view{"world"});
|
||||
EXPECT_EQ(params["str"].asString(), "world");
|
||||
|
||||
root.child("num").set(std::uint32_t{7});
|
||||
EXPECT_EQ(params["num"].asUInt(), 7U);
|
||||
|
||||
root.child("neg").set(std::int64_t{-1});
|
||||
EXPECT_EQ(params["neg"].asInt(), -1);
|
||||
|
||||
root.child("flag").set(false);
|
||||
EXPECT_FALSE(params["flag"].asBool());
|
||||
|
||||
root.child("real").set(2.5);
|
||||
EXPECT_EQ(params["real"].asDouble(), 2.5);
|
||||
|
||||
// Write access reaches through a child.
|
||||
root.child("obj").child("inner").set(std::string_view{"changed"});
|
||||
EXPECT_EQ(params["obj"]["inner"].asString(), "changed");
|
||||
}
|
||||
|
||||
TEST(JsonView, field_set_spans_both_32_bit_types)
|
||||
{
|
||||
auto params = makeParams();
|
||||
ObjectView root{params};
|
||||
|
||||
// The spec's toNumber modifier produces values up to UINT32_MAX. json::Int is 32 bits,
|
||||
// so routing everything through it would wrap those negative and lose the value.
|
||||
root.child("num").set(std::int64_t{3000000000});
|
||||
auto const big = root.child("num");
|
||||
EXPECT_TRUE(big.isUint32());
|
||||
EXPECT_EQ(big.asUint32(), 3000000000U);
|
||||
EXPECT_EQ(big.asInt64(), 3000000000LL);
|
||||
|
||||
root.child("neg").set(std::int64_t{-2000000000});
|
||||
auto const negative = root.child("neg");
|
||||
EXPECT_TRUE(negative.isInt64());
|
||||
EXPECT_EQ(negative.asInt64(), -2000000000LL);
|
||||
EXPECT_FALSE(negative.isUint32());
|
||||
}
|
||||
|
||||
TEST(JsonView, mutable_navigation)
|
||||
{
|
||||
auto params = makeParams();
|
||||
ObjectView root{params};
|
||||
|
||||
EXPECT_FALSE(root.child("nope").present());
|
||||
EXPECT_FALSE(root.child("obj").child("nope").present());
|
||||
EXPECT_FALSE(root.child("str").child("inner").present());
|
||||
EXPECT_FALSE(root.child("arr").element(2).present());
|
||||
EXPECT_FALSE(root.child("str").element(0).present());
|
||||
EXPECT_FALSE(params.isMember("nope"));
|
||||
EXPECT_FALSE(params["obj"].isMember("nope"));
|
||||
|
||||
auto second = root.child("arr").element(1);
|
||||
ASSERT_TRUE(second.present());
|
||||
EXPECT_EQ(second.key(), "arr");
|
||||
second.set(std::string_view{"z"});
|
||||
EXPECT_EQ(params["arr"][1u].asString(), "z");
|
||||
|
||||
json::Value array{json::ValueType::Array};
|
||||
ObjectView arrayRoot{array};
|
||||
EXPECT_FALSE(arrayRoot.child("anything").present());
|
||||
}
|
||||
|
||||
TEST(JsonView, object_root)
|
||||
{
|
||||
auto params = makeParams();
|
||||
|
||||
ObjectView const root{params};
|
||||
EXPECT_TRUE(root.isObject());
|
||||
EXPECT_FALSE(root.isArray());
|
||||
EXPECT_TRUE(root.child("str").present());
|
||||
EXPECT_FALSE(root.child("nope").present());
|
||||
|
||||
// Must not create the key: the non-const operator[] would have inserted a null.
|
||||
EXPECT_FALSE(params.isMember("nope"));
|
||||
|
||||
json::Value array{json::ValueType::Array};
|
||||
ObjectView const arrayRoot{array};
|
||||
EXPECT_TRUE(arrayRoot.isArray());
|
||||
EXPECT_FALSE(arrayRoot.isObject());
|
||||
EXPECT_FALSE(arrayRoot.child("anything").present());
|
||||
}
|
||||
|
||||
TEST(JsonView, mutable_root_over_const_source_reads)
|
||||
{
|
||||
auto const params = makeParams();
|
||||
ObjectView root{params};
|
||||
|
||||
auto const str = root.child("str");
|
||||
ASSERT_TRUE(str.present());
|
||||
EXPECT_EQ(str.asString(), "hello");
|
||||
EXPECT_EQ(root.child("obj").child("inner").asString(), "deep");
|
||||
EXPECT_EQ(root.child("arr").element(1).asString(), "b");
|
||||
EXPECT_FALSE(root.child("nope").present());
|
||||
}
|
||||
|
||||
TEST(SpecBridge, shared_spec_parses_json_value)
|
||||
{
|
||||
json::Value params{json::ValueType::Object};
|
||||
params["ledger_index"] = 42U;
|
||||
params["binary"] = true;
|
||||
params["transactions"] = true;
|
||||
|
||||
auto const input = ::rpc::spec::handlers::ledger::kSpec.parse(params, 2);
|
||||
ASSERT_TRUE(input.has_value());
|
||||
|
||||
EXPECT_TRUE(input->binary);
|
||||
EXPECT_TRUE(input->transactions);
|
||||
EXPECT_FALSE(input->expand);
|
||||
ASSERT_TRUE(std::holds_alternative<std::uint32_t>(input->ledger.value));
|
||||
EXPECT_EQ(std::get<std::uint32_t>(input->ledger.value), 42U);
|
||||
}
|
||||
|
||||
TEST(SpecBridge, shared_spec_reports_a_bad_json_value)
|
||||
{
|
||||
json::Value params{json::ValueType::Object};
|
||||
params["ledger_index"] = "not-a-ledger";
|
||||
|
||||
auto const input = ::rpc::spec::handlers::ledger::kSpec.parse(params, 2);
|
||||
ASSERT_FALSE(input.has_value());
|
||||
EXPECT_EQ(std::get<::rpc::XrpldError>(input.error().code), RpcInvalidParams);
|
||||
}
|
||||
|
||||
TEST(SpecBridge, shared_spec_warns_on_a_deprecated_field)
|
||||
{
|
||||
json::Value params{json::ValueType::Object};
|
||||
params["type"] = "hashes";
|
||||
|
||||
auto const warnings = ::rpc::spec::handlers::ledger::kSpec.check(params, 2);
|
||||
ASSERT_EQ(warnings.size(), 1U);
|
||||
EXPECT_EQ(warnings[0].code, ::rpc::WarningCode::WarnRpcDeprecated);
|
||||
}
|
||||
|
||||
TEST(SpecBridge, inject_spec_warnings)
|
||||
{
|
||||
std::string const kDeprecatedBase{
|
||||
::rpc::getWarningInfo(::rpc::WarningCode::WarnRpcDeprecated).message};
|
||||
|
||||
{
|
||||
json::Value out{json::ValueType::Object};
|
||||
rpc::injectSpecWarnings(out, {});
|
||||
EXPECT_FALSE(out.isMember(jss::warnings));
|
||||
}
|
||||
{
|
||||
json::Value out{json::ValueType::Object};
|
||||
rpc::injectSpecWarnings(
|
||||
out,
|
||||
{
|
||||
{
|
||||
.code = ::rpc::WarningCode::WarnRpcDeprecated,
|
||||
.field = "type",
|
||||
.message = "Field 'type' is deprecated.",
|
||||
},
|
||||
});
|
||||
|
||||
ASSERT_TRUE(out[jss::warnings].isArray());
|
||||
ASSERT_EQ(out[jss::warnings].size(), 1U);
|
||||
EXPECT_EQ(out[jss::warnings][0u][jss::id].asInt(), WarnRpcFieldsDeprecated);
|
||||
EXPECT_EQ(
|
||||
out[jss::warnings][0u][jss::message], kDeprecatedBase + " Field 'type' is deprecated.");
|
||||
}
|
||||
{
|
||||
// One entry per code, both details appended.
|
||||
json::Value out{json::ValueType::Object};
|
||||
rpc::injectSpecWarnings(
|
||||
out,
|
||||
{
|
||||
{
|
||||
.code = ::rpc::WarningCode::WarnRpcDeprecated,
|
||||
.field = "type",
|
||||
.message = "Field 'type' is deprecated.",
|
||||
},
|
||||
{
|
||||
.code = ::rpc::WarningCode::WarnRpcDeprecated,
|
||||
.field = "ledger",
|
||||
.message = "Field 'ledger' is deprecated.",
|
||||
},
|
||||
});
|
||||
|
||||
ASSERT_EQ(out[jss::warnings].size(), 1U);
|
||||
EXPECT_EQ(
|
||||
out[jss::warnings][0u][jss::message],
|
||||
kDeprecatedBase + " Field 'type' is deprecated. Field 'ledger' is deprecated.");
|
||||
}
|
||||
{
|
||||
// Ordered by code, so the response does not depend on field visit order.
|
||||
json::Value out{json::ValueType::Object};
|
||||
rpc::injectSpecWarnings(
|
||||
out,
|
||||
{
|
||||
{
|
||||
.code = ::rpc::WarningCode::WarnRpcDeprecated,
|
||||
.field = "type",
|
||||
.message = "Field 'type' is deprecated.",
|
||||
},
|
||||
{.code = ::rpc::WarningCode::WarnRpcOutdated, .field = "", .message = ""},
|
||||
});
|
||||
|
||||
ASSERT_EQ(out[jss::warnings].size(), 2U);
|
||||
EXPECT_EQ(
|
||||
out[jss::warnings][0u][jss::id].asInt(),
|
||||
static_cast<int>(::rpc::WarningCode::WarnRpcOutdated));
|
||||
EXPECT_EQ(
|
||||
out[jss::warnings][0u][jss::message],
|
||||
std::string{::rpc::getWarningInfo(::rpc::WarningCode::WarnRpcOutdated).message});
|
||||
EXPECT_EQ(out[jss::warnings][1u][jss::id].asInt(), WarnRpcFieldsDeprecated);
|
||||
}
|
||||
{
|
||||
// A handler's own warnings are kept.
|
||||
json::Value out{json::ValueType::Object};
|
||||
json::Value existing{json::ValueType::Array};
|
||||
json::Value& entry = existing.append(json::Value{json::ValueType::Object});
|
||||
entry[jss::id] = WarnRpcUnsupportedMajority;
|
||||
out[jss::warnings] = existing;
|
||||
|
||||
rpc::injectSpecWarnings(
|
||||
out,
|
||||
{
|
||||
{
|
||||
.code = ::rpc::WarningCode::WarnRpcDeprecated,
|
||||
.field = "type",
|
||||
.message = "Field 'type' is deprecated.",
|
||||
},
|
||||
});
|
||||
|
||||
ASSERT_EQ(out[jss::warnings].size(), 2U);
|
||||
EXPECT_EQ(out[jss::warnings][0u][jss::id].asInt(), WarnRpcUnsupportedMajority);
|
||||
EXPECT_EQ(out[jss::warnings][1u][jss::id].asInt(), WarnRpcFieldsDeprecated);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
288
src/xrpld/rpc/detail/JsonFieldView.hpp
Normal file
288
src/xrpld/rpc/detail/JsonFieldView.hpp
Normal file
@@ -0,0 +1,288 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/json/json_forwards.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
|
||||
#include <rpcspec/Concepts.hpp>
|
||||
#include <rpcspec/Types.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <limits> // IWYU pragma: keep
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
|
||||
namespace xrpl::rpc {
|
||||
|
||||
/**
|
||||
* The json::Value backend for the spec DSL: a view of one resolved field.
|
||||
*
|
||||
* The DSL is written against the SomeFieldView / SomeObjectView concepts rather than any
|
||||
* JSON type.
|
||||
*
|
||||
* A null pointer means the field is absent, which every predicate has to answer for. Which
|
||||
* of the two pointers is set carries const-correctness: a view built from a const value
|
||||
* cannot be written through, so a modifier cannot run during the check phase.
|
||||
*/
|
||||
class JsonFieldView
|
||||
{
|
||||
json::Value const* readValue_;
|
||||
json::Value* writeValue_;
|
||||
|
||||
std::string_view key_;
|
||||
|
||||
public:
|
||||
JsonFieldView(json::Value* value, std::string_view key) noexcept
|
||||
: readValue_(value), writeValue_(value), key_(key)
|
||||
{
|
||||
}
|
||||
|
||||
JsonFieldView(json::Value const* value, std::string_view key) noexcept
|
||||
: readValue_(value), writeValue_(nullptr), key_(key)
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]] static JsonFieldView
|
||||
absent(std::string_view key) noexcept
|
||||
{
|
||||
return {static_cast<json::Value const*>(nullptr), key};
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string_view
|
||||
key() const noexcept
|
||||
{
|
||||
return key_;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
present() const noexcept
|
||||
{
|
||||
return readValue_ != nullptr;
|
||||
}
|
||||
|
||||
// Not json::Value::isIntegral(), which is also true for a boolean: boost's is_int64()
|
||||
// is not, and a spec must not accept `true` as a number on one server only.
|
||||
[[nodiscard]] bool
|
||||
isInt64() const noexcept
|
||||
{
|
||||
return readValue_ != nullptr && (readValue_->isInt() || readValue_->isUInt());
|
||||
}
|
||||
|
||||
[[nodiscard]] std::int64_t
|
||||
asInt64() const
|
||||
{
|
||||
return readValue_->isInt() ? static_cast<std::int64_t>(readValue_->asInt())
|
||||
: static_cast<std::int64_t>(readValue_->asUInt());
|
||||
}
|
||||
|
||||
// A question about the value, not the storage: a non-negative Int is a uint32.
|
||||
// Both of json::Value's integer types are 32 bits, so that is the only check.
|
||||
[[nodiscard]] bool
|
||||
isUint32() const noexcept
|
||||
{
|
||||
if (readValue_ == nullptr)
|
||||
return false;
|
||||
if (readValue_->isUInt())
|
||||
return true;
|
||||
return readValue_->isInt() && readValue_->asInt() >= 0;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::uint32_t
|
||||
asUint32() const
|
||||
{
|
||||
return readValue_->isUInt() ? readValue_->asUInt()
|
||||
: static_cast<std::uint32_t>(readValue_->asInt());
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
isBool() const noexcept
|
||||
{
|
||||
return readValue_ != nullptr && readValue_->isBool();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
asBool() const
|
||||
{
|
||||
return readValue_->asBool();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
isString() const noexcept
|
||||
{
|
||||
return readValue_ != nullptr && readValue_->isString();
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string_view
|
||||
asString() const
|
||||
{
|
||||
auto const* str = readValue_->asCString();
|
||||
return str != nullptr ? std::string_view{str} : std::string_view{};
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
isDouble() const noexcept
|
||||
{
|
||||
return readValue_ != nullptr && readValue_->isDouble();
|
||||
}
|
||||
|
||||
[[nodiscard]] double
|
||||
asDouble() const
|
||||
{
|
||||
return readValue_->asDouble();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
isObject() const noexcept
|
||||
{
|
||||
return readValue_ != nullptr && readValue_->isObject();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
isArray() const noexcept
|
||||
{
|
||||
return readValue_ != nullptr && readValue_->isArray();
|
||||
}
|
||||
|
||||
[[nodiscard]] std::size_t
|
||||
arraySize() const noexcept
|
||||
{
|
||||
if (readValue_ == nullptr || !readValue_->isArray())
|
||||
return 0;
|
||||
return readValue_->size();
|
||||
}
|
||||
|
||||
[[nodiscard]] std::size_t
|
||||
objectSize() const noexcept
|
||||
{
|
||||
if (readValue_ == nullptr || !readValue_->isObject())
|
||||
return 0;
|
||||
return readValue_->size();
|
||||
}
|
||||
|
||||
[[nodiscard]] JsonFieldView
|
||||
child(std::string_view childKey) const
|
||||
{
|
||||
// Guarded rather than indexed: the non-const operator[] inserts a null member.
|
||||
std::string const key{childKey};
|
||||
|
||||
if (writeValue_ != nullptr)
|
||||
{
|
||||
if (!writeValue_->isObject() || !writeValue_->isMember(key))
|
||||
return absent(childKey);
|
||||
return {&(*writeValue_)[key], childKey};
|
||||
}
|
||||
|
||||
if (readValue_ == nullptr || !readValue_->isObject() || !readValue_->isMember(key))
|
||||
return absent(childKey);
|
||||
return {&(*readValue_)[key], childKey};
|
||||
}
|
||||
|
||||
// Inherits this field's key, so an error about an element names the array.
|
||||
[[nodiscard]] JsonFieldView
|
||||
element(std::size_t idx) const
|
||||
{
|
||||
if (writeValue_ != nullptr)
|
||||
{
|
||||
if (!writeValue_->isArray() || idx >= writeValue_->size())
|
||||
return absent(key_);
|
||||
return {&(*writeValue_)[static_cast<json::UInt>(idx)], key_};
|
||||
}
|
||||
|
||||
if (readValue_ == nullptr || !readValue_->isArray() || idx >= readValue_->size())
|
||||
return absent(key_);
|
||||
return {&(*readValue_)[static_cast<json::UInt>(idx)], key_};
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
[[nodiscard]] bool
|
||||
is() const noexcept
|
||||
{
|
||||
if constexpr (std::is_same_v<T, std::int64_t>)
|
||||
{
|
||||
return isInt64();
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, std::uint32_t>)
|
||||
{
|
||||
return isUint32();
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, bool>)
|
||||
{
|
||||
return isBool();
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, std::string>)
|
||||
{
|
||||
return isString();
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, double>)
|
||||
{
|
||||
return isDouble();
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, ::rpc::spec::JsonObject>)
|
||||
{
|
||||
return isObject();
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, ::rpc::spec::JsonArray>)
|
||||
{
|
||||
return isArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
static_assert(false, "xrpl::rpc::JsonFieldView::is : unsupported type");
|
||||
}
|
||||
}
|
||||
|
||||
// Only reachable from a modifier, which only ever runs against a mutable view.
|
||||
//
|
||||
// json::Value has no 64-bit integer type, so the value goes into whichever of its two
|
||||
// 32-bit types can hold it. Casting everything to json::Int would wrap anything above
|
||||
// INT32_MAX, and the spec's toNumber modifier deliberately produces values up to
|
||||
// UINT32_MAX (ledger_entry's oracle_document_id), which the boost backend stores fine.
|
||||
void
|
||||
set(std::int64_t value)
|
||||
{
|
||||
if (value >= 0)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
value <= std::numeric_limits<json::UInt>::max(),
|
||||
"xrpl::rpc::JsonFieldView::set : value representable as json::UInt");
|
||||
*writeValue_ = static_cast<json::UInt>(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
value >= std::numeric_limits<json::Int>::min(),
|
||||
"xrpl::rpc::JsonFieldView::set : value representable as json::Int");
|
||||
*writeValue_ = static_cast<json::Int>(value);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
set(std::uint32_t value)
|
||||
{
|
||||
*writeValue_ = static_cast<json::UInt>(value);
|
||||
}
|
||||
|
||||
void
|
||||
set(std::string_view value)
|
||||
{
|
||||
*writeValue_ = std::string{value};
|
||||
}
|
||||
|
||||
void
|
||||
set(bool value)
|
||||
{
|
||||
*writeValue_ = value;
|
||||
}
|
||||
|
||||
void
|
||||
set(double value)
|
||||
{
|
||||
*writeValue_ = value;
|
||||
}
|
||||
};
|
||||
|
||||
static_assert(::rpc::spec::SomeFieldView<JsonFieldView>);
|
||||
|
||||
} // namespace xrpl::rpc
|
||||
83
src/xrpld/rpc/detail/JsonObjectView.hpp
Normal file
83
src/xrpld/rpc/detail/JsonObjectView.hpp
Normal file
@@ -0,0 +1,83 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpld/rpc/detail/JsonFieldView.hpp>
|
||||
|
||||
#include <xrpl/json/json_value.h>
|
||||
|
||||
#include <rpcspec/Concepts.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl::rpc {
|
||||
|
||||
/**
|
||||
* The request params object the spec DSL resolves fields from.
|
||||
*
|
||||
* A separate type from JsonFieldView, as in the boost backend, so that a root — which
|
||||
* has no key of its own — cannot be passed where a field is expected.
|
||||
*/
|
||||
class JsonObjectView
|
||||
{
|
||||
json::Value const* readValue_;
|
||||
json::Value* writeValue_;
|
||||
|
||||
public:
|
||||
explicit JsonObjectView(json::Value& value) noexcept : readValue_(&value), writeValue_(&value)
|
||||
{
|
||||
}
|
||||
|
||||
explicit JsonObjectView(json::Value const& value) noexcept
|
||||
: readValue_(&value), writeValue_(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
isObject() const noexcept
|
||||
{
|
||||
return readValue_->isObject();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
isArray() const noexcept
|
||||
{
|
||||
return readValue_->isArray();
|
||||
}
|
||||
|
||||
[[nodiscard]] JsonFieldView
|
||||
child(std::string_view key)
|
||||
{
|
||||
if (writeValue_ == nullptr)
|
||||
return std::as_const(*this).child(key);
|
||||
|
||||
std::string const name{key};
|
||||
if (!writeValue_->isObject() || !writeValue_->isMember(name))
|
||||
return JsonFieldView::absent(key);
|
||||
return {&(*writeValue_)[name], key};
|
||||
}
|
||||
|
||||
[[nodiscard]] JsonFieldView
|
||||
child(std::string_view key) const
|
||||
{
|
||||
std::string const name{key};
|
||||
if (!readValue_->isObject() || !readValue_->isMember(name))
|
||||
return JsonFieldView::absent(key);
|
||||
return {&(*readValue_)[name], key};
|
||||
}
|
||||
};
|
||||
|
||||
static_assert(::rpc::spec::SomeObjectView<JsonObjectView>);
|
||||
|
||||
} // namespace xrpl::rpc
|
||||
|
||||
namespace rpc::spec {
|
||||
|
||||
// Binds json::Value to xrpld's view, so a spec can be handed request params directly.
|
||||
template <>
|
||||
struct ObjectViewFor<::json::Value>
|
||||
{
|
||||
using Type = ::xrpl::rpc::JsonObjectView;
|
||||
};
|
||||
|
||||
} // namespace rpc::spec
|
||||
51
src/xrpld/rpc/detail/SpecBridge.hpp
Normal file
51
src/xrpld/rpc/detail/SpecBridge.hpp
Normal file
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <rpcspec/Errors.hpp>
|
||||
#include <rpcspec/Types.hpp>
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::rpc {
|
||||
|
||||
// Warnings are grouped by code into one entry each: the code's standard message followed by
|
||||
// every per-field detail, space separated.
|
||||
//
|
||||
// Entries already in the response are kept, so a handler may add its own.
|
||||
inline void
|
||||
injectSpecWarnings(json::Value& object, ::rpc::spec::Warnings const& warnings)
|
||||
{
|
||||
if (warnings.empty())
|
||||
return;
|
||||
|
||||
// Ordered, so the response does not depend on the order fields were visited in.
|
||||
std::map<::rpc::WarningCode, std::vector<std::string>> grouped;
|
||||
for (auto const& warning : warnings)
|
||||
grouped[warning.code].push_back(warning.message);
|
||||
|
||||
json::Value& array = object.isMember(jss::warnings)
|
||||
? object[jss::warnings]
|
||||
: (object[jss::warnings] = json::Value{json::ValueType::Array});
|
||||
|
||||
for (auto const& [code, messages] : grouped)
|
||||
{
|
||||
std::string message{::rpc::getWarningInfo(code).message};
|
||||
for (auto const& detail : messages)
|
||||
{
|
||||
if (detail.empty())
|
||||
continue;
|
||||
message += ' ';
|
||||
message += detail;
|
||||
}
|
||||
|
||||
json::Value& entry = array.append(json::Value{json::ValueType::Object});
|
||||
entry[jss::id] = static_cast<int>(code);
|
||||
entry[jss::message] = message;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace xrpl::rpc
|
||||
Reference in New Issue
Block a user