pointer.h 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. #pragma once
  2. #include <cassert>
  3. #include <cstddef>
  4. #include <cstdlib>
  5. #include <iostream>
  6. #include <stdexcept> // IWYU pragma: keep
  7. #include <string>
  8. #include <string_view>
  9. #include <jvalidate/compat/compare.h> // IWYU pragma: keep
  10. #include <jvalidate/detail/expect.h>
  11. #include <jvalidate/detail/number.h>
  12. #include <jvalidate/enum.h>
  13. #include <jvalidate/forward.h>
  14. namespace jvalidate::detail {
  15. /**
  16. * @brief A helper struct for use in appending elements to a json Pointer object
  17. * in a way that allows it to be used as a template parameter - similar to how
  18. * ostream allows operator<<(void(*)(ostream&)) to pass in a function callback
  19. * for implementing various iomanip functions as piped (read:fluent) values.
  20. *
  21. * However, the primary usecase for this is in a template context, where I want
  22. * to add 0-or-more path components to a JSON-Pointer of any type, and also want
  23. * to support neighbor Pointers, instead of only child Pointers.
  24. *
  25. * For example, @see ValidationVisitor::visit(constraint::ConditionalConstraint)
  26. * where we use parent to rewind the path back to the owning scope for
  27. * if-then-else processing.
  28. */
  29. struct parent_t {}; // NOLINT(readability-identifier-naming)
  30. constexpr parent_t parent; // NOLINT(readability-identifier-naming)
  31. class Pointer {
  32. private:
  33. class iterator; // NOLINT(readability-identifier-naming)
  34. public:
  35. Pointer() = default;
  36. /**
  37. * @brief Parse a JSON-Pointer from a serialized JSON-Pointer-String. In
  38. * principle, this should either be a factory function returning an optional/
  39. * throwing on error - but we'll generously assume that all JSON-Pointers are
  40. * valid - and therefore that an invalidly formatter pointer string will
  41. * point to somewhere non-existant (since it will be used in schema handling)
  42. */
  43. explicit(false) Pointer(std::string_view path);
  44. static std::string deserialize(std::string_view view) {
  45. std::string in(view);
  46. for (size_t i = 0; i < in.size(); ++i) {
  47. // Allow URL-Escaped characters (%\x\x) to be turned into their
  48. // matching ASCII characters. This allows passing abnormal chars other
  49. // than '/' and '~' to be handled in all contexts.
  50. // TODO(samjaffe): Only do this if enc is hex-like (currently throws?)
  51. if (in[i] == '%') {
  52. std::string_view const enc = std::string_view(in).substr(i + 1, 2);
  53. // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers)
  54. in.replace(i, 3, 1, from_str<char>(enc, 16));
  55. continue;
  56. }
  57. if (in[i] != '~') {
  58. // Not a special char-sequence, does not need massaging
  59. continue;
  60. }
  61. // In order to properly support '/' inside the property name of an
  62. // object, we must escape it. The designers of the JSON-Pointer RFC
  63. // chose to use '~' as a special signifier. Mapping '~0' to '~', and
  64. // '~1' to '/'.
  65. if (in[i + 1] == '0') {
  66. in.replace(i, 2, 1, '~');
  67. } else if (in[i + 1] == '1') {
  68. in.replace(i, 2, 1, '/');
  69. } else {
  70. JVALIDATE_THROW(std::runtime_error, "Illegal ~ code");
  71. }
  72. }
  73. return in;
  74. }
  75. /**
  76. * @brief Dive into a JSON object throught the entire path of the this object
  77. *
  78. * @param document A JSON Adapter document - confirming to the following spec:
  79. * 1. Is indexable by size_t, returning its own type
  80. * 2. Is indexable by std::string, returning its own type
  81. * 3. Indexing into a null/incorrect json type, or for an absent child is safe
  82. *
  83. * @returns A new JSON Adapter at the pointed to location, or a generic null
  84. * JSON object.
  85. */
  86. auto walk(Adapter auto document) const;
  87. /**
  88. * @brief Fetch the last item in this pointer as a string (for easy
  89. * formatting). This function is used more-or-less exclusively to support the
  90. * improved annotation/error listing concepts described in the article:
  91. * https://json-schema.org/blog/posts/fixing-json-schema-output
  92. */
  93. std::string back() const;
  94. bool empty() const { return value_.empty(); }
  95. /**
  96. * @brief Determines if this JSON-Pointer is prefixed by the other
  97. * JSON-Pointer. For example: `"/A/B/C"_jsptr.starts_with("/A/B") == true`
  98. *
  99. * This is an important thing to know when dealing with schemas that use
  100. * Anchors or nest $id tags in a singular document. Consider the schema below:
  101. * @code{.json}
  102. * {
  103. * "$id": "A",
  104. * "$defs": {
  105. * "B": {
  106. * "$anchor": "B"
  107. * "$defs": {
  108. * "C": {
  109. * "$anchor": "C"
  110. * }
  111. * }
  112. * }
  113. * }
  114. * }
  115. * @endcode
  116. *
  117. * How can we deduce that "A#B" and "A#C" are related to one-another as parent
  118. * and child nodes? First we translate them both into absolute (no-anchor)
  119. * forms "A#/$defs/B" and "A#/$defs/B/$defs/C". Visually - these are now
  120. * obviously related - but we need to expose the functionalty to make that
  121. * check happen (that "/$defs/B/$defs/C" starts with "/$defs/B").
  122. */
  123. bool starts_with(Pointer const & other) const { return value_.starts_with(other.value_); }
  124. /**
  125. * @brief A corollary function to starts_with, create a "relative"
  126. * JSON-Pointer to some parent. Relative pointers are only partially supported
  127. * (e.g. if you tried to print it it would still emit the leading slash), so
  128. * the standard use case of this function is to either use it when choosing
  129. * a URI or Anchor that is a closer parent:
  130. * `Reference(uri, anchor, ptr.relative_to(other))`
  131. * or immediately concatenating it onto another absolute pointer:
  132. * `abs /= ptr.relative_to(other)`
  133. */
  134. Pointer relative_to(Pointer const & other) const {
  135. assert(starts_with(other));
  136. Pointer rval;
  137. rval.value_ = value_.substr(other.value_.size());
  138. return rval;
  139. }
  140. Pointer parent(size_t levels = 1) const;
  141. Pointer & operator/=(Pointer const & relative) {
  142. value_ += relative.value_;
  143. return *this;
  144. }
  145. Pointer operator/(Pointer const & relative) const { return Pointer(*this) /= relative; }
  146. Pointer & operator/=(parent_t);
  147. Pointer operator/(parent_t) const { return parent(); }
  148. Pointer & operator/=(std::string_view key) {
  149. value_ += '/';
  150. value_ += std::string(key);
  151. return *this;
  152. }
  153. Pointer operator/(std::string_view key) const { return Pointer(*this) /= key; }
  154. Pointer & operator/=(size_t index) {
  155. value_ += '/';
  156. value_ += std::to_string(index);
  157. return *this;
  158. }
  159. Pointer operator/(size_t index) const { return Pointer(*this) /= index; }
  160. iterator begin() const;
  161. iterator end() const;
  162. explicit operator std::string const &() const { return value_; }
  163. friend std::ostream & operator<<(std::ostream & os, Pointer const & self) {
  164. return os << self.value_;
  165. }
  166. auto operator<=>(Pointer const &) const = default;
  167. private:
  168. std::string value_;
  169. };
  170. class Pointer::iterator {
  171. public:
  172. using value_type = std::string_view;
  173. using reference = std::string_view;
  174. using pointer = void;
  175. using difference_type = std::ptrdiff_t;
  176. using iterator_category = std::bidirectional_iterator_tag;
  177. explicit iterator(std::string_view view, size_t position = std::string_view::npos) : view_(view) {
  178. if (position < view.size()) {
  179. curr_ = position;
  180. next_ = view_.find('/', curr_ + 1);
  181. }
  182. }
  183. std::string_view operator*() const {
  184. if (next_ == std::string_view::npos) {
  185. return view_.substr(curr_ + 1);
  186. }
  187. return view_.substr(curr_ + 1, next_ - curr_ - 1);
  188. }
  189. iterator & operator++() {
  190. curr_ = next_;
  191. if (curr_ != std::string_view::npos) {
  192. next_ = view_.find('/', curr_ + 1);
  193. }
  194. return *this;
  195. }
  196. iterator & operator--() {
  197. next_ = curr_;
  198. if (next_ == std::string_view::npos) {
  199. curr_ = view_.rfind('/');
  200. } else if (next_ != 0) {
  201. curr_ = view_.rfind('/', next_ - 1);
  202. }
  203. return *this;
  204. }
  205. friend bool operator==(iterator const & lhs, iterator const & rhs) = default;
  206. private:
  207. friend class Pointer;
  208. std::string_view view_;
  209. size_t curr_ = std::string_view::npos;
  210. size_t next_ = std::string_view::npos;
  211. };
  212. inline auto Pointer::begin() const -> iterator { return iterator(value_, 0); }
  213. inline auto Pointer::end() const -> iterator { return iterator(value_); }
  214. inline std::string Pointer::back() const { return std::string(*--end()); }
  215. inline Pointer Pointer::parent(size_t levels) const {
  216. if (levels == 0) {
  217. return *this;
  218. }
  219. iterator it = end();
  220. std::advance(it, -levels);
  221. if (it.curr_ > value_.size()) {
  222. return {};
  223. }
  224. Pointer rval = *this;
  225. rval.value_.resize(it.curr_);
  226. return rval;
  227. }
  228. inline Pointer & Pointer::operator/=(parent_t) {
  229. iterator it = --end();
  230. value_.resize(it.curr_ > value_.size() ? 0 : it.curr_);
  231. return *this;
  232. }
  233. inline Pointer::Pointer(std::string_view path) : value_(path) {
  234. if (path.empty()) {
  235. return;
  236. }
  237. // JSON-Pointers are required to start with a '/'.
  238. EXPECT_M(path.starts_with('/'), "Missing leading '/' in JSON Pointer: " << path);
  239. // The rules of JSON-Pointer is that if a token were to contain a '/' as a
  240. // strict character: then that character would be escaped, using the above
  241. // rules. We take advantage of string_view's sliding view to make iteration
  242. // easy.
  243. for (std::string_view token : *this) {
  244. deserialize(token);
  245. }
  246. }
  247. inline auto Pointer::walk(Adapter auto document) const {
  248. for (std::string_view token : *this) {
  249. if (document.type() == adapter::Type::Array) {
  250. document = document[from_str<size_t>(token)];
  251. continue;
  252. }
  253. document = document[deserialize(token)];
  254. }
  255. return document;
  256. }
  257. }
  258. template <> struct std::hash<jvalidate::detail::Pointer> {
  259. auto operator()(jvalidate::detail::Pointer const & value) const {
  260. return std::hash<std::string>()(static_cast<std::string const &>(value));
  261. }
  262. };