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