Skip to content

Commit 81efb22

Browse files
authored
refactor: Dual path for rpc handlers (#3198)
1 parent 08255c4 commit 81efb22

9 files changed

Lines changed: 465 additions & 17 deletions

File tree

src/rpc/RPCHelpers.cpp

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include <boost/lexical_cast/bad_lexical_cast.hpp>
2828
#include <fmt/format.h>
2929
#include <rpcspec/Errors.hpp>
30+
#include <rpcspec/Ledger.hpp>
3031
#include <xrpl/basics/Slice.h>
3132
#include <xrpl/basics/StringUtilities.h>
3233
#include <xrpl/basics/base_uint.h>
@@ -547,6 +548,47 @@ getLedgerHeaderFromHashOrSeq(
547548
return *lgrInfo;
548549
}
549550

551+
std::expected<xrpl::LedgerHeader, Status>
552+
getLedgerHeaderFromLedgerSpecifier(
553+
BackendInterface const& backend,
554+
boost::asio::yield_context yield,
555+
rpc::spec::LedgerSpecifier const& ledger,
556+
uint32_t maxSeq
557+
)
558+
{
559+
auto const err = std::unexpected{Status{RippledError::RpcLgrNotFound, "ledgerNotFound"}};
560+
auto const resolved = ledger.resolved();
561+
562+
if (resolved.isHash()) {
563+
auto const maybeLgrInfo =
564+
backend.fetchLedgerByHash(std::get<xrpl::uint256>(resolved.value), yield);
565+
if (not maybeLgrInfo.has_value() or maybeLgrInfo->seq > maxSeq)
566+
return err;
567+
568+
return *maybeLgrInfo;
569+
}
570+
571+
if (resolved.isShortcut()) {
572+
auto const shortcut = std::get<rpc::spec::LedgerShortcut>(resolved.value);
573+
ASSERT(
574+
shortcut == rpc::spec::LedgerShortcut::Validated,
575+
"current/closed ledgers must be forwarded before dispatch"
576+
);
577+
}
578+
579+
auto const ledgerSequence = resolved.isSequence() ? std::get<uint32_t>(resolved.value) : maxSeq;
580+
581+
// return without hitting the db
582+
if (ledgerSequence > maxSeq)
583+
return err;
584+
585+
auto const maybeLgrInfo = backend.fetchLedgerBySequence(ledgerSequence, yield);
586+
if (not maybeLgrInfo.has_value())
587+
return err;
588+
589+
return *maybeLgrInfo;
590+
}
591+
550592
std::vector<unsigned char>
551593
ledgerHeaderToBlob(xrpl::LedgerHeader const& info, bool includeHash)
552594
{

src/rpc/RPCHelpers.hpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include <boost/regex.hpp>
2323
#include <boost/regex/v5/regex_match.hpp>
2424
#include <fmt/format.h>
25+
#include <rpcspec/Ledger.hpp>
2526
#include <xrpl/basics/Number.h>
2627
#include <xrpl/basics/base_uint.h>
2728
#include <xrpl/json/json_value.h>
@@ -58,6 +59,7 @@
5859
#include <memory>
5960
#include <optional>
6061
#include <string>
62+
#include <string_view>
6163
#include <tuple>
6264
#include <utility>
6365
#include <vector>
@@ -299,6 +301,35 @@ getLedgerHeaderFromHashOrSeq(
299301
uint32_t maxSeq
300302
);
301303

304+
/**
305+
* @brief Get ledger header from a spec-library ledger specifier.
306+
*
307+
* The strong-typed counterpart of @ref getLedgerHeaderFromHashOrSeq, for handlers whose
308+
* spec produces a @c LedgerSpecifier instead of a ledger_hash / ledger_index pair.
309+
* Behaviour matches that overload: a hash or sequence beyond @p maxSeq, or one absent from
310+
* the backend, yields @c ledgerNotFound.
311+
*
312+
* A @c validated shortcut resolves to @p maxSeq, which is what it means for a server that
313+
* only serves validated data. An unspecified ledger resolves via
314+
* @c LedgerSpecifier::resolved(), which the spec library fixes to @c validated for Clio.
315+
*
316+
* @c current and @c closed cannot reach here: @ref specifiesCurrentOrClosedLedger forwards
317+
* those upstream before dispatch.
318+
*
319+
* @param backend The backend to use
320+
* @param yield The coroutine context
321+
* @param ledger The ledger the request selected
322+
* @param maxSeq The maximum sequence to search
323+
* @return The ledger header or an error status
324+
*/
325+
std::expected<xrpl::LedgerHeader, Status>
326+
getLedgerHeaderFromLedgerSpecifier(
327+
BackendInterface const& backend,
328+
boost::asio::yield_context yield,
329+
rpc::spec::LedgerSpecifier const& ledger,
330+
uint32_t maxSeq
331+
);
332+
302333
/**
303334
* @brief Traverse nodes owned by an account
304335
*

src/rpc/common/Concepts.hpp

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77
#include <boost/json/value.hpp>
88
#include <boost/json/value_from.hpp>
99
#include <boost/json/value_to.hpp>
10+
#include <rpcspec/Errors.hpp>
11+
#include <rpcspec/RpcSpecView.hpp>
1012

13+
#include <concepts>
1114
#include <cstdint>
15+
#include <expected>
1216
#include <optional>
1317
#include <string>
1418

@@ -71,17 +75,46 @@ concept SomeHandlerWithInput = requires(T a, uint32_t version) {
7175
{ a.spec(version) } -> std::same_as<RpcSpec const&>;
7276
} and SomeContextProcessWithInput<T> and boost::json::has_value_to<typename T::Input>::value;
7377

78+
/**
79+
* @brief Specifies what a Handler validated by the shared consteval spec must provide.
80+
*
81+
* Such a handler inherits @c rpc::spec::HandlerFor<Input> from the spec library, which
82+
* supplies a static @c parseInput (validate and deserialise in one pass) and a static
83+
* @c spec returning a type-erased @c RpcSpecView. Presence of @c parseInput is what
84+
* selects this path over @c SomeHandlerWithInput.
85+
*
86+
* The two input paths are mutually exclusive by construction: a legacy handler returns
87+
* @c RpcSpec @c const& from a non-static @c spec and needs a @c value_to for its Input,
88+
* neither of which holds here. @c kIsSingleInputPath asserts that below.
89+
*/
90+
template <typename T>
91+
concept SomeHandlerWithTypedInput = requires(uint32_t version, boost::json::value jv) {
92+
typename T::Input;
93+
{ T::parseInput(jv, version) } -> std::same_as<std::expected<typename T::Input, Status>>;
94+
{ T::spec(version) } -> std::same_as<rpc::spec::RpcSpecView>;
95+
} and SomeContextProcessWithInput<T>;
96+
7497
/**
7598
* @brief Specifies what a Handler without Input must provide.
7699
*/
77100
template <typename T>
78101
concept SomeHandlerWithoutInput = SomeContextProcessWithoutInput<T>;
79102

103+
/**
104+
* @brief True when @p T does not straddle the legacy and typed input paths.
105+
*
106+
* Guards the @c if @c constexpr chain in @c DefaultProcessor - were a handler to satisfy
107+
* both, the dispatch order alone would silently decide which spec ran.
108+
*/
109+
template <typename T>
110+
constexpr bool kIsSingleInputPath = not(SomeHandlerWithInput<T> and SomeHandlerWithTypedInput<T>);
111+
80112
/**
81113
* @brief Specifies what a Handler type must provide.
82114
*/
83115
template <typename T>
84-
concept SomeHandler = (SomeHandlerWithInput<T> or SomeHandlerWithoutInput<T>) and
116+
concept SomeHandler =
117+
(SomeHandlerWithInput<T> or SomeHandlerWithTypedInput<T> or SomeHandlerWithoutInput<T>) and
85118
boost::json::has_value_from<typename T::Output>::value;
86119

87120
} // namespace rpc

src/rpc/common/impl/Processors.hpp

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
#include "rpc/common/Concepts.hpp"
44
#include "rpc/common/Types.hpp"
5-
#include "util/UnsupportedType.hpp"
65

76
#include <boost/json/value.hpp>
7+
#include <rpcspec/WarningsToJson.hpp>
8+
9+
#include <utility>
810

911
namespace rpc::impl {
1012

@@ -19,37 +21,61 @@ struct DefaultProcessor final {
1921
{
2022
using boost::json::value_from;
2123
using boost::json::value_to;
22-
if constexpr (SomeHandlerWithInput<HandlerType>) {
23-
// first we run validation against specified API version
2424

25+
static_assert(
26+
kIsSingleInputPath<HandlerType>,
27+
"handler satisfies both the legacy and the typed input path; dispatch would be "
28+
"decided by the order of the branches below rather than by the handler"
29+
);
30+
static_assert(
31+
SomeHandlerWithTypedInput<HandlerType> or SomeHandlerWithInput<HandlerType> or
32+
SomeHandlerWithoutInput<HandlerType>,
33+
"handler matches none of the branches below"
34+
);
35+
36+
// New `rpc-spec`-based handler
37+
if constexpr (SomeHandlerWithTypedInput<HandlerType>) {
38+
auto input = HandlerType::parseInput(value, ctx.apiVersion);
39+
auto warnings = rpc::spec::toJsonArray(HandlerType::spec(ctx.apiVersion).check(value));
40+
41+
if (not input.has_value())
42+
return ReturnType{Error{std::move(input).error()}, std::move(warnings)};
43+
44+
auto ret = handler.process(*input, ctx);
45+
46+
if (not ret.has_value())
47+
return ReturnType{Error{std::move(ret).error()}, std::move(warnings)};
48+
49+
return ReturnType{value_from(std::move(ret).value()), std::move(warnings)};
50+
}
51+
52+
if constexpr (SomeHandlerWithInput<HandlerType>) {
53+
// Old spec-based handler: first we run validation against specified API version
54+
// TODO: This will be eventually removed once fully migraded to new rpc-spec system.
2555
auto const spec = handler.spec(ctx.apiVersion);
2656
auto warnings = spec.check(value);
2757
auto input = value; // copy here, spec require mutable data
2858

29-
if (auto const ret = spec.process(input); not ret)
59+
if (auto const ret = spec.process(input); not ret.has_value())
3060
return ReturnType{Error{ret.error()}, std::move(warnings)}; // forward Status
3161

3262
auto const inData = value_to<typename HandlerType::Input>(input);
3363
auto ret = handler.process(inData, ctx);
3464

3565
// real handler is given expected Input, not json
36-
if (!ret) {
37-
return ReturnType{
38-
Error{std::move(ret).error()}, std::move(warnings)
39-
}; // forward Status
40-
}
66+
if (not ret.has_value())
67+
return ReturnType{Error{std::move(ret).error()}, std::move(warnings)};
68+
4169
return ReturnType{value_from(std::move(ret).value()), std::move(warnings)};
42-
} else if constexpr (SomeHandlerWithoutInput<HandlerType>) {
70+
}
71+
72+
if constexpr (SomeHandlerWithoutInput<HandlerType>) {
4373
// no input to pass, ignore the value
4474
auto const ret = handler.process(ctx);
45-
if (not ret) {
75+
if (not ret.has_value())
4676
return ReturnType{Error{ret.error()}}; // forward Status
47-
}
77+
4878
return ReturnType{value_from(ret.value())};
49-
} else {
50-
// when concept SomeHandlerWithInput and SomeHandlerWithoutInput not cover all Handler
51-
// case
52-
static_assert(util::Unsupported<HandlerType>);
5379
}
5480
}
5581
};

tests/common/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ add_library(clio_testing_common)
33
target_sources(
44
clio_testing_common
55
PRIVATE
6+
rpc/FakesAndMocks.cpp
67
util/AssignRandomPort.cpp
78
util/BinaryTestObject.cpp
89
util/CallWithTimeout.cpp

tests/common/rpc/FakesAndMocks.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#include "rpc/FakesAndMocks.hpp"
2+
3+
#include <rpcspec/HandlerFor.hpp>
4+
#include <rpcspec/HandlerForDefs.hpp> // IWYU pragma: keep
5+
6+
template struct rpc::spec::HandlerFor<tests::common::typed_fake::TypedInput>;

tests/common/rpc/FakesAndMocks.hpp

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@
1010
#include <boost/json/value_from.hpp>
1111
#include <boost/json/value_to.hpp>
1212
#include <gmock/gmock.h>
13+
#include <rpcspec/Aliases.hpp>
14+
#include <rpcspec/Converters.hpp>
15+
#include <rpcspec/Errors.hpp>
16+
#include <rpcspec/FieldSpec.hpp>
17+
#include <rpcspec/HandlerFor.hpp>
18+
#include <rpcspec/Typed.hpp>
19+
#include <rpcspec/VersionedSpec.hpp>
1320

1421
#include <cstdint>
1522
#include <optional>
@@ -153,4 +160,54 @@ struct HandlerWithoutInputMock {
153160
MOCK_METHOD(Result, process, (rpc::Context const&), (const));
154161
};
155162

163+
// The shared consteval spec resolves a handler's spec from its Input type via an ADL
164+
// `specFor` hook, so the fake Input below needs its own namespace to host that hook.
165+
namespace typed_fake {
166+
167+
// input data for TypedHandlerFake; mirrors TestInput so the two paths stay comparable
168+
struct TypedInput {
169+
std::string hello;
170+
std::optional<uint32_t> limit;
171+
};
172+
173+
inline constexpr auto kInputSpec = rpc::spec::spec<TypedInput>(
174+
rpc::spec::field("hello", &TypedInput::hello, rpc::spec::required, rpc::spec::asString),
175+
rpc::spec::field("limit", &TypedInput::limit, rpc::spec::asUint32),
176+
rpc::spec::field("old_field", rpc::spec::deprecated)
177+
);
178+
179+
inline constexpr auto kSpec = rpc::spec::versioned<TypedInput>(kInputSpec);
180+
181+
[[nodiscard]] constexpr auto const&
182+
specFor(TypedInput const*) noexcept
183+
{
184+
return kSpec;
185+
}
186+
187+
} // namespace typed_fake
188+
189+
class TypedHandlerFake : public rpc::spec::HandlerFor<typed_fake::TypedInput> {
190+
public:
191+
using Output = TestOutput;
192+
using Result = rpc::HandlerReturnType<Output>;
193+
194+
static Result
195+
process(Input const& input, [[maybe_unused]] rpc::Context const& ctx)
196+
{
197+
return Output{input.hello + '_' + std::to_string(input.limit.value_or(0))};
198+
}
199+
};
200+
201+
class FailingTypedHandlerFake : public rpc::spec::HandlerFor<typed_fake::TypedInput> {
202+
public:
203+
using Output = TestOutput;
204+
using Result = rpc::HandlerReturnType<Output>;
205+
206+
static Result
207+
process([[maybe_unused]] Input const& input, [[maybe_unused]] rpc::Context const& ctx)
208+
{
209+
return rpc::Error{rpc::Status{"Very custom error"}};
210+
}
211+
};
212+
156213
} // namespace tests::common

0 commit comments

Comments
 (0)