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 <array>
9 #include <optional>
10 
11 #include <nlohmann/json.hpp>
12 
13 #include "../arg.h"
14 #include "../args_specs.h"
15 #include "../internal/config.h"
16 #include "../internal/expected.h"
17 #include "../internal/log.h"
18 #include "../internal/rpc.h"
19 #include "incremental_buffers.h"
20 
21 namespace packio {
22 namespace nl_json_rpc {
23 namespace internal {
24 
25 template <typename... Args>
26 constexpr bool positional_args_v = (!is_arg_v<Args> && ...);
27 
28 template <typename... Args>
29 constexpr bool named_args_v = sizeof...(Args) > 0 && (is_arg_v<Args> && ...);
30 
31 using id_type = nlohmann::json;
32 using native_type = nlohmann::json;
33 using packio::internal::expected;
34 using packio::internal::unexpected;
35 
37 struct request {
38  call_type type;
39  internal::id_type id;
40  std::string method;
41  native_type args;
42 };
43 
45 struct response {
46  id_type id;
47  native_type result;
48  native_type error;
49 };
50 
53 public:
54  expected<request, std::string> get_request()
55  {
56  try_parse_object();
57  if (!parsed_) {
58  return unexpected{"no request parsed"};
59  }
60  auto object = std::move(*parsed_);
61  parsed_.reset();
62  return parse_request(std::move(object));
63  }
64 
65  expected<response, std::string> get_response()
66  {
67  try_parse_object();
68  if (!parsed_) {
69  return unexpected{"no response parsed"};
70  }
71  auto object = std::move(*parsed_);
72  parsed_.reset();
73  return parse_response(std::move(object));
74  }
75 
76  char* buffer()
77  { //
78  return incremental_buffers_.in_place_buffer();
79  }
80 
81  std::size_t buffer_capacity() const
82  { //
83  return incremental_buffers_.in_place_buffer_capacity();
84  }
85 
86  void buffer_consumed(std::size_t bytes)
87  { //
88  incremental_buffers_.in_place_buffer_consumed(bytes);
89  }
90 
91  void reserve_buffer(std::size_t bytes)
92  { //
93  incremental_buffers_.reserve_in_place_buffer(bytes);
94  }
95 
96 private:
97  void try_parse_object()
98  {
99  if (parsed_) {
100  return;
101  }
102  auto buffer = incremental_buffers_.get_parsed_buffer();
103  if (buffer) {
104  parsed_ = nlohmann::json::parse(*buffer);
105  }
106  }
107 
108  static expected<response, std::string> parse_response(nlohmann::json&& res)
109  {
110  auto id_it = res.find("id");
111  auto result_it = res.find("result");
112  auto error_it = res.find("error");
113 
114  if (id_it == end(res)) {
115  return unexpected{"missing id field"};
116  }
117  if (result_it == end(res) && error_it == end(res)) {
118  return unexpected{"missing error and result field"};
119  }
120 
121  response parsed;
122  parsed.id = std::move(*id_it);
123  if (error_it != end(res)) {
124  parsed.error = std::move(*error_it);
125  }
126  if (result_it != end(res)) {
127  parsed.result = std::move(*result_it);
128  }
129  return {std::move(parsed)};
130  }
131 
132  static expected<request, std::string> parse_request(nlohmann::json&& req)
133  {
134  auto id_it = req.find("id");
135  auto method_it = req.find("method");
136  auto params_it = req.find("params");
137 
138  if (method_it == end(req)) {
139  return unexpected{"missing method field"};
140  }
141  if (!method_it->is_string()) {
142  return unexpected{"method field is not a string"};
143  }
144 
145  request parsed;
146  parsed.method = method_it->get<std::string>();
147  if (params_it == end(req) || params_it->is_null()) {
148  parsed.args = nlohmann::json::array();
149  }
150  else if (!params_it->is_array() && !params_it->is_object()) {
151  return unexpected{"non-structured arguments are not supported"};
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 {std::move(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 F>
323  static internal::expected<T, std::string> extract_args(
324  const nlohmann::json& args,
325  const args_specs<F>& specs)
326  {
327  try {
328  if (args.is_array()) {
329  return convert_positional_args<T>(args, specs);
330  }
331  else if (args.is_object()) {
332  return convert_named_args<T>(args, specs);
333  }
334  else {
335  throw std::runtime_error{"arguments are not a structured type"};
336  }
337  }
338  catch (const std::exception& exc) {
339  return internal::unexpected{
340  std::string{"cannot convert arguments: "} + exc.what()};
341  }
342  }
343 
344 private:
345  template <typename T, typename F>
346  static constexpr T convert_positional_args(
347  const nlohmann::json& array,
348  const args_specs<F>& specs)
349  {
350  return convert_positional_args<T>(
351  array, specs, std::make_index_sequence<args_specs<F>::size()>());
352  }
353 
354  template <typename T, typename F, std::size_t... Idxs>
355  static constexpr T convert_positional_args(
356  const nlohmann::json& array,
357  const args_specs<F>& specs,
358  std::index_sequence<Idxs...>)
359  {
360  if (!specs.options().allow_extra_arguments
361  && array.size() > std::tuple_size_v<T>) {
362  throw std::runtime_error{"too many arguments"};
363  }
364  return {[&]() {
365  if (Idxs < array.size()) {
366  try {
367  return array.at(Idxs).get<std::tuple_element_t<Idxs, T>>();
368  }
369  catch (const ::nlohmann::json::type_error&) {
370  throw std::runtime_error{
371  "invalid type for argument "
372  + specs.template get<Idxs>().name()};
373  }
374  }
375  if (const auto& value = specs.template get<Idxs>().default_value()) {
376  return *value;
377  }
378  throw std::runtime_error{
379  "no value for argument " + specs.template get<Idxs>().name()};
380  }()...};
381  }
382 
383  template <typename T, typename F>
384  static T convert_named_args(const nlohmann::json& args, const args_specs<F>& specs)
385  {
386  return convert_named_args<T>(
387  args, specs, std::make_index_sequence<args_specs<F>::size()>{});
388  }
389 
390  template <typename T, typename F, std::size_t... Idxs>
391  static T convert_named_args(
392  const nlohmann::json& args,
393  const args_specs<F>& specs,
394  std::index_sequence<Idxs...>)
395  {
396  if (!specs.options().allow_extra_arguments) {
397  const std::array<const std::string*, sizeof...(Idxs)>
398  available_arguments = {&specs.template get<Idxs>().name()...};
399  for (auto it = args.begin(); it != args.end(); ++it) {
400  auto arg_it = std::find_if(
401  available_arguments.begin(),
402  available_arguments.end(),
403  [&](const std::string* arg) { return *arg == it.key(); });
404  if (arg_it == available_arguments.end()) {
405  throw std::runtime_error{"unexpected argument " + it.key()};
406  }
407  }
408  }
409 
410  return T{[&]() {
411  auto it = args.find(specs.template get<Idxs>().name());
412  if (it != args.end()) {
413  try {
414  return it->template get<std::tuple_element_t<Idxs, T>>();
415  }
416  catch (const ::nlohmann::json::type_error&) {
417  throw std::runtime_error{
418  "invalid type for argument "
419  + specs.template get<Idxs>().name()};
420  }
421  }
422  if (const auto& value = specs.template get<Idxs>().default_value()) {
423  return *value;
424  }
425  throw std::runtime_error{
426  "no value for argument " + specs.template get<Idxs>().name()};
427  }()...};
428  }
429 };
430 
431 } // nl_json_rpc
432 } // packio
433 
434 #endif // PACKIO_NL_JSON_RPC_RPC_H
The incremental parser for JSON-RPC objects.
Definition: rpc.h:52
The JSON-RPC protocol implementation.
Definition: rpc.h:174
internal::native_type native_type
The native type of the serialization library.
Definition: rpc.h:180
internal::id_type id_type
Type of the call ID.
Definition: rpc.h:177
The packio namespace.
Definition: arg.h:14
The object representing a client request.
Definition: rpc.h:37
The object representing the response to a call.
Definition: rpc.h:45