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