JSON RPC
JSON-RPC 2.0 library for C++
dispatcher_p.cpp
Go to the documentation of this file.
1#include "dispatcher_p.h"
2#include "dispatcher.h"
3#include "exception.h"
4
5/**
6 * @file
7 * @brief Implementation of the private members and methods of the JSON RPC dispatcher class.
8 * @internal
9 *
10 * This file contains the definitions of the private members and methods used by the `dispatcher` class to manage method handlers and process requests.
11 * It includes functions for parsing, validating, and invoking JSON RPC requests, as well as generating error responses.
12 */
13
14namespace {
15
16/**
17 * @brief Checks if the provided JSON value is a valid JSON RPC request ID.
18 *
19 * @param id The JSON value to check.
20 * @return `true` if the JSON value is a valid request ID, `false` otherwise.
21 *
22 * @details This function checks if the provided JSON value is a valid JSON RPC request ID.
23 * According to the JSON RPC specification, a valid request ID can be a string, a number, or null.
24 * Additionally, this function also considers a discarded JSON value as valid.
25 *
26 * @see https://www.jsonrpc.org/specification#request_object
27 * @internal
28 */
29bool is_valid_request_id(const nlohmann::json& id)
30{
31 return id.is_string() || id.is_number() || id.is_null() || id.is_discarded();
32}
33
34} // namespace
35
36namespace wwa::json_rpc {
37
38/**
39 * @brief Deserializes a JSON object into a `jsonrpc_request` structure.
40 * @internal
41 *
42 * @param j The JSON object to deserialize.
43 * @param r The `jsonrpc_request` structure to populate.
44 *
45 * @details This function deserializes a JSON object into a `jsonrpc_request` structure.
46 * * It extracts the `jsonrpc` version, `method` name, `params`, and `id` from the JSON object.
47 * * If the `params` field is not present, it defaults to an empty array.
48 * * If the `params` field is an object, it is wrapped in an array.
49 *
50 * @note This function cannot be moved to an anonymous namespace because of Argument-Dependent Lookup (ADL).
51 *
52 * @see https://www.jsonrpc.org/specification#request_object
53 * @see https://github.com/nlohmann/json?tab=readme-ov-file#arbitrary-types-conversions
54 */
55// NOLINT(misc-use-anonymous-namespace) -- cannot move to an anonymous namespace because of ADL
56static void from_json(const nlohmann::json& j, jsonrpc_request& r)
57{
58 r.params = nlohmann::json(nlohmann::json::value_t::discarded);
59 r.id = nlohmann::json(nlohmann::json::value_t::discarded);
60
61 j.at("jsonrpc").get_to(r.jsonrpc);
62 j.at("method").get_to(r.method);
63
64 if (j.contains("params")) {
65 j.at("params").get_to(r.params);
66 }
67
68 if (j.contains("id")) {
69 j.at("id").get_to(r.id);
70 }
71
72 if (r.params.is_discarded()) {
73 r.params = nlohmann::json::array();
74 }
75 else if (r.params.is_object()) {
76 r.params = nlohmann::json::array({r.params});
77 }
78}
79
80void dispatcher_private::add_handler(std::string&& method, dispatcher::handler_t&& handler)
81{
82 this->m_methods.try_emplace(std::move(method), std::move(handler));
83}
84
85jsonrpc_request dispatcher_private::parse_request(const nlohmann::json& request, nlohmann::json& extra)
86{
87 try {
88 auto req = request.get<jsonrpc_request>();
89 extra = request;
90 extra.erase("jsonrpc");
91 extra.erase("method");
92 extra.erase("params");
93 extra.erase("id");
94 return req;
95 }
96 catch (const nlohmann::json::exception& e) {
97 throw exception(exception::INVALID_REQUEST, e.what());
98 }
99}
100
101// NOLINTNEXTLINE(misc-no-recursion) -- false positive, process_request() will never be called with an object
103{
104 if (request.empty()) {
105 this->q_ptr->on_request(extra);
106 this->q_ptr->on_request_processed({}, exception::INVALID_REQUEST, extra);
107 return dispatcher_private::generate_error_response(
108 exception(exception::INVALID_REQUEST, err_empty_batch), nlohmann::json(nullptr)
109 );
110 }
111
112 auto response = nlohmann::json::array();
113 for (const auto& req : request) {
114 if (!req.is_object()) {
115 this->q_ptr->on_request(extra);
116 const auto r = dispatcher_private::generate_error_response(
117 exception(exception::INVALID_REQUEST, err_not_jsonrpc_2_0_request), nlohmann::json(nullptr)
118 );
119
120 response.push_back(r);
121 this->q_ptr->on_request_processed({}, exception::INVALID_REQUEST, extra);
122 }
123 else if (const auto res = this->process_request(req, extra); !res.is_discarded()) {
124 response.push_back(res);
125 }
126 }
127
128 return response.empty() ? nlohmann::json(nlohmann::json::value_t::discarded) : response;
129}
130
131// NOLINTNEXTLINE(misc-no-recursion) -- there is one level of recursion for batch requests
133{
134 if (request.is_array()) {
135 return this->process_batch_request(request, extra);
136 }
137
138 this->q_ptr->on_request(extra);
139 auto request_id = request.contains("id") ? request["id"] : nlohmann::json(nullptr);
140 if (!is_valid_request_id(request_id)) {
141 request_id = nlohmann::json(nullptr);
142 }
143
144 std::string method;
145 nlohmann::json extra_data = extra;
146 try {
147 nlohmann::json extra_fields;
148 auto req = dispatcher_private::parse_request(request, extra_fields);
149 dispatcher_private::validate_request(req);
150 method = req.method;
151 request_id = req.id;
152
153 if (extra.is_object() && !extra_fields.empty()) {
154 extra_data["extra"] = extra_fields;
155 }
156
157 const auto res = this->invoke(method, req.params, extra_data);
158 if (!req.id.is_discarded()) {
159 return nlohmann::json({{"jsonrpc", "2.0"}, {"result", res}, {"id", req.id}});
160 }
161
162 // NOLINTNEXTLINE(modernize-return-braced-init-list) -- braced init will create a JSON array
163 return nlohmann::json(nlohmann::json::value_t::discarded);
164 }
165 catch (const exception& e) {
166 this->q_ptr->on_request_processed(method, e.code(), extra_data);
167 return request_id.is_discarded() ? nlohmann::json(nlohmann::json::value_t::discarded)
168 : dispatcher_private::generate_error_response(e, request_id);
169 }
170 catch (const std::exception& e) {
171 this->q_ptr->on_request_processed(method, exception::INTERNAL_ERROR, extra_data);
172 return request_id.is_discarded() ? nlohmann::json(nlohmann::json::value_t::discarded)
173 : dispatcher_private::generate_error_response(
174 exception(exception::INTERNAL_ERROR, e.what()), request_id
175 );
176 }
177}
178
180{
181 if (r.jsonrpc != "2.0") {
182 throw json_rpc::exception(json_rpc::exception::INVALID_REQUEST, json_rpc::err_not_jsonrpc_2_0_request);
183 }
184
185 if (!r.params.is_array()) {
186 throw json_rpc::exception(json_rpc::exception::INVALID_PARAMS, json_rpc::err_bad_params_type);
187 }
188
189 if (r.method.empty()) {
190 throw json_rpc::exception(json_rpc::exception::INVALID_REQUEST, json_rpc::err_empty_method);
191 }
192
193 if (!is_valid_request_id(r.id)) {
194 throw json_rpc::exception(json_rpc::exception::INVALID_REQUEST, json_rpc::err_bad_id_type);
195 }
196}
197
198nlohmann::json dispatcher_private::generate_error_response(const exception& e, const nlohmann::json& id)
199{
200 return nlohmann::json({{"jsonrpc", "2.0"}, {"error", e.to_json()}, {"id", id}});
201}
202
205{
206 if (const auto it = this->m_methods.find(method); it != this->m_methods.end()) {
207 this->q_ptr->on_method(method, extra);
208 const auto response = it->second(extra, params);
209 this->q_ptr->on_request_processed(method, 0, extra);
210 return response;
211 }
212
213 throw exception(exception::METHOD_NOT_FOUND, err_method_not_found);
214}
215
216} // namespace wwa::json_rpc
Private implementation of the JSON RPC dispatcher class.
nlohmann::json invoke(const std::string &method, const nlohmann::json &params, const nlohmann::json &extra)
Invokes a method handler.
nlohmann::json process_batch_request(const nlohmann::json &request, const nlohmann::json &extra)
Processes a batch request.
static void validate_request(const jsonrpc_request &r)
Validates a JSON RPC request.
void add_handler(std::string &&method, dispatcher::handler_t &&handler)
Adds a method handler.
nlohmann::json process_request(const nlohmann::json &request, const nlohmann::json &extra)
Processes a JSON RPC request.
JSON RPC Exception class.
Definition exception.h:79
bool is_valid_request_id(const nlohmann::json &id)
Checks if the provided JSON value is a valid JSON RPC request ID.
static void from_json(const nlohmann::json &j, jsonrpc_request &r)
Deserializes a JSON object into a jsonrpc_request structure.
Represents a JSON RPC request.