Horizon
json_pointer.hpp
1 #pragma once
2 
3 #include <algorithm> // all_of
4 #include <cassert> // assert
5 #include <cctype> // isdigit
6 #include <numeric> // accumulate
7 #include <string> // string
8 #include <utility> // move
9 #include <vector> // vector
10 
11 #include <nlohmann/detail/exceptions.hpp>
12 #include <nlohmann/detail/macro_scope.hpp>
13 #include <nlohmann/detail/value_t.hpp>
14 
15 namespace nlohmann
16 {
17 template<typename BasicJsonType>
18 class json_pointer
19 {
20  // allow basic_json to access private members
21  NLOHMANN_BASIC_JSON_TPL_DECLARATION
22  friend class basic_json;
23 
24  public:
46  explicit json_pointer(const std::string& s = "")
47  : reference_tokens(split(s))
48  {}
49 
64  std::string to_string() const
65  {
66  return std::accumulate(reference_tokens.begin(), reference_tokens.end(),
67  std::string{},
68  [](const std::string & a, const std::string & b)
69  {
70  return a + "/" + escape(b);
71  });
72  }
73 
75  operator std::string() const
76  {
77  return to_string();
78  }
79 
97  {
98  reference_tokens.insert(reference_tokens.end(),
99  ptr.reference_tokens.begin(),
100  ptr.reference_tokens.end());
101  return *this;
102  }
103 
120  json_pointer& operator/=(std::string token)
121  {
122  push_back(std::move(token));
123  return *this;
124  }
125 
142  json_pointer& operator/=(std::size_t array_index)
143  {
144  return *this /= std::to_string(array_index);
145  }
146 
163  const json_pointer& rhs)
164  {
165  return json_pointer(lhs) /= rhs;
166  }
167 
183  friend json_pointer operator/(const json_pointer& ptr, std::string token)
184  {
185  return json_pointer(ptr) /= std::move(token);
186  }
187 
203  friend json_pointer operator/(const json_pointer& ptr, std::size_t array_index)
204  {
205  return json_pointer(ptr) /= array_index;
206  }
207 
222  {
223  if (empty())
224  {
225  return *this;
226  }
227 
228  json_pointer res = *this;
229  res.pop_back();
230  return res;
231  }
232 
246  void pop_back()
247  {
248  if (JSON_HEDLEY_UNLIKELY(empty()))
249  {
250  JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent"));
251  }
252 
253  reference_tokens.pop_back();
254  }
255 
270  const std::string& back() const
271  {
272  if (JSON_HEDLEY_UNLIKELY(empty()))
273  {
274  JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent"));
275  }
276 
277  return reference_tokens.back();
278  }
279 
292  void push_back(const std::string& token)
293  {
294  reference_tokens.push_back(token);
295  }
296 
298  void push_back(std::string&& token)
299  {
300  reference_tokens.push_back(std::move(token));
301  }
302 
317  bool empty() const noexcept
318  {
319  return reference_tokens.empty();
320  }
321 
322  private:
330  static int array_index(const std::string& s)
331  {
332  std::size_t processed_chars = 0;
333  const int res = std::stoi(s, &processed_chars);
334 
335  // check if the string was completely read
336  if (JSON_HEDLEY_UNLIKELY(processed_chars != s.size()))
337  {
338  JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + s + "'"));
339  }
340 
341  return res;
342  }
343 
344  json_pointer top() const
345  {
346  if (JSON_HEDLEY_UNLIKELY(empty()))
347  {
348  JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent"));
349  }
350 
351  json_pointer result = *this;
352  result.reference_tokens = {reference_tokens[0]};
353  return result;
354  }
355 
364  BasicJsonType& get_and_create(BasicJsonType& j) const
365  {
366  using size_type = typename BasicJsonType::size_type;
367  auto result = &j;
368 
369  // in case no reference tokens exist, return a reference to the JSON value
370  // j which will be overwritten by a primitive value
371  for (const auto& reference_token : reference_tokens)
372  {
373  switch (result->type())
374  {
376  {
377  if (reference_token == "0")
378  {
379  // start a new array if reference token is 0
380  result = &result->operator[](0);
381  }
382  else
383  {
384  // start a new object otherwise
385  result = &result->operator[](reference_token);
386  }
387  break;
388  }
389 
391  {
392  // create an entry in the object
393  result = &result->operator[](reference_token);
394  break;
395  }
396 
398  {
399  // create an entry in the array
400  JSON_TRY
401  {
402  result = &result->operator[](static_cast<size_type>(array_index(reference_token)));
403  }
404  JSON_CATCH(std::invalid_argument&)
405  {
406  JSON_THROW(detail::parse_error::create(109, 0, "array index '" + reference_token + "' is not a number"));
407  }
408  break;
409  }
410 
411  /*
412  The following code is only reached if there exists a reference
413  token _and_ the current value is primitive. In this case, we have
414  an error situation, because primitive values may only occur as
415  single value; that is, with an empty list of reference tokens.
416  */
417  default:
418  JSON_THROW(detail::type_error::create(313, "invalid value to unflatten"));
419  }
420  }
421 
422  return *result;
423  }
424 
444  BasicJsonType& get_unchecked(BasicJsonType* ptr) const
445  {
446  using size_type = typename BasicJsonType::size_type;
447  for (const auto& reference_token : reference_tokens)
448  {
449  // convert null values to arrays or objects before continuing
450  if (ptr->is_null())
451  {
452  // check if reference token is a number
453  const bool nums =
454  std::all_of(reference_token.begin(), reference_token.end(),
455  [](const unsigned char x)
456  {
457  return std::isdigit(x);
458  });
459 
460  // change value to array for numbers or "-" or to object otherwise
461  *ptr = (nums or reference_token == "-")
464  }
465 
466  switch (ptr->type())
467  {
469  {
470  // use unchecked object access
471  ptr = &ptr->operator[](reference_token);
472  break;
473  }
474 
476  {
477  // error condition (cf. RFC 6901, Sect. 4)
478  if (JSON_HEDLEY_UNLIKELY(reference_token.size() > 1 and reference_token[0] == '0'))
479  {
480  JSON_THROW(detail::parse_error::create(106, 0,
481  "array index '" + reference_token +
482  "' must not begin with '0'"));
483  }
484 
485  if (reference_token == "-")
486  {
487  // explicitly treat "-" as index beyond the end
488  ptr = &ptr->operator[](ptr->m_value.array->size());
489  }
490  else
491  {
492  // convert array index to number; unchecked access
493  JSON_TRY
494  {
495  ptr = &ptr->operator[](
496  static_cast<size_type>(array_index(reference_token)));
497  }
498  JSON_CATCH(std::invalid_argument&)
499  {
500  JSON_THROW(detail::parse_error::create(109, 0, "array index '" + reference_token + "' is not a number"));
501  }
502  }
503  break;
504  }
505 
506  default:
507  JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + reference_token + "'"));
508  }
509  }
510 
511  return *ptr;
512  }
513 
520  BasicJsonType& get_checked(BasicJsonType* ptr) const
521  {
522  using size_type = typename BasicJsonType::size_type;
523  for (const auto& reference_token : reference_tokens)
524  {
525  switch (ptr->type())
526  {
528  {
529  // note: at performs range check
530  ptr = &ptr->at(reference_token);
531  break;
532  }
533 
535  {
536  if (JSON_HEDLEY_UNLIKELY(reference_token == "-"))
537  {
538  // "-" always fails the range check
539  JSON_THROW(detail::out_of_range::create(402,
540  "array index '-' (" + std::to_string(ptr->m_value.array->size()) +
541  ") is out of range"));
542  }
543 
544  // error condition (cf. RFC 6901, Sect. 4)
545  if (JSON_HEDLEY_UNLIKELY(reference_token.size() > 1 and reference_token[0] == '0'))
546  {
547  JSON_THROW(detail::parse_error::create(106, 0,
548  "array index '" + reference_token +
549  "' must not begin with '0'"));
550  }
551 
552  // note: at performs range check
553  JSON_TRY
554  {
555  ptr = &ptr->at(static_cast<size_type>(array_index(reference_token)));
556  }
557  JSON_CATCH(std::invalid_argument&)
558  {
559  JSON_THROW(detail::parse_error::create(109, 0, "array index '" + reference_token + "' is not a number"));
560  }
561  break;
562  }
563 
564  default:
565  JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + reference_token + "'"));
566  }
567  }
568 
569  return *ptr;
570  }
571 
585  const BasicJsonType& get_unchecked(const BasicJsonType* ptr) const
586  {
587  using size_type = typename BasicJsonType::size_type;
588  for (const auto& reference_token : reference_tokens)
589  {
590  switch (ptr->type())
591  {
593  {
594  // use unchecked object access
595  ptr = &ptr->operator[](reference_token);
596  break;
597  }
598 
600  {
601  if (JSON_HEDLEY_UNLIKELY(reference_token == "-"))
602  {
603  // "-" cannot be used for const access
604  JSON_THROW(detail::out_of_range::create(402,
605  "array index '-' (" + std::to_string(ptr->m_value.array->size()) +
606  ") is out of range"));
607  }
608 
609  // error condition (cf. RFC 6901, Sect. 4)
610  if (JSON_HEDLEY_UNLIKELY(reference_token.size() > 1 and reference_token[0] == '0'))
611  {
612  JSON_THROW(detail::parse_error::create(106, 0,
613  "array index '" + reference_token +
614  "' must not begin with '0'"));
615  }
616 
617  // use unchecked array access
618  JSON_TRY
619  {
620  ptr = &ptr->operator[](
621  static_cast<size_type>(array_index(reference_token)));
622  }
623  JSON_CATCH(std::invalid_argument&)
624  {
625  JSON_THROW(detail::parse_error::create(109, 0, "array index '" + reference_token + "' is not a number"));
626  }
627  break;
628  }
629 
630  default:
631  JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + reference_token + "'"));
632  }
633  }
634 
635  return *ptr;
636  }
637 
644  const BasicJsonType& get_checked(const BasicJsonType* ptr) const
645  {
646  using size_type = typename BasicJsonType::size_type;
647  for (const auto& reference_token : reference_tokens)
648  {
649  switch (ptr->type())
650  {
652  {
653  // note: at performs range check
654  ptr = &ptr->at(reference_token);
655  break;
656  }
657 
659  {
660  if (JSON_HEDLEY_UNLIKELY(reference_token == "-"))
661  {
662  // "-" always fails the range check
663  JSON_THROW(detail::out_of_range::create(402,
664  "array index '-' (" + std::to_string(ptr->m_value.array->size()) +
665  ") is out of range"));
666  }
667 
668  // error condition (cf. RFC 6901, Sect. 4)
669  if (JSON_HEDLEY_UNLIKELY(reference_token.size() > 1 and reference_token[0] == '0'))
670  {
671  JSON_THROW(detail::parse_error::create(106, 0,
672  "array index '" + reference_token +
673  "' must not begin with '0'"));
674  }
675 
676  // note: at performs range check
677  JSON_TRY
678  {
679  ptr = &ptr->at(static_cast<size_type>(array_index(reference_token)));
680  }
681  JSON_CATCH(std::invalid_argument&)
682  {
683  JSON_THROW(detail::parse_error::create(109, 0, "array index '" + reference_token + "' is not a number"));
684  }
685  break;
686  }
687 
688  default:
689  JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + reference_token + "'"));
690  }
691  }
692 
693  return *ptr;
694  }
695 
700  bool contains(const BasicJsonType* ptr) const
701  {
702  using size_type = typename BasicJsonType::size_type;
703  for (const auto& reference_token : reference_tokens)
704  {
705  switch (ptr->type())
706  {
708  {
709  if (not ptr->contains(reference_token))
710  {
711  // we did not find the key in the object
712  return false;
713  }
714 
715  ptr = &ptr->operator[](reference_token);
716  break;
717  }
718 
720  {
721  if (JSON_HEDLEY_UNLIKELY(reference_token == "-"))
722  {
723  // "-" always fails the range check
724  return false;
725  }
726 
727  // error condition (cf. RFC 6901, Sect. 4)
728  if (JSON_HEDLEY_UNLIKELY(reference_token.size() > 1 and reference_token[0] == '0'))
729  {
730  JSON_THROW(detail::parse_error::create(106, 0,
731  "array index '" + reference_token +
732  "' must not begin with '0'"));
733  }
734 
735  JSON_TRY
736  {
737  const auto idx = static_cast<size_type>(array_index(reference_token));
738  if (idx >= ptr->size())
739  {
740  // index out of range
741  return false;
742  }
743 
744  ptr = &ptr->operator[](idx);
745  break;
746  }
747  JSON_CATCH(std::invalid_argument&)
748  {
749  JSON_THROW(detail::parse_error::create(109, 0, "array index '" + reference_token + "' is not a number"));
750  }
751  break;
752  }
753 
754  default:
755  {
756  // we do not expect primitive values if there is still a
757  // reference token to process
758  return false;
759  }
760  }
761  }
762 
763  // no reference token left means we found a primitive value
764  return true;
765  }
766 
776  static std::vector<std::string> split(const std::string& reference_string)
777  {
778  std::vector<std::string> result;
779 
780  // special case: empty reference string -> no reference tokens
781  if (reference_string.empty())
782  {
783  return result;
784  }
785 
786  // check if nonempty reference string begins with slash
787  if (JSON_HEDLEY_UNLIKELY(reference_string[0] != '/'))
788  {
789  JSON_THROW(detail::parse_error::create(107, 1,
790  "JSON pointer must be empty or begin with '/' - was: '" +
791  reference_string + "'"));
792  }
793 
794  // extract the reference tokens:
795  // - slash: position of the last read slash (or end of string)
796  // - start: position after the previous slash
797  for (
798  // search for the first slash after the first character
799  std::size_t slash = reference_string.find_first_of('/', 1),
800  // set the beginning of the first reference token
801  start = 1;
802  // we can stop if start == 0 (if slash == std::string::npos)
803  start != 0;
804  // set the beginning of the next reference token
805  // (will eventually be 0 if slash == std::string::npos)
806  start = (slash == std::string::npos) ? 0 : slash + 1,
807  // find next slash
808  slash = reference_string.find_first_of('/', start))
809  {
810  // use the text between the beginning of the reference token
811  // (start) and the last slash (slash).
812  auto reference_token = reference_string.substr(start, slash - start);
813 
814  // check reference tokens are properly escaped
815  for (std::size_t pos = reference_token.find_first_of('~');
816  pos != std::string::npos;
817  pos = reference_token.find_first_of('~', pos + 1))
818  {
819  assert(reference_token[pos] == '~');
820 
821  // ~ must be followed by 0 or 1
822  if (JSON_HEDLEY_UNLIKELY(pos == reference_token.size() - 1 or
823  (reference_token[pos + 1] != '0' and
824  reference_token[pos + 1] != '1')))
825  {
826  JSON_THROW(detail::parse_error::create(108, 0, "escape character '~' must be followed with '0' or '1'"));
827  }
828  }
829 
830  // finally, store the reference token
831  unescape(reference_token);
832  result.push_back(reference_token);
833  }
834 
835  return result;
836  }
837 
851  static void replace_substring(std::string& s, const std::string& f,
852  const std::string& t)
853  {
854  assert(not f.empty());
855  for (auto pos = s.find(f); // find first occurrence of f
856  pos != std::string::npos; // make sure f was found
857  s.replace(pos, f.size(), t), // replace with t, and
858  pos = s.find(f, pos + t.size())) // find next occurrence of f
859  {}
860  }
861 
863  static std::string escape(std::string s)
864  {
865  replace_substring(s, "~", "~0");
866  replace_substring(s, "/", "~1");
867  return s;
868  }
869 
871  static void unescape(std::string& s)
872  {
873  replace_substring(s, "~1", "/");
874  replace_substring(s, "~0", "~");
875  }
876 
884  static void flatten(const std::string& reference_string,
885  const BasicJsonType& value,
886  BasicJsonType& result)
887  {
888  switch (value.type())
889  {
891  {
892  if (value.m_value.array->empty())
893  {
894  // flatten empty array as null
895  result[reference_string] = nullptr;
896  }
897  else
898  {
899  // iterate array and use index as reference string
900  for (std::size_t i = 0; i < value.m_value.array->size(); ++i)
901  {
902  flatten(reference_string + "/" + std::to_string(i),
903  value.m_value.array->operator[](i), result);
904  }
905  }
906  break;
907  }
908 
910  {
911  if (value.m_value.object->empty())
912  {
913  // flatten empty object as null
914  result[reference_string] = nullptr;
915  }
916  else
917  {
918  // iterate object and use keys as reference string
919  for (const auto& element : *value.m_value.object)
920  {
921  flatten(reference_string + "/" + escape(element.first), element.second, result);
922  }
923  }
924  break;
925  }
926 
927  default:
928  {
929  // add primitive value with its reference string
930  result[reference_string] = value;
931  break;
932  }
933  }
934  }
935 
946  static BasicJsonType
947  unflatten(const BasicJsonType& value)
948  {
949  if (JSON_HEDLEY_UNLIKELY(not value.is_object()))
950  {
951  JSON_THROW(detail::type_error::create(314, "only objects can be unflattened"));
952  }
953 
954  BasicJsonType result;
955 
956  // iterate the JSON object values
957  for (const auto& element : *value.m_value.object)
958  {
959  if (JSON_HEDLEY_UNLIKELY(not element.second.is_primitive()))
960  {
961  JSON_THROW(detail::type_error::create(315, "values in object must be primitive"));
962  }
963 
964  // assign value to reference pointed to by JSON pointer; Note that if
965  // the JSON pointer is "" (i.e., points to the whole value), function
966  // get_and_create returns a reference to result itself. An assignment
967  // will then create a primitive value.
968  json_pointer(element.first).get_and_create(result) = element.second;
969  }
970 
971  return result;
972  }
973 
985  friend bool operator==(json_pointer const& lhs,
986  json_pointer const& rhs) noexcept
987  {
988  return lhs.reference_tokens == rhs.reference_tokens;
989  }
990 
1002  friend bool operator!=(json_pointer const& lhs,
1003  json_pointer const& rhs) noexcept
1004  {
1005  return not (lhs == rhs);
1006  }
1007 
1009  std::vector<std::string> reference_tokens;
1010 };
1011 } // namespace nlohmann
nlohmann::json_pointer::empty
bool empty() const noexcept
return whether pointer points to the root document
Definition: json_pointer.hpp:317
nlohmann::detail::value_t::null
@ null
null value
nlohmann::detail::value_t::object
@ 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:46
nlohmann::json_pointer::operator/
friend json_pointer operator/(const json_pointer &lhs, const json_pointer &rhs)
create a new JSON pointer by appending the right JSON pointer at the end of the left JSON pointer
Definition: json_pointer.hpp:162
nlohmann
namespace for Niels Lohmann
Definition: adl_serializer.hpp:9
nlohmann::json_pointer
JSON Pointer.
Definition: json_pointer.hpp:19
nlohmann::json_pointer::parent_pointer
json_pointer parent_pointer() const
returns the parent of this JSON pointer
Definition: json_pointer.hpp:221
nlohmann::json_pointer::to_string
std::string to_string() const
return a string representation of the JSON pointer
Definition: json_pointer.hpp:64
nlohmann::json_pointer::operator/
friend json_pointer operator/(const json_pointer &ptr, std::string token)
create a new JSON pointer by appending the unescaped token at the end of the JSON pointer
Definition: json_pointer.hpp:183
nlohmann::json_pointer::back
const std::string & back() const
return last reference token
Definition: json_pointer.hpp:270
nlohmann::json_pointer::operator/
friend json_pointer operator/(const json_pointer &ptr, std::size_t array_index)
create a new JSON pointer by appending the array-index-token at the end of the JSON pointer
Definition: json_pointer.hpp:203
nlohmann::json_pointer::pop_back
void pop_back()
remove last reference token
Definition: json_pointer.hpp:246
nlohmann::detail::value_t::array
@ array
array (ordered collection of values)
nlohmann::json_pointer::push_back
void push_back(std::string &&token)
append an unescaped token at the end of the reference pointer
Definition: json_pointer.hpp:298
nlohmann::json_pointer::operator!=
friend bool operator!=(json_pointer const &lhs, json_pointer const &rhs) noexcept
compares two JSON pointers for inequality
Definition: json_pointer.hpp:1002
nlohmann::json_pointer::operator/=
json_pointer & operator/=(std::string token)
append an unescaped reference token at the end of this JSON pointer
Definition: json_pointer.hpp:120
nlohmann::json_pointer::operator/=
json_pointer & operator/=(std::size_t array_index)
append an array index at the end of this JSON pointer
Definition: json_pointer.hpp:142
nlohmann::json_pointer::operator/=
json_pointer & operator/=(const json_pointer &ptr)
append another JSON pointer at the end of this JSON pointer
Definition: json_pointer.hpp:96
nlohmann::detail::parse_error::create
static parse_error create(int id_, const position_t &pos, const std::string &what_arg)
create a parse error exception
Definition: exceptions.hpp:129
nlohmann::json_pointer::push_back
void push_back(const std::string &token)
append an unescaped token at the end of the reference pointer
Definition: json_pointer.hpp:292
nlohmann::json_pointer::operator==
friend bool operator==(json_pointer const &lhs, json_pointer const &rhs) noexcept
compares two JSON pointers for equality
Definition: json_pointer.hpp:985