packio
rpc.h
1 // This Source Code Form is subject to the terms of the Mozilla Public
2 // License, v. 2.0. If a copy of the MPL was not distributed with this
3 // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 
5 #ifndef PACKIO_NLOHMANN_JSON_RPC_H
6 #define PACKIO_NLOHMANN_JSON_RPC_H
7 
8 #include <deque>
9 
10 #include <nlohmann/json.hpp>
11 
12 #include "../arg.h"
13 #include "../internal/config.h"
14 #include "../internal/log.h"
15 #include "../internal/rpc.h"
16 #include "incremental_buffers.h"
17 
18 namespace packio::nlohmann_json {
19 
20 template <typename... Args>
21 constexpr bool positional_args_v = (!is_arg_v<Args> && ...);
22 
23 template <typename... Args>
24 constexpr bool named_args_v = sizeof...(Args) > 0 && (is_arg_v<Args> && ...);
25 
27 class rpc {
28  using buffer_type = std::string;
29 
30 public:
32  using id_type = nlohmann::json;
33 
35  struct request {
36  call_type type;
37  id_type id;
38  std::string method;
39  nlohmann::json args;
40  };
41 
43  struct response {
44  id_type id;
45  nlohmann::json result;
46  nlohmann::json error;
47  };
48 
49  class incremental_parser {
50  public:
51  std::optional<request> get_request()
52  {
53  try_parse_object();
54  if (!parsed_) {
55  return std::nullopt;
56  }
57  auto object = std::move(*parsed_);
58  parsed_.reset();
59  return parse_request(std::move(object));
60  }
61 
62  std::optional<response> get_response()
63  {
64  try_parse_object();
65  if (!parsed_) {
66  return std::nullopt;
67  }
68  auto object = std::move(*parsed_);
69  parsed_.reset();
70  return parse_response(std::move(object));
71  }
72 
73  char* buffer()
74  { //
75  return buffer_.data();
76  }
77 
78  std::size_t buffer_capacity() const
79  { //
80  return buffer_.size();
81  }
82 
83  void buffer_consumed(std::size_t bytes)
84  {
85  if (bytes == 0) {
86  return;
87  }
88  incremental_buffers_.feed(std::string_view(buffer_.data(), bytes));
89  std::copy(begin(buffer_) + bytes, end(buffer_), begin(buffer_));
90  buffer_.resize(buffer_.size() - bytes);
91  }
92 
93  void reserve_buffer(std::size_t bytes)
94  {
95  if (buffer_.size() >= bytes) {
96  return;
97  }
98  buffer_.resize(bytes);
99  }
100 
101  private:
102  void try_parse_object()
103  {
104  if (parsed_) {
105  return;
106  }
107  auto buffer = incremental_buffers_.get_buffer();
108  if (buffer) {
109  parsed_ = nlohmann::json::parse(*buffer);
110  }
111  }
112 
113  std::vector<char> buffer_;
114  std::optional<nlohmann::json> parsed_;
115  incremental_buffers incremental_buffers_;
116  };
117 
118  static std::string format_id(const id_type& id)
119  { //
120  return id.dump();
121  }
122 
123  template <typename... Args>
124  static auto serialize_notification(std::string_view method, Args&&... args)
125  -> std::enable_if_t<positional_args_v<Args...>, buffer_type>
126  {
127  return nlohmann::json({
128  {"jsonrpc", "2.0"},
129  {"method", method},
130  {"params",
131  nlohmann::json::array({nlohmann::json(
132  std::forward<Args>(args))...})},
133  })
134  .dump();
135  }
136 
137  template <typename... Args>
138  static auto serialize_notification(std::string_view method, Args&&... args)
139  -> std::enable_if_t<named_args_v<Args...>, buffer_type>
140  {
141  return nlohmann::json({
142  {"jsonrpc", "2.0"},
143  {"method", method},
144  {"params", {{args.name, args.value}...}},
145  })
146  .dump();
147  }
148 
149  template <typename... Args>
150  static auto serialize_notification(std::string_view, Args&&...)
151  -> std::enable_if_t<!positional_args_v<Args...> && !named_args_v<Args...>, buffer_type>
152  {
153  static_assert(
154  positional_args_v<Args...> || named_args_v<Args...>,
155  "JSON-RPC does not support mixed named and unnamed arguments");
156  }
157 
158  template <typename... Args>
159  static auto serialize_request(id_type id, std::string_view method, Args&&... args)
160  -> std::enable_if_t<positional_args_v<Args...>, buffer_type>
161  {
162  return nlohmann::json({
163  {"jsonrpc", "2.0"},
164  {"method", method},
165  {"params",
166  nlohmann::json::array({nlohmann::json(
167  std::forward<Args>(args))...})},
168  {"id", id},
169  })
170  .dump();
171  }
172 
173  template <typename... Args>
174  static auto serialize_request(id_type id, std::string_view method, Args&&... args)
175  -> std::enable_if_t<named_args_v<Args...>, buffer_type>
176  {
177  return nlohmann::json({
178  {"jsonrpc", "2.0"},
179  {"method", method},
180  {"params", {{args.name, args.value}...}},
181  {"id", id},
182  })
183  .dump();
184  }
185 
186  template <typename... Args>
187  static auto serialize_request(id_type, std::string_view, Args&&...)
188  -> std::enable_if_t<!positional_args_v<Args...> && !named_args_v<Args...>, buffer_type>
189  {
190  static_assert(
191  positional_args_v<Args...> || named_args_v<Args...>,
192  "JSON-RPC does not support mixed named and unnamed arguments");
193  }
194 
195  template <typename... Args>
196  static auto serialize_response(response response)
197  {
198  nlohmann::json resp = {
199  {"jsonrpc", "2.0"},
200  {"id", response.id},
201  };
202  if (!response.error.is_null()) {
203  resp["error"] = std::move(response.error);
204  }
205  else {
206  resp["result"] = std::move(response.result);
207  }
208  return resp.dump();
209  }
210 
211  static net::const_buffer buffer(const buffer_type& buf)
212  {
213  return net::const_buffer(buf.data(), buf.size());
214  }
215 
216  template <typename T, typename NamesContainer>
217  static std::optional<T> extract_args(
218  const nlohmann::json& args,
219  const NamesContainer& names)
220  {
221  try {
222  if (args.is_array()) {
223  if (args.size() != std::tuple_size_v<T>) {
224  // keep this check otherwise the converter
225  // may silently drop arguments
226  PACKIO_WARN(
227  "cannot convert args: wrong number of arguments");
228  return std::nullopt;
229  }
230 
231  return args.get<T>();
232  }
233  else if (args.is_object()) {
234  return convert_named_args<T>(
235  args, names, std::make_index_sequence<std::tuple_size_v<T>>{});
236  }
237  else {
238  PACKIO_ERROR("arguments are not a structured type");
239  return std::nullopt;
240  }
241  }
242  catch (const std::exception& exc) {
243  PACKIO_WARN("cannot convert args: {}", exc.what());
244  return std::nullopt;
245  }
246  }
247 
248  static response make_response(id_type id)
249  {
250  return make_response(id, nlohmann::json{});
251  }
252 
253  template <typename T>
254  static response make_response(id_type id, T&& value)
255  {
256  response resp;
257  resp.id = id;
258  resp.result = std::forward<T>(value);
259  return resp;
260  }
261 
262  template <typename T>
263  static response make_error_response(id_type id, T&& value)
264  {
265  response resp;
266  resp.id = id;
267  resp.error = [&]() {
268  nlohmann::json error = {
269  {"code", -32000}, // -32000 is an implementation-defined error
270  {"data", std::forward<T>(value)},
271  };
272  if (error["data"].is_string()) {
273  error["message"] = error["data"];
274  }
275  else {
276  error["message"] = "Unknown error";
277  }
278  return error;
279  }();
280  return resp;
281  }
282 
283 private:
284  template <typename T, typename NamesContainer, std::size_t... Idxs>
285  static T convert_named_args(
286  const nlohmann::json& args,
287  const NamesContainer& names,
288  std::index_sequence<Idxs...>)
289  {
290  return T{(
291  args[names.at(Idxs)].template get<std::tuple_element_t<Idxs, T>>())...};
292  }
293 
294  static std::optional<response> parse_response(nlohmann::json&& res)
295  {
296  auto id_it = res.find("id");
297  auto result_it = res.find("result");
298  auto error_it = res.find("error");
299 
300  if (id_it == end(res)) {
301  PACKIO_ERROR("missing id field");
302  return std::nullopt;
303  }
304  if (result_it == end(res) && error_it == end(res)) {
305  PACKIO_ERROR("missing error and result field");
306  return std::nullopt;
307  }
308 
309  response parsed;
310  parsed.id = std::move(*id_it);
311  if (error_it != end(res)) {
312  parsed.error = std::move(*error_it);
313  }
314  if (result_it != end(res)) {
315  parsed.result = std::move(*result_it);
316  }
317  return parsed;
318  }
319 
320  static std::optional<request> parse_request(nlohmann::json&& req)
321  {
322  auto id_it = req.find("id");
323  auto method_it = req.find("method");
324  auto params_it = req.find("params");
325 
326  if (method_it == end(req)) {
327  PACKIO_ERROR("missing method field");
328  return std::nullopt;
329  }
330  if (!method_it->is_string()) {
331  PACKIO_ERROR("method field is not a string");
332  return std::nullopt;
333  }
334 
335  request parsed;
336  parsed.method = method_it->get<std::string>();
337  if (params_it == end(req) || params_it->is_null()) {
338  parsed.args = nlohmann::json::array();
339  }
340  else if (!params_it->is_array() && !params_it->is_object()) {
341  PACKIO_ERROR("non-structured arguments are not supported");
342  return std::nullopt;
343  }
344  else {
345  parsed.args = std::move(*params_it);
346  }
347 
348  if (id_it == end(req) || id_it->is_null()) {
349  parsed.type = call_type::notification;
350  }
351  else {
352  parsed.type = call_type::request;
353  parsed.id = std::move(*id_it);
354  }
355  return parsed;
356  }
357 };
358 
359 } // packio::nlohmann_json
360 
361 #endif // PACKIO_NLOHMAANN_JSON_RPC_H
packio::nlohmann_json::rpc::id_type
nlohmann::json id_type
Type of the call ID.
Definition: rpc.h:32
packio::nlohmann_json::rpc
The JSON-RPC protocol implementation.
Definition: rpc.h:27
packio::nlohmann_json
Definition: incremental_buffers.h:13
packio::nlohmann_json::rpc::response
The object representing the response to a call.
Definition: rpc.h:43
packio::nlohmann_json::rpc::request
The object representing a client request.
Definition: rpc.h:35