JSON RPC
JSON-RPC 2.0 library for C++
dispatcher.h
Go to the documentation of this file.
1#ifndef FAB131EA_3F90_43B6_833D_EB89DA373735
2#define FAB131EA_3F90_43B6_833D_EB89DA373735
3
4/**
5 * @file dispatcher.h
6 * @brief Defines the JSON RPC dispatcher class.
7 *
8 * This file contains the definition of the `dispatcher` class, which is responsible for managing JSON RPC method handlers
9 * and processing JSON RPC requests. The dispatcher supports adding various types of handlers, including plain functions,
10 * static class methods, lambda functions, and member functions. These handlers can accept and return values that are
11 * convertible to and from `nlohmann::json` values.
12 */
13
14#include <any>
15#include <cassert>
16#include <cstdint>
17#include <functional>
18#include <memory>
19#include <string>
20#include <string_view>
21#include <tuple>
22#include <type_traits>
23#include <utility>
24
25#include <nlohmann/json.hpp>
26
27#include "details.h"
28#include "exception.h"
29#include "export.h"
30
31/**
32 * @brief Library namespace.
33 */
34namespace wwa::json_rpc {
35
37struct jsonrpc_request;
38
39/**
40 * @brief A class that manages JSON RPC method handlers and processes JSON RPC requests.
41 *
42 * The dispatcher class allows adding method handlers for JSON RPC methods and processes JSON RPC requests.
43 * It supports adding plain functions, static class methods, lambda functions, and member functions as handlers.
44 * The handlers can accept and return values that can be converted to and from `nlohmann::json` values.
45 *
46 * @note The dispatcher class is non-copyable but movable.
47 *
48 * @details
49 * The dispatcher class provides the following functionalities:
50 * - Adding method handlers for JSON RPC methods.
51 * - Parsing and processing JSON RPC requests.
52 * - Invoking method handlers with the appropriate arguments.
53 * - Handling exceptions thrown by method handlers and returning appropriate JSON RPC error responses.
54 *
55 * @par Example Usage:
56 * ```cpp
57 * dispatcher d;
58 * d.add("subtract", [](const nlohmann::json& params) {
59 * int minuend = params["minuend"];
60 * int subtrahend = params["subtrahend"];
61 * return minuend - subtrahend;
62 * });
63 *
64 * const auto request = R"({"jsonrpc": "2.0", "method": "subtract", "params": {"minuend": 42, "subtrahend": 23}, "id": 1})";
65 * const auto response = d.process_request(nlohmann::json::parse(request)).dump();
66 * ```
67 *
68 * @par Adding Method Handlers:
69 * Method handlers can be added using the `add` method. The handler function can accept any number of arguments
70 * as long as they can be converted from a `nlohmann::json` value. The handler function can also return any type
71 * that can be converted to a `nlohmann::json` value.
72 *
73 * @par Handling Exceptions:
74 * If a handler function throws an exception derived from `std::exception`, the exception will be caught and
75 * an appropriate JSON RPC error response will be returned.
76 */
78public:
79 /**
80 * @brief Optional context data for method handlers.
81 *
82 * @details This type alias defines a context data type that can be passed to method handlers.
83 * The context data is a pair of two values:
84 * - The first value is an `std::any` object that is passed to the `process_request()` method;
85 * - The second value is a `nlohmann::json` object that contains additional fields extracted from the JSON RPC request.
86 */
88
89private:
90 friend class dispatcher_private;
91
92 /**
93 * @brief Method handler type.
94 *
95 * @details This type alias defines a method handler function that takes two parameters:
96 * - `ctx`: An additional parameter that can be used to pass additional information to the handler.
97 * - `params`: A JSON object containing the parameters for the method.
98 *
99 * The handler function returns a JSON object as a result.
100 *
101 * This type alias is used to define the signature of functions that handle method calls in the dispatcher.
102 */
104
105public:
106 /** @brief Class constructor. */
107 dispatcher();
108
109 /** @brief Class destructor. */
110 virtual ~dispatcher();
111
112 dispatcher(const dispatcher&) = delete;
113 dispatcher& operator=(const dispatcher&) = delete;
114
115 /**
116 * @brief Move constructor.
117 * @param rhs Right-hand side object.
118 */
119 dispatcher(dispatcher&& rhs) = default;
120
121 /**
122 * @brief Move assignment operator.
123 * @param rhs Right-hand side object.
124 * @return Reference to this object.
125 */
126 dispatcher& operator=(dispatcher&& rhs) = default;
127
128 /**
129 * @brief Adds a method handler @a f for the method @a method.
130 * @tparam F Type of the handler function @a f.
131 * @param method The name of the method to add the handler for.
132 * @param f The handler function.
133 * @details This overload is used to add a plain function, a static class method, or a lambda function as a handler.
134 *
135 * Internally, the handler is a function that accepts a `nlohmann::json` as its argument and returns a `nlohmann::json` value.
136 * However, the handler function can accept any number of arguments, as long as they
137 * [can be converted](https://github.com/nlohmann/json?tab=readme-ov-file#arbitrary-types-conversions) from a `nlohmann::json` value.
138 * The same is true for the return value: it can be any type that can be converted to a `nlohmann::json` value (or `void`).
139 * Of course, the handler can accept and/or return `nlohmann::json` values directly.
140 *
141 * @par Accepting Arguments in a Handler:
142 * The Specification [defines](https://www.jsonrpc.org/specification#parameter_structures) two types of parameters: *named* and *positional*.
143 * Since there is no easy way to match named parameters to the handler function arguments, the named parameters are treated as a single structured value.
144 * @par
145 * For example, if the parameters are passed like this: `"params": {"subtrahend": 23, "minuend": 42}`, the handler function must accept a single argument of a struct type:
146 * ```cpp
147 * struct subtract_params {
148 * int minuend;
149 * int subtrahend;
150 * };
151 *
152 * int subtract(const subtract_params& params)
153 * {
154 * return params.minuend - params.subtrahend;
155 * }
156 * ```
157 * @par
158 * Because there is no automatic conversion from a JSON object to `subtract_params`, you must define the conversion function yourself.
159 * The easiest way is to [use a macro](https://github.com/nlohmann/json?tab=readme-ov-file#simplify-your-life-with-macros):
160 * ```cpp
161 * NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(subtract_params, minuend, subtrahend);
162 * ```
163 * @par
164 * In the case of positional parameters, the handler function must accept the same number of arguments as the number of parameters passed in the request.
165 * @par
166 * If the handler needs to accept a variable number of arguments, it must accept a single `nlohmann::json` argument and parse it as needed, like this:
167 * ```cpp
168 * dispatcher.add("sum", [](const nlohmann::json& params) {
169 * std::vector<int> v;
170 * params.get_to(v);
171 * return std::accumulate(v.begin(), v.end(), 0);
172 * });
173 * ```
174 * @note If the handler accepts a single `nlohmann::json` argument, it will accept *any* parameters. For example:
175 * ```cpp
176 * void handler(const nlohmann::json& params)
177 * {
178 * if (params.is_null()) {
179 * // No parameters
180 * }
181 * else if (params.is_array()) {
182 * // Array of positional parameters
183 * }
184 * else if (params.is_object()) {
185 * // Named parameters
186 * }
187 * }
188 * ```
189 *
190 * @par Returning Values from a Handler:
191 * 1. The handler can return any value as long as it can be converted to a `nlohmann::json` value. If there is no default conversion available,
192 * the handler can either return a `nlohmann::json` value directly,
193 * or use [a custom `to_json()` function](https://github.com/nlohmann/json?tab=readme-ov-file#arbitrary-types-conversions).
194 * 2. If the handler function returns `void`, it will be automatically converted to `null` in the JSON response.
195 *
196 * @par Exception Handling:
197 * If the hander function throws an exception (derived from `std::exception`), the exception will be caught, and the error will be returned in the JSON response:
198 * 1. `json_rpc::exception` will be converted to a JSON RPC error object using json_rpc::exception::to_json();
199 * 2. other exceptions derived from `std::exception` will be converted to a JSON RPC error object with code @a -32603 (`exception::INTERNAL_ERROR`)
200 * and the exception message ([what()](https://en.cppreference.com/w/cpp/error/exception/what)) as the error message.
201 */
202 template<typename F>
203 void add(std::string_view method, F&& f)
204 {
205 this->add(method, std::forward<F>(f), nullptr);
206 }
207
208 /**
209 * @brief Adds a method to the dispatcher with the specified instance and function.
210 * @tparam C The type of the class instance.
211 * @tparam F The type of the function to be added.
212 * @param method The name of the method to be added.
213 * @param f The function to be added, which will be bound to the instance.
214 * @param instance The instance of the class to which the function belongs.
215 * @overload
216 * @details This template method allows adding a method to the dispatcher by binding a member function
217 * of a class instance. It uses function traits to deduce the argument types and creates a closure
218 * that is then added to the internal method map.
219 */
220 template<typename C, typename F>
221 void add(std::string_view method, F&& f, C instance)
222 {
223 using traits = details::function_traits<std::decay_t<F>>;
224 using ArgsTuple = typename traits::args_tuple;
225
226 const auto&& closure = this->create_closure<C, F, void, ArgsTuple>(instance, std::forward<F>(f));
227 this->add_internal_method(method, std::forward<decltype(closure)>(closure));
228 }
229
230 /**
231 * @brief Adds a method handler with a context parameter.
232 *
233 * @tparam F The type of the handler function.
234 * @param method The name of the method to add the handler for.
235 * @param f The handler function.
236 *
237 * @details This method allows adding a handler function with an additional context parameter.
238 * The context parameter can be used to pass additional information to the handler function.
239 * The handler function can accept any number of arguments as long as they can be converted from a `nlohmann::json` value.
240 * The same is true for the return value: it can be any type that can be converted to a `nlohmann::json` value (or `void`).
241 *
242 * This overload is used to add a plain function, a static class method, or a lambda function as a handler.
243 *
244 * @par Sample Usage:
245 * ```cpp
246 * struct extra_params {
247 * std::string ip;
248 * };
249 *
250 * dispatcher.add_ex("sum", [](const std::any& extra, const nlohmann::json& params) {
251 * std::cout << "Invoking sum() method for " << std::any_cast<extra_params>(extra).ip << "\n";
252 * std::vector<int> v;
253 * params.get_to(v);
254 * return std::accumulate(v.begin(), v.end(), 0);
255 * });
256 * ```
257 *
258 * See `process_request()` for more details on the extra parameter.
259 *
260 * @see add()
261 */
262 template<typename F>
263 void add_ex(std::string_view method, F&& f)
264 {
265 this->add_ex(method, std::forward<F>(f), nullptr);
266 }
267
268 /**
269 * @brief Adds a method handler with a context parameter and a class instance.
270 *
271 * @tparam C The type of the class instance.
272 * @tparam F The type of the handler function.
273 * @param method The name of the method to add the handler for.
274 * @param f The handler function.
275 * @param instance The instance of the class to which the function belongs.
276 *
277 * @overload
278 * @details This method allows adding a class method handler with an additional context parameter.
279 * The context parameter can be used to pass additional information to the handler function.
280 * The handler function can accept any number of arguments as long as they can be converted from a `nlohmann::json` value.
281 * The same is true for the return value: it can be any type that can be converted to a `nlohmann::json` value (or `void`).
282 *
283 * @see add()
284 */
285 template<typename C, typename F>
286 void add_ex(std::string_view method, F&& f, C instance)
287 {
288 using traits = details::function_traits<std::decay_t<F>>;
289 using ArgsTuple = typename traits::args_tuple;
290
291 static_assert(
292 std::tuple_size<std::decay_t<ArgsTuple>>::value > 0,
293 "Handler function must accept the `context` argument. Use `add()` for handlers without the `context` "
294 "argument."
295 );
296
297 const auto&& closure = this->create_closure<C, F, context_t, ArgsTuple>(instance, std::forward<F>(f));
298 this->add_internal_method(method, std::forward<decltype(closure)>(closure));
299 }
300
301 /**
302 * @brief Processes a JSON RPC request.
303 *
304 * @param request The JSON RPC request as a `nlohmann::json` object.
305 * @param data Optional data that can be passed to the handler function (only for handlers added with @a add_ex()).
306 * @return The response as a `nlohmann::json` object. If the request is a [Notification](https://www.jsonrpc.org/specification#notification),
307 * it will be of the [discarded_t](https://json.nlohmann.me/api/basic_json/is_discarded/) type.
308 *
309 * @details This method processes a JSON RPC request.
310 *
311 * Exceptions derived from `std::exception` thrown by the handler function are caught and returned as JSON RPC error responses:
312 * @li `json_rpc::exception` will be converted to a JSON RPC error object using `json_rpc::exception::to_json()`.
313 * @li Other exceptions derived from `std::exception` will be converted to a JSON RPC error object with code `-32603` (exception::INTERNAL_ERROR)
314 * and the exception message as the error message.
315 *
316 * For the handlers that accept the `context` parameter, this method will construct the context as follows:
317 * 1. The `data` parameter will be passed as the first element of the `context` tuple.
318 * 2. The extra fields from the JSON RPC request will be passed as the second element of the `context` tuple as a JSON object.
319 *
320 * For example, given this request:
321 * ```json
322 * {
323 * "jsonrpc": "2.0",
324 * "method": "subtract",
325 * "params": {"minuend": 42, "subtrahend": 23},
326 * "id": 1,
327 * "auth": "secret",
328 * "user": "admin"
329 * }
330 * ```
331 * and `data` set to `std::string("some_data")`, the `context` parameter passed to the handler will be a pair of values:
332 * - `std::string("some_data")` as `std::any`;
333 * - `nlohmann::json` representing the object `{ "auth": "secret", "user": "admin" }`.
334 */
335 nlohmann::json process_request(const nlohmann::json& request, const std::any& data = {});
336
337protected:
338 /**
339 * @brief Processes a single, non-batch JSON RPC request.
340 *
341 * @param request The JSON RPC request as a JSON object.
342 * @param data Additional information to pass to the method handlers as a part of the context.
343 * @param is_batch Indicates whether the request is a part of a batch request.
344 * @param unique_id The unique request ID.
345 * @return JSON response.
346 *
347 * @details This method processes a JSON RPC request by invoking the method handlers for the specified method.
348 * If the request is a batch request, it will call `process_batch_request()`.
349 */
350 virtual nlohmann::json
351 do_process_request(const nlohmann::json& request, const std::any& data, bool is_batch, std::uint64_t unique_id);
352
353 /**
354 * @brief Processes a batch request.
355 *
356 * @param request The batch request as a JSON array.
357 * @param data Additional information to pass to the method handlers as a part of the context.
358 * @param unique_id The unique request ID.
359 * @return The response as a JSON array.
360 *
361 * @details This method processes a batch request by invoking the method handlers for each request in the batch.
362 */
363 virtual nlohmann::json
364 process_batch_request(const nlohmann::json& request, const std::any& data, std::uint64_t unique_id);
365
366 /**
367 * @brief Invoked after the request has been parsed.
368 *
369 * @param request The parsed request.
370 * @param data Additional information to pass to the method handlers as a part of the context.
371 * @param unique_id The unique request ID.
372 */
373 virtual void request_parsed(const jsonrpc_request& request, const std::any& data, std::uint64_t unique_id);
374
375 /**
376 * @brief Invokes a method handler.
377 *
378 * @param method The name of the method to invoke.
379 * @param params The parameters for the method.
380 * @param ctx The context to pass to the method handlers.
381 * @param unique_id The unique request ID.
382 * @return The result of the method invocation as a JSON object.
383 *
384 * @details This method finds the handler for the specified method and invokes it with the provided parameters.
385 * @throws exception If the method is not found or the invocation fails.
386 * @see exception::METHOD_NOT_FOUND
387 */
388 virtual nlohmann::json invoke(
389 const std::string& method, const nlohmann::json& params, const dispatcher::context_t& ctx,
390 std::uint64_t unique_id
391 );
392
393 /**
394 * @brief Invoked when a request fails.
395 *
396 * @param request_id JSON RPC request ID.
397 * @param e Exception that is the reason for the failure.
398 * @param is_batch Whether this is a top-level batch request.
399 * @param unique_id Unique request ID.
400 */
401 virtual void
402 request_failed(const nlohmann::json& request_id, const std::exception* e, bool is_batch, std::uint64_t unique_id);
403
404private:
405 /**
406 * @brief Pointer to the implementation (Pimpl idiom).
407 *
408 * @details This unique pointer holds the private implementation details of the dispatcher class.
409 * It is used to hide the implementation details and reduce compilation dependencies.
410 */
412
413 /**
414 * @brief Adds a method handler for the specified method.
415 *
416 * @param method The name of the method.
417 * @param handler The handler function.
418 *
419 * @details This method registers a handler function for a given method name.
420 * The handler function will be invoked when a request for the specified method is received.
421 */
422 void add_internal_method(std::string_view method, handler_t&& handler);
423
424 /**
425 * @brief Creates a closure for invoking a member function with JSON parameters.
426 *
427 * @tparam C The type of the class instance (can be a pointer or null pointer).
428 * @tparam F The type of the member function (if C is not `std::nullptr_t`) or the function.
429 * @tparam Context The type of the context parameter that can be passed to the member function (can be `void` or `context_t`).
430 * @tparam Args The type of the arguments tuple.
431 *
432 * @param inst The instance of the class (can be a pointer or null pointer).
433 * @param f The member function to be invoked.
434 *
435 * @return A lambda function that takes a JSON object as a parameter and invokes the member function with the appropriate arguments.
436 *
437 * @details This method creates a closure (lambda function) that can be used to invoke a member function with arguments extracted from a JSON object.
438 *
439 * The closure performs the following steps:
440 * 1. Checks if the JSON object is an array.
441 * 2. If the JSON object is an array and the member function takes a single argument of type `nlohmann::json`, it directly passes the JSON object to the member function.
442 * 3. If the JSON object is an array and the number of elements matches the number of arguments expected by the member function, it extracts the arguments from the JSON array and invokes the member function.
443 * 4. If the JSON object is not an array or the number of elements does not match the number of arguments, it throws a json_rpc::exception with the `exception::INVALID_PARAMS` code.
444 *
445 * The `invoke_function` method is used to invoke the member function with the extracted arguments.
446 *
447 * The `std::apply` function is used to unpack the tuple and pass the arguments to the member function.
448 *
449 * Compile-time checks ensure that the code is type-safe and that certain conditions are met before the code is compiled.
450 * This helps catch potential errors early in the development process and improves the overall robustness of the code.
451 */
452 template<typename C, typename F, typename Context, typename Args>
453 constexpr auto create_closure(C inst, F&& f) const
454 {
455 static_assert((std::is_pointer_v<C> && std::is_class_v<std::remove_pointer_t<C>>) || std::is_null_pointer_v<C>);
456 return [func = std::forward<F>(f), inst](const context_t& ctx, const nlohmann::json& params) {
457 assert(params.is_array());
458 constexpr auto args_size = std::tuple_size<std::decay_t<Args>>::value;
459 constexpr auto arg_pos = std::is_void_v<Context> ? 0 : 1;
460
461 if constexpr (args_size == arg_pos + 1) {
462 if constexpr (std::is_same_v<std::decay_t<std::tuple_element_t<arg_pos, Args>>, nlohmann::json>) {
463 auto&& tuple_args = std::tuple_cat(
464 details::make_inst_tuple(inst), details::make_context_tuple<Context>(ctx),
465 std::make_tuple(params)
466 );
467
468 return details::invoke_function(func, std::forward<decltype(tuple_args)>(tuple_args));
469 }
470 }
471
472 if (params.size() + arg_pos == args_size) {
473 constexpr auto offset = std::is_void_v<Context> ? 0U : 1U;
474 auto&& tuple_args = std::tuple_cat(
475 details::make_inst_tuple(inst), details::make_context_tuple<Context>(ctx),
476 details::convert_args<Context, Args>(
477 params, details::offset_sequence_t<offset, std::make_index_sequence<args_size - offset>>{}
478 )
479 );
480
481 return details::invoke_function(func, std::forward<decltype(tuple_args)>(tuple_args));
482 }
483
484 throw exception(exception::INVALID_PARAMS, err_invalid_params_passed_to_method);
485 };
486 }
487};
488
489} // namespace wwa::json_rpc
490
491#endif /* FAB131EA_3F90_43B6_833D_EB89DA373735 */
Private implementation of the JSON RPC dispatcher class.
static std::uint64_t get_and_increment_counter() noexcept
Generates a unique request ID.
A class that manages JSON RPC method handlers and processes JSON RPC requests.
Definition dispatcher.h:77
virtual nlohmann::json do_process_request(const nlohmann::json &request, const std::any &data, bool is_batch, std::uint64_t unique_id)
Processes a single, non-batch JSON RPC request.
dispatcher & operator=(dispatcher &&rhs)=default
Move assignment operator.
dispatcher(dispatcher &&rhs)=default
Move constructor.
void add_ex(std::string_view method, F &&f, C instance)
Adds a method handler with a context parameter and a class instance.
Definition dispatcher.h:286
constexpr auto create_closure(C inst, F &&f) const
Creates a closure for invoking a member function with JSON parameters.
Definition dispatcher.h:453
virtual void request_parsed(const jsonrpc_request &request, const std::any &data, std::uint64_t unique_id)
Invoked after the request has been parsed.
void add_internal_method(std::string_view method, handler_t &&handler)
Adds a method handler for the specified method.
nlohmann::json process_request(const nlohmann::json &request, const std::any &data={})
Processes a JSON RPC request.
virtual void request_failed(const nlohmann::json &request_id, const std::exception *e, bool is_batch, std::uint64_t unique_id)
Invoked when a request fails.
virtual nlohmann::json invoke(const std::string &method, const nlohmann::json &params, const dispatcher::context_t &ctx, std::uint64_t unique_id)
Invokes a method handler.
void add(std::string_view method, F &&f)
Adds a method handler f for the method method.
Definition dispatcher.h:203
void add(std::string_view method, F &&f, C instance)
Adds a method to the dispatcher with the specified instance and function.
Definition dispatcher.h:221
virtual nlohmann::json process_batch_request(const nlohmann::json &request, const std::any &data, std::uint64_t unique_id)
Processes a batch request.
dispatcher()
Class constructor.
dispatcher(const dispatcher &)=delete
dispatcher & operator=(const dispatcher &)=delete
virtual ~dispatcher()
Class destructor.
void add_ex(std::string_view method, F &&f)
Adds a method handler with a context parameter.
Definition dispatcher.h:263
std::unique_ptr< dispatcher_private > d_ptr
Pointer to the implementation (Pimpl idiom).
Definition dispatcher.h:411
JSON RPC Exception class.
Definition exception.h:86
Exception thrown when the method is not found.
Definition exception.h:238
#define WWA_JSONRPC_EXPORT
Macro for exporting symbols when building the library dynamically or importing symbols when using the...
Definition export.h:42
static constexpr int INVALID_PARAMS
Invalid method parameter(s).
Definition exception.h:115
static constexpr int INTERNAL_ERROR
Internal JSON-RPC error.
Definition exception.h:120
static constexpr int INVALID_REQUEST
The JSON sent is not a valid Request object.
Definition exception.h:105
Represents a JSON RPC request.
Definition request.h:22