From e4dbd7de3aca7dd670eea24fedba7334be3b3a80 Mon Sep 17 00:00:00 2001 From: Alex Kremer Date: Mon, 28 Sep 2026 18:22:04 +0000 Subject: [PATCH] refactor: Add initial integration of `rpc-spec` (#8284) --- .../scripts/levelization/results/ordering.txt | 3 + .../workflows/reusable-build-test-config.yml | 5 +- CMakeLists.txt | 2 + cmake/XrplCore.cmake | 9 +- cmake/XrplCov.cmake | 2 +- conan.lock | 9 +- conanfile.py | 2 + src/tests/xrpld/CMakeLists.txt | 33 ++ src/tests/xrpld/main.cpp | 9 + src/tests/xrpld/rpc/SpecBridge.cpp | 412 ++++++++++++++++++ src/xrpld/rpc/detail/JsonFieldView.hpp | 288 ++++++++++++ src/xrpld/rpc/detail/JsonObjectView.hpp | 83 ++++ src/xrpld/rpc/detail/SpecBridge.hpp | 51 +++ 13 files changed, 903 insertions(+), 5 deletions(-) create mode 100644 src/tests/xrpld/CMakeLists.txt create mode 100644 src/tests/xrpld/main.cpp create mode 100644 src/tests/xrpld/rpc/SpecBridge.cpp create mode 100644 src/xrpld/rpc/detail/JsonFieldView.hpp create mode 100644 src/xrpld/rpc/detail/JsonObjectView.hpp create mode 100644 src/xrpld/rpc/detail/SpecBridge.hpp diff --git a/.github/scripts/levelization/results/ordering.txt b/.github/scripts/levelization/results/ordering.txt index 284fc9cdb7..66177a0026 100644 --- a/.github/scripts/levelization/results/ordering.txt +++ b/.github/scripts/levelization/results/ordering.txt @@ -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 diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml index 2a0c9332f3..c1780d933a 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -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 }} diff --git a/CMakeLists.txt b/CMakeLists.txt index a324cecedc..ed4434ce03 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/cmake/XrplCore.cmake b/cmake/XrplCore.cmake index f3951d4eac..a949f7ed7c 100644 --- a/cmake/XrplCore.cmake +++ b/cmake/XrplCore.cmake @@ -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 diff --git a/cmake/XrplCov.cmake b/cmake/XrplCov.cmake index 05d9ed3806..ff7cda21dc 100644 --- a/cmake/XrplCov.cmake +++ b/cmake/XrplCov.cmake @@ -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) diff --git a/conan.lock b/conan.lock index 176f0b27cb..01d4079c36 100644 --- a/conan.lock +++ b/conan.lock @@ -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" ], diff --git a/conanfile.py b/conanfile.py index 52d4da4286..7643f7e0f3 100644 --- a/conanfile.py +++ b/conanfile.py @@ -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, } diff --git a/src/tests/xrpld/CMakeLists.txt b/src/tests/xrpld/CMakeLists.txt new file mode 100644 index 0000000000..f83c45400a --- /dev/null +++ b/src/tests/xrpld/CMakeLists.txt @@ -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) diff --git a/src/tests/xrpld/main.cpp b/src/tests/xrpld/main.cpp new file mode 100644 index 0000000000..f9114bffc4 --- /dev/null +++ b/src/tests/xrpld/main.cpp @@ -0,0 +1,9 @@ +#include +#include + +int +main(int argc, char** argv) +{ + ::testing::InitGoogleMock(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/src/tests/xrpld/rpc/SpecBridge.cpp b/src/tests/xrpld/rpc/SpecBridge.cpp new file mode 100644 index 0000000000..149b95122f --- /dev/null +++ b/src/tests/xrpld/rpc/SpecBridge.cpp @@ -0,0 +1,412 @@ +#include + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +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()); + 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()); + EXPECT_TRUE(num.is()); + + 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()); + + auto const real = root.child("real"); + EXPECT_TRUE(real.isDouble()); + EXPECT_EQ(real.asDouble(), 1.5); + EXPECT_TRUE(real.is()); + EXPECT_FALSE(real.isInt64()); + + auto const flag = root.child("flag"); + EXPECT_TRUE(flag.isBool()); + EXPECT_TRUE(flag.asBool()); + EXPECT_TRUE(flag.is()); + + // 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()); + 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(input->ledger.value)); + EXPECT_EQ(std::get(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(::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 diff --git a/src/xrpld/rpc/detail/JsonFieldView.hpp b/src/xrpld/rpc/detail/JsonFieldView.hpp new file mode 100644 index 0000000000..bf53ddc3f9 --- /dev/null +++ b/src/xrpld/rpc/detail/JsonFieldView.hpp @@ -0,0 +1,288 @@ +#pragma once + +#include +#include +#include + +#include +#include + +#include +#include +#include // IWYU pragma: keep +#include +#include +#include + +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(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(readValue_->asInt()) + : static_cast(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(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(idx)], key_}; + } + + if (readValue_ == nullptr || !readValue_->isArray() || idx >= readValue_->size()) + return absent(key_); + return {&(*readValue_)[static_cast(idx)], key_}; + } + + template + [[nodiscard]] bool + is() const noexcept + { + if constexpr (std::is_same_v) + { + return isInt64(); + } + else if constexpr (std::is_same_v) + { + return isUint32(); + } + else if constexpr (std::is_same_v) + { + return isBool(); + } + else if constexpr (std::is_same_v) + { + return isString(); + } + else if constexpr (std::is_same_v) + { + return isDouble(); + } + else if constexpr (std::is_same_v) + { + return isObject(); + } + else if constexpr (std::is_same_v) + { + 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::max(), + "xrpl::rpc::JsonFieldView::set : value representable as json::UInt"); + *writeValue_ = static_cast(value); + } + else + { + XRPL_ASSERT( + value >= std::numeric_limits::min(), + "xrpl::rpc::JsonFieldView::set : value representable as json::Int"); + *writeValue_ = static_cast(value); + } + } + + void + set(std::uint32_t value) + { + *writeValue_ = static_cast(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); + +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/detail/JsonObjectView.hpp b/src/xrpld/rpc/detail/JsonObjectView.hpp new file mode 100644 index 0000000000..44c78879bf --- /dev/null +++ b/src/xrpld/rpc/detail/JsonObjectView.hpp @@ -0,0 +1,83 @@ +#pragma once + +#include + +#include + +#include + +#include +#include +#include + +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); + +} // 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 diff --git a/src/xrpld/rpc/detail/SpecBridge.hpp b/src/xrpld/rpc/detail/SpecBridge.hpp new file mode 100644 index 0000000000..d89fe9e358 --- /dev/null +++ b/src/xrpld/rpc/detail/SpecBridge.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include +#include + +#include +#include + +#include +#include +#include + +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> 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(code); + entry[jss::message] = message; + } +} + +} // namespace xrpl::rpc