pointer.h 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. #pragma once
  2. #include <algorithm>
  3. #include <cassert>
  4. #include <charconv>
  5. #include <iostream>
  6. #include <string>
  7. #include <string_view>
  8. #include <system_error>
  9. #include <variant>
  10. #include <vector>
  11. #include <jvalidate/compat/compare.h>
  12. #include <jvalidate/compat/expected.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 {};
  30. constexpr parent_t parent;
  31. class Pointer {
  32. private:
  33. using Token = std::variant<std::string, size_t>;
  34. public:
  35. Pointer() = default;
  36. Pointer(std::vector<Token> const & tokens) : tokens_(tokens) {}
  37. /**
  38. * @brief Parse a JSON-Pointer from a serialized JSON-Pointer-String. In
  39. * principle, this should either be a factory function returning an optional/
  40. * throwing on error - but we'll generously assume that all JSON-Pointers are
  41. * valid - and therefore that an invalidly formatter pointer string will
  42. * point to somewhere non-existant (since it will be used in schema handling)
  43. */
  44. Pointer(std::string_view path) {
  45. if (path.empty()) {
  46. return;
  47. }
  48. // JSON-Pointers are required to start with a '/' although we only enforce
  49. // that rule in Reference.
  50. path.remove_prefix(1);
  51. // The rules of JSON-Pointer is that if a token were to contain a '/' as a
  52. // strict character: then that character would be escaped, using the above
  53. // rules. We take advantage of string_view's sliding view to make iteration
  54. // easy.
  55. for (size_t p = path.find('/'); p != std::string::npos;
  56. path.remove_prefix(p + 1), p = path.find('/')) {
  57. tokens_.push_back(parse_token(std::string(path.substr(0, p))).value());
  58. }
  59. tokens_.push_back(parse_token(std::string(path)).value());
  60. }
  61. static expected<Pointer, std::string> parse(std::string_view path) {
  62. if (path.empty()) {
  63. return Pointer();
  64. }
  65. // JSON-Pointers are required to start with a '/' although we only enforce
  66. // that rule in Reference.
  67. path.remove_prefix(1);
  68. // The rules of JSON-Pointer is that if a token were to contain a '/' as a
  69. // strict character: then that character would be escaped, using the above
  70. // rules. We take advantage of string_view's sliding view to make iteration
  71. // easy.
  72. Pointer rval;
  73. for (size_t p = path.find('/'); p != std::string::npos;
  74. path.remove_prefix(p + 1), p = path.find('/')) {
  75. expected token = parse_token(std::string(path.substr(0, p)));
  76. JVALIDATE_PROPIGATE_UNEXPECTED(token);
  77. rval.tokens_.push_back(*token);
  78. }
  79. expected token = parse_token(std::string(path));
  80. JVALIDATE_PROPIGATE_UNEXPECTED(token);
  81. rval.tokens_.push_back(*token);
  82. return rval;
  83. }
  84. template <typename T>
  85. static expected<T, std::string> parse_integer(std::string_view in, int base = 10) {
  86. T rval = 0;
  87. auto [ptr, ec] = std::from_chars(in.begin(), in.end(), rval, base);
  88. if (ec != std::errc{}) {
  89. return unexpected(std::make_error_code(ec).message());
  90. }
  91. return rval;
  92. }
  93. static expected<Token, std::string> parse_token(std::string in) {
  94. // Best-guess that the input token text represents a numeric value.
  95. // Technically - this could mean that we have an object key that is also
  96. // a number (e.g. the jsonized form of map<int, T>), but we can generally
  97. // assume that we are not going to use those kinds of paths in a reference
  98. // field. Therefore we don't need to include any clever tricks for storage
  99. if (not in.empty() && in.find_first_not_of("0123456789") == std::string::npos) {
  100. return parse_integer<size_t>(in);
  101. }
  102. for (size_t i = 0; i < in.size(); ++i) {
  103. // Allow URL-Escaped characters (%\x\x) to be turned into their
  104. // matching ASCII characters. This allows passing abnormal chars other
  105. // than '/' and '~' to be handled in all contexts.
  106. // TODO(samjaffe): Only do this if enc is hex-like (currently throws?)
  107. if (in[i] == '%') {
  108. if (auto code = parse_integer<char>(std::string_view(in).substr(i + 1, 2), 16)) {
  109. in.replace(i, 3, 1, *code);
  110. } else {
  111. return code.error();
  112. }
  113. } else if (in[i] != '~') {
  114. // Not a special char-sequence, does not need massaging
  115. continue;
  116. }
  117. // In order to properly support '/' inside the property name of an
  118. // object, we must escape it. The designers of the JSON-Pointer RFC
  119. // chose to use '~' as a special signifier. Mapping '~0' to '~', and
  120. // '~1' to '/'.
  121. if (in[i + 1] == '0') {
  122. in.replace(i, 2, 1, '~');
  123. } else if (in[i + 1] == '1') {
  124. in.replace(i, 2, 1, '/');
  125. } else {
  126. // return unexpected("illegal tilde '" + in.substr(i, 2) + "'");
  127. }
  128. }
  129. return in;
  130. }
  131. /**
  132. * @brief Dive into a JSON object throught the entire path of the this object
  133. *
  134. * @param document A JSON Adapter document - confirming to the following spec:
  135. * 1. Is indexable by size_t, returning its own type
  136. * 2. Is indexable by std::string, returning its own type
  137. * 3. Indexing into a null/incorrect json type, or for an absent child is safe
  138. *
  139. * @returns A new JSON Adapter at the pointed to location, or a generic null
  140. * JSON object.
  141. */
  142. auto walk(Adapter auto document) const {
  143. for (auto const & token : tokens_) {
  144. document = std::visit([&document](auto const & next) { return document[next]; }, token);
  145. }
  146. return document;
  147. }
  148. /**
  149. * @brief Fetch the last item in this pointer as a string (for easy
  150. * formatting). This function is used more-or-less exclusively to support the
  151. * improved annotation/error listing concepts described in the article:
  152. * https://json-schema.org/blog/posts/fixing-json-schema-output
  153. */
  154. std::string back() const {
  155. struct {
  156. std::string operator()(std::string const & in) const { return in; }
  157. std::string operator()(size_t in) const { return std::to_string(in); }
  158. } g_as_str;
  159. return tokens_.empty() ? "" : std::visit(g_as_str, tokens_.back());
  160. }
  161. bool empty() const { return tokens_.empty(); }
  162. /**
  163. * @brief Determines if this JSON-Pointer is prefixed by the other
  164. * JSON-Pointer. For example: `"/A/B/C"_jsptr.starts_with("/A/B") == true`
  165. *
  166. * This is an important thing to know when dealing with schemas that use
  167. * Anchors or nest $id tags in a singular document. Consider the schema below:
  168. * @code{.json}
  169. * {
  170. * "$id": "A",
  171. * "$defs": {
  172. * "B": {
  173. * "$anchor": "B"
  174. * "$defs": {
  175. * "C": {
  176. * "$anchor": "C"
  177. * }
  178. * }
  179. * }
  180. * }
  181. * }
  182. * @endcode
  183. *
  184. * How can we deduce that "A#B" and "A#C" are related to one-another as parent
  185. * and child nodes? First we translate them both into absolute (no-anchor)
  186. * forms "A#/$defs/B" and "A#/$defs/B/$defs/C". Visually - these are now
  187. * obviously related - but we need to expose the functionalty to make that
  188. * check happen (that "/$defs/B/$defs/C" starts with "/$defs/B").
  189. */
  190. bool starts_with(Pointer const & other) const {
  191. return other.tokens_.size() <= tokens_.size() &&
  192. std::equal(other.tokens_.begin(), other.tokens_.end(), tokens_.begin());
  193. }
  194. /**
  195. * @brief A corollary function to starts_with, create a "relative"
  196. * JSON-Pointer to some parent. Relative pointers are only partially supported
  197. * (e.g. if you tried to print it it would still emit the leading slash), so
  198. * the standard use case of this function is to either use it when choosing
  199. * a URI or Anchor that is a closer parent:
  200. * `Reference(uri, anchor, ptr.relative_to(other))`
  201. * or immediately concatenating it onto another absolute pointer:
  202. * `abs /= ptr.relative_to(other)`
  203. */
  204. Pointer relative_to(Pointer const & other) const {
  205. assert(starts_with(other));
  206. return Pointer(std::vector(tokens_.begin() + other.tokens_.size(), tokens_.end()));
  207. }
  208. Pointer parent(size_t i = 1) const { return Pointer({tokens_.begin(), tokens_.end() - i}); }
  209. Pointer & operator/=(Pointer const & relative) {
  210. tokens_.insert(tokens_.end(), relative.tokens_.begin(), relative.tokens_.end());
  211. return *this;
  212. }
  213. Pointer operator/(Pointer const & relative) const { return Pointer(*this) /= relative; }
  214. Pointer & operator/=(parent_t) {
  215. tokens_.pop_back();
  216. return *this;
  217. }
  218. Pointer operator/(parent_t) const { return parent(); }
  219. Pointer & operator/=(std::string_view key) {
  220. tokens_.emplace_back(std::string(key));
  221. return *this;
  222. }
  223. Pointer operator/(std::string_view key) const { return Pointer(*this) /= key; }
  224. Pointer & operator/=(size_t index) {
  225. tokens_.emplace_back(index);
  226. return *this;
  227. }
  228. Pointer operator/(size_t index) const { return Pointer(*this) /= index; }
  229. friend std::ostream & operator<<(std::ostream & os, Pointer const & self) {
  230. for (auto const & elem : self.tokens_) {
  231. std::visit([&os](auto const & v) { os << '/' << v; }, elem);
  232. }
  233. return os;
  234. }
  235. auto operator<=>(Pointer const &) const = default;
  236. private:
  237. std::vector<Token> tokens_{};
  238. };
  239. }