refactor: Add initial integration of rpc-spec (#8284)

This commit is contained in:
Alex Kremer
2026-09-28 18:22:04 +00:00
committed by GitHub
parent 646d2ce620
commit e4dbd7de3a
13 changed files with 903 additions and 5 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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
View 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();
}

View 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

View 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

View 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

View 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