Horizon
json_pointer.hpp
1 #pragma once
2 
3 #include <cassert> // assert
4 #include <numeric> // accumulate
5 #include <string> // string
6 #include <vector> // vector
7 
8 #include <nlohmann/detail/macro_scope.hpp>
9 #include <nlohmann/detail/exceptions.hpp>
10 #include <nlohmann/detail/value_t.hpp>
11 
12 namespace nlohmann
13 {
14 template<typename BasicJsonType>
16 {
17  // allow basic_json to access private members
18  NLOHMANN_BASIC_JSON_TPL_DECLARATION
19  friend class basic_json;
20 
21  public:
43  explicit json_pointer(const std::string& s = "")
44  : reference_tokens(split(s))
45  {}
46 
62  std::string to_string() const noexcept
63  {
64  return std::accumulate(reference_tokens.begin(), reference_tokens.end(),
65  std::string{},
66  [](const std::string & a, const std::string & b)
67  {
68  return a + "/" + escape(b);
69  });
70  }
71 
73  operator std::string() const
74  {
75  return to_string();
76  }
77 
85  static int array_index(const std::string& s)
86  {
87  std::size_t processed_chars = 0;
88  const int res = std::stoi(s, &processed_chars);
89 
90  // check if the string was completely read
91  if (JSON_UNLIKELY(processed_chars != s.size()))
92  {
93  JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + s + "'"));
94  }
95 
96  return res;
97  }
98 
99  private:
104  std::string pop_back()
105  {
106  if (JSON_UNLIKELY(is_root()))
107  {
108  JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent"));
109  }
110 
111  auto last = reference_tokens.back();
112  reference_tokens.pop_back();
113  return last;
114  }
115 
117  bool is_root() const
118  {
119  return reference_tokens.empty();
120  }
121 
122  json_pointer top() const
123  {
124  if (JSON_UNLIKELY(is_root()))
125  {
126  JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent"));
127  }
128 
129  json_pointer result = *this;
130  result.reference_tokens = {reference_tokens[0]};
131  return result;
132  }
133 
142  BasicJsonType& get_and_create(BasicJsonType& j) const
143  {
144  using size_type = typename BasicJsonType::size_type;
145  auto result = &j;
146 
147  // in case no reference tokens exist, return a reference to the JSON value
148  // j which will be overwritten by a primitive value
149  for (const auto& reference_token : reference_tokens)
150  {
151  switch (result->m_type)
152  {
154  {
155  if (reference_token == "0")
156  {
157  // start a new array if reference token is 0
158  result = &result->operator[](0);
159  }
160  else
161  {
162  // start a new object otherwise
163  result = &result->operator[](reference_token);
164  }
165  break;
166  }
167 
169  {
170  // create an entry in the object
171  result = &result->operator[](reference_token);
172  break;
173  }
174 
176  {
177  // create an entry in the array
178  JSON_TRY
179  {
180  result = &result->operator[](static_cast<size_type>(array_index(reference_token)));
181  }
182  JSON_CATCH(std::invalid_argument&)
183  {
184  JSON_THROW(detail::parse_error::create(109, 0, "array index '" + reference_token + "' is not a number"));
185  }
186  break;
187  }
188 
189  /*
190  The following code is only reached if there exists a reference
191  token _and_ the current value is primitive. In this case, we have
192  an error situation, because primitive values may only occur as
193  single value; that is, with an empty list of reference tokens.
194  */
195  default:
196  JSON_THROW(detail::type_error::create(313, "invalid value to unflatten"));
197  }
198  }
199 
200  return *result;
201  }
202 
222  BasicJsonType& get_unchecked(BasicJsonType* ptr) const
223  {
224  using size_type = typename BasicJsonType::size_type;
225  for (const auto& reference_token : reference_tokens)
226  {
227  // convert null values to arrays or objects before continuing
228  if (ptr->m_type == detail::value_t::null)
229  {
230  // check if reference token is a number
231  const bool nums =
232  std::all_of(reference_token.begin(), reference_token.end(),
233  [](const char x)
234  {
235  return (x >= '0' and x <= '9');
236  });
237 
238  // change value to array for numbers or "-" or to object otherwise
239  *ptr = (nums or reference_token == "-")
242  }
243 
244  switch (ptr->m_type)
245  {
247  {
248  // use unchecked object access
249  ptr = &ptr->operator[](reference_token);
250  break;
251  }
252 
254  {
255  // error condition (cf. RFC 6901, Sect. 4)
256  if (JSON_UNLIKELY(reference_token.size() > 1 and reference_token[0] == '0'))
257  {
258  JSON_THROW(detail::parse_error::create(106, 0,
259  "array index '" + reference_token +
260  "' must not begin with '0'"));
261  }
262 
263  if (reference_token == "-")
264  {
265  // explicitly treat "-" as index beyond the end
266  ptr = &ptr->operator[](ptr->m_value.array->size());
267  }
268  else
269  {
270  // convert array index to number; unchecked access
271  JSON_TRY
272  {
273  ptr = &ptr->operator[](
274  static_cast<size_type>(array_index(reference_token)));
275  }
276  JSON_CATCH(std::invalid_argument&)
277  {
278  JSON_THROW(detail::parse_error::create(109, 0, "array index '" + reference_token + "' is not a number"));
279  }
280  }
281  break;
282  }
283 
284  default:
285  JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + reference_token + "'"));
286  }
287  }
288 
289  return *ptr;
290  }
291 
298  BasicJsonType& get_checked(BasicJsonType* ptr) const
299  {
300  using size_type = typename BasicJsonType::size_type;
301  for (const auto& reference_token : reference_tokens)
302  {
303  switch (ptr->m_type)
304  {
306  {
307  // note: at performs range check
308  ptr = &ptr->at(reference_token);
309  break;
310  }
311 
313  {
314  if (JSON_UNLIKELY(reference_token == "-"))
315  {
316  // "-" always fails the range check
317  JSON_THROW(detail::out_of_range::create(402,
318  "array index '-' (" + std::to_string(ptr->m_value.array->size()) +
319  ") is out of range"));
320  }
321 
322  // error condition (cf. RFC 6901, Sect. 4)
323  if (JSON_UNLIKELY(reference_token.size() > 1 and reference_token[0] == '0'))
324  {
325  JSON_THROW(detail::parse_error::create(106, 0,
326  "array index '" + reference_token +
327  "' must not begin with '0'"));
328  }
329 
330  // note: at performs range check
331  JSON_TRY
332  {
333  ptr = &ptr->at(static_cast<size_type>(array_index(reference_token)));
334  }
335  JSON_CATCH(std::invalid_argument&)
336  {
337  JSON_THROW(detail::parse_error::create(109, 0, "array index '" + reference_token + "' is not a number"));
338  }
339  break;
340  }
341 
342  default:
343  JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + reference_token + "'"));
344  }
345  }
346 
347  return *ptr;
348  }
349 
363  const BasicJsonType& get_unchecked(const BasicJsonType* ptr) const
364  {
365  using size_type = typename BasicJsonType::size_type;
366  for (const auto& reference_token : reference_tokens)
367  {
368  switch (ptr->m_type)
369  {
371  {
372  // use unchecked object access
373  ptr = &ptr->operator[](reference_token);
374  break;
375  }
376 
378  {
379  if (JSON_UNLIKELY(reference_token == "-"))
380  {
381  // "-" cannot be used for const access
382  JSON_THROW(detail::out_of_range::create(402,
383  "array index '-' (" + std::to_string(ptr->m_value.array->size()) +
384  ") is out of range"));
385  }
386 
387  // error condition (cf. RFC 6901, Sect. 4)
388  if (JSON_UNLIKELY(reference_token.size() > 1 and reference_token[0] == '0'))
389  {
390  JSON_THROW(detail::parse_error::create(106, 0,
391  "array index '" + reference_token +
392  "' must not begin with '0'"));
393  }
394 
395  // use unchecked array access
396  JSON_TRY
397  {
398  ptr = &ptr->operator[](
399  static_cast<size_type>(array_index(reference_token)));
400  }
401  JSON_CATCH(std::invalid_argument&)
402  {
403  JSON_THROW(detail::parse_error::create(109, 0, "array index '" + reference_token + "' is not a number"));
404  }
405  break;
406  }
407 
408  default:
409  JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + reference_token + "'"));
410  }
411  }
412 
413  return *ptr;
414  }
415 
422  const BasicJsonType& get_checked(const BasicJsonType* ptr) const
423  {
424  using size_type = typename BasicJsonType::size_type;
425  for (const auto& reference_token : reference_tokens)
426  {
427  switch (ptr->m_type)
428  {
430  {
431  // note: at performs range check
432  ptr = &ptr->at(reference_token);
433  break;
434  }
435 
437  {
438  if (JSON_UNLIKELY(reference_token == "-"))
439  {
440  // "-" always fails the range check
441  JSON_THROW(detail::out_of_range::create(402,
442  "array index '-' (" + std::to_string(ptr->m_value.array->size()) +
443  ") is out of range"));
444  }
445 
446  // error condition (cf. RFC 6901, Sect. 4)
447  if (JSON_UNLIKELY(reference_token.size() > 1 and reference_token[0] == '0'))
448  {
449  JSON_THROW(detail::parse_error::create(106, 0,
450  "array index '" + reference_token +
451  "' must not begin with '0'"));
452  }
453 
454  // note: at performs range check
455  JSON_TRY
456  {
457  ptr = &ptr->at(static_cast<size_type>(array_index(reference_token)));
458  }
459  JSON_CATCH(std::invalid_argument&)
460  {
461  JSON_THROW(detail::parse_error::create(109, 0, "array index '" + reference_token + "' is not a number"));
462  }
463  break;
464  }
465 
466  default:
467  JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + reference_token + "'"));
468  }
469  }
470 
471  return *ptr;
472  }
473 
483  static std::vector<std::string> split(const std::string& reference_string)
484  {
485  std::vector<std::string> result;
486 
487  // special case: empty reference string -> no reference tokens
488  if (reference_string.empty())
489  {
490  return result;
491  }
492 
493  // check if nonempty reference string begins with slash
494  if (JSON_UNLIKELY(reference_string[0] != '/'))
495  {
496  JSON_THROW(detail::parse_error::create(107, 1,
497  "JSON pointer must be empty or begin with '/' - was: '" +
498  reference_string + "'"));
499  }
500 
501  // extract the reference tokens:
502  // - slash: position of the last read slash (or end of string)
503  // - start: position after the previous slash
504  for (
505  // search for the first slash after the first character
506  std::size_t slash = reference_string.find_first_of('/', 1),
507  // set the beginning of the first reference token
508  start = 1;
509  // we can stop if start == string::npos+1 = 0
510  start != 0;
511  // set the beginning of the next reference token
512  // (will eventually be 0 if slash == std::string::npos)
513  start = slash + 1,
514  // find next slash
515  slash = reference_string.find_first_of('/', start))
516  {
517  // use the text between the beginning of the reference token
518  // (start) and the last slash (slash).
519  auto reference_token = reference_string.substr(start, slash - start);
520 
521  // check reference tokens are properly escaped
522  for (std::size_t pos = reference_token.find_first_of('~');
523  pos != std::string::npos;
524  pos = reference_token.find_first_of('~', pos + 1))
525  {
526  assert(reference_token[pos] == '~');
527 
528  // ~ must be followed by 0 or 1
529  if (JSON_UNLIKELY(pos == reference_token.size() - 1 or
530  (reference_token[pos + 1] != '0' and
531  reference_token[pos + 1] != '1')))
532  {
533  JSON_THROW(detail::parse_error::create(108, 0, "escape character '~' must be followed with '0' or '1'"));
534  }
535  }
536 
537  // finally, store the reference token
538  unescape(reference_token);
539  result.push_back(reference_token);
540  }
541 
542  return result;
543  }
544 
558  static void replace_substring(std::string& s, const std::string& f,
559  const std::string& t)
560  {
561  assert(not f.empty());
562  for (auto pos = s.find(f); // find first occurrence of f
563  pos != std::string::npos; // make sure f was found
564  s.replace(pos, f.size(), t), // replace with t, and
565  pos = s.find(f, pos + t.size())) // find next occurrence of f
566  {}
567  }
568 
570  static std::string escape(std::string s)
571  {
572  replace_substring(s, "~", "~0");
573  replace_substring(s, "/", "~1");
574  return s;
575  }
576 
578  static void unescape(std::string& s)
579  {
580  replace_substring(s, "~1", "/");
581  replace_substring(s, "~0", "~");
582  }
583 
591  static void flatten(const std::string& reference_string,
592  const BasicJsonType& value,
593  BasicJsonType& result)
594  {
595  switch (value.m_type)
596  {
598  {
599  if (value.m_value.array->empty())
600  {
601  // flatten empty array as null
602  result[reference_string] = nullptr;
603  }
604  else
605  {
606  // iterate array and use index as reference string
607  for (std::size_t i = 0; i < value.m_value.array->size(); ++i)
608  {
609  flatten(reference_string + "/" + std::to_string(i),
610  value.m_value.array->operator[](i), result);
611  }
612  }
613  break;
614  }
615 
617  {
618  if (value.m_value.object->empty())
619  {
620  // flatten empty object as null
621  result[reference_string] = nullptr;
622  }
623  else
624  {
625  // iterate object and use keys as reference string
626  for (const auto& element : *value.m_value.object)
627  {
628  flatten(reference_string + "/" + escape(element.first), element.second, result);
629  }
630  }
631  break;
632  }
633 
634  default:
635  {
636  // add primitive value with its reference string
637  result[reference_string] = value;
638  break;
639  }
640  }
641  }
642 
653  static BasicJsonType
654  unflatten(const BasicJsonType& value)
655  {
656  if (JSON_UNLIKELY(not value.is_object()))
657  {
658  JSON_THROW(detail::type_error::create(314, "only objects can be unflattened"));
659  }
660 
661  BasicJsonType result;
662 
663  // iterate the JSON object values
664  for (const auto& element : *value.m_value.object)
665  {
666  if (JSON_UNLIKELY(not element.second.is_primitive()))
667  {
668  JSON_THROW(detail::type_error::create(315, "values in object must be primitive"));
669  }
670 
671  // assign value to reference pointed to by JSON pointer; Note that if
672  // the JSON pointer is "" (i.e., points to the whole value), function
673  // get_and_create returns a reference to result itself. An assignment
674  // will then create a primitive value.
675  json_pointer(element.first).get_and_create(result) = element.second;
676  }
677 
678  return result;
679  }
680 
681  friend bool operator==(json_pointer const& lhs,
682  json_pointer const& rhs) noexcept
683  {
684  return (lhs.reference_tokens == rhs.reference_tokens);
685  }
686 
687  friend bool operator!=(json_pointer const& lhs,
688  json_pointer const& rhs) noexcept
689  {
690  return not (lhs == rhs);
691  }
692 
694  std::vector<std::string> reference_tokens;
695 };
696 }
nlohmann::detail::value_t::null
null value
nlohmann::detail::value_t::object
object (unordered set of name/value pairs)
nlohmann::json_pointer::json_pointer
json_pointer(const std::string &s="")
create JSON pointer
Definition: json_pointer.hpp:43
nlohmann
namespace for Niels Lohmann
Definition: adl_serializer.hpp:8
nlohmann::json_pointer
JSON Pointer.
Definition: json_pointer.hpp:15
nlohmann::detail::parse_error::create
static parse_error create(int id_, std::size_t byte_, const std::string &what_arg)
create a parse error exception
Definition: exceptions.hpp:122
nlohmann::detail::value_t::array
array (ordered collection of values)
nlohmann::basic_json
a class to store JSON values
Definition: json.hpp:161
nlohmann::json_pointer::to_string
std::string to_string() const noexcept
return a string representation of the JSON pointer
Definition: json_pointer.hpp:62
nlohmann::json_pointer::array_index
static int array_index(const std::string &s)
Definition: json_pointer.hpp:85