| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 |
- #pragma once
- #include <algorithm>
- #include <cassert>
- #include <iostream>
- #include <string>
- #include <string_view>
- #include <variant>
- #include <vector>
- #include <jvalidate/compat/compare.h>
- #include <jvalidate/forward.h>
- namespace jvalidate::detail {
- /**
- * @brief A helper struct for use in appending elements to a json Pointer object
- * in a way that allows it to be used as a template parameter - similar to how
- * ostream allows operator<<(void(*)(ostream&)) to pass in a function callback
- * for implementing various iomanip functions as piped (read:fluent) values.
- *
- * However, the primary usecase for this is in a template context, where I want
- * to add 0-or-more path components to a JSON-Pointer of any type, and also want
- * to support neighbor Pointers, instead of only child Pointers.
- *
- * For example, @see ValidationVisitor::visit(constraint::ConditionalConstraint)
- * where we use parent to rewind the path back to the owning scope for
- * if-then-else processing.
- */
- struct parent_t {};
- constexpr parent_t parent;
- class Pointer {
- public:
- Pointer() = default;
- Pointer(std::vector<std::variant<std::string, size_t>> const & tokens) : tokens_(tokens) {}
- /**
- * @brief Parse a JSON-Pointer from a serialized JSON-Pointer-String. In
- * principle, this should either be a factory function returning an optional/
- * throwing on error - but we'll generously assume that all JSON-Pointers are
- * valid - and therefore that an invalidly formatter pointer string will
- * point to somewhere non-existant (since it will be used in schema handling)
- */
- Pointer(std::string_view path) {
- if (path.empty()) {
- return;
- }
- auto append_with_parse = [this](std::string in) {
- // Best-guess that the input token text represents a numeric value.
- // Technically - this could mean that we have an object key that is also
- // a number (e.g. the jsonized form of map<int, T>), but we can generally
- // assume that we are not going to use those kinds of paths in a reference
- // field. Therefore we don't need to include any clever tricks for storage
- if (not in.empty() && in.find_first_not_of("0123456789") == std::string::npos) {
- return tokens_.push_back(std::stoull(in));
- }
- for (size_t i = 0; i < in.size(); ++i) {
- // Allow URL-Escaped characters (%\x\x) to be turned into their
- // matching ASCII characters. This allows passing abnormal chars other
- // than '/' and '~' to be handled in all contexts.
- // TODO(samjaffe): Only do this if enc is hex-like (currently throws?)
- if (in[i] == '%') {
- char const enc[3] = {in[i + 1], in[i + 2]};
- in.replace(i, 3, 1, char(std::stoi(enc, nullptr, 16)));
- } else if (in[i] != '~') {
- // Not a special char-sequence, does not need massaging
- continue;
- }
- // In order to properly support '/' inside the property name of an
- // object, we must escape it. The designers of the JSON-Pointer RFC
- // chose to use '~' as a special signifier. Mapping '~0' to '~', and
- // '~1' to '/'.
- if (in[i + 1] == '0') {
- in.replace(i, 2, 1, '~');
- } else if (in[i + 1] == '1') {
- in.replace(i, 2, 1, '/');
- }
- }
- tokens_.push_back(std::move(in));
- };
- // JSON-Pointers are required to start with a '/' although we only enforce
- // that rule in Reference.
- path.remove_prefix(1);
- // The rules of JSON-Pointer is that if a token were to contain a '/' as a
- // strict character: then that character would be escaped, using the above
- // rules. We take advantage of string_view's sliding view to make iteration
- // easy.
- for (size_t p = path.find('/'); p != std::string::npos;
- path.remove_prefix(p + 1), p = path.find('/')) {
- append_with_parse(std::string(path.substr(0, p)));
- }
- append_with_parse(std::string(path));
- }
- /**
- * @brief Dive into a JSON object throught the entire path of the this object
- *
- * @param document A JSON Adapter document - confirming to the following spec:
- * 1. Is indexable by size_t, returning its own type
- * 2. Is indexable by std::string, returning its own type
- * 3. Indexing into a null/incorrect json type, or for an absent child is safe
- *
- * @returns A new JSON Adapter at the pointed to location, or a generic null
- * JSON object.
- */
- auto walk(Adapter auto document) const {
- for (auto const & token : tokens_) {
- document = std::visit([&document](auto const & next) { return document[next]; }, token);
- }
- return document;
- }
- /**
- * @brief Fetch the last item in this pointer as a string (for easy
- * formatting). This function is used more-or-less exclusively to support the
- * improved annotation/error listing concepts described in the article:
- * https://json-schema.org/blog/posts/fixing-json-schema-output
- */
- std::string back() const {
- struct {
- std::string operator()(std::string const & in) const { return in; }
- std::string operator()(size_t in) const { return std::to_string(in); }
- } g_as_str;
- return tokens_.empty() ? "" : std::visit(g_as_str, tokens_.back());
- }
- bool empty() const { return tokens_.empty(); }
- /**
- * @brief Determines if this JSON-Pointer is prefixed by the other
- * JSON-Pointer. For example: `"/A/B/C"_jsptr.starts_with("/A/B") == true`
- *
- * This is an important thing to know when dealing with schemas that use
- * Anchors or nest $id tags in a singular document. Consider the schema below:
- * @code{.json}
- * {
- * "$id": "A",
- * "$defs": {
- * "B": {
- * "$anchor": "B"
- * "$defs": {
- * "C": {
- * "$anchor": "C"
- * }
- * }
- * }
- * }
- * }
- * @endcode
- *
- * How can we deduce that "A#B" and "A#C" are related to one-another as parent
- * and child nodes? First we translate them both into absolute (no-anchor)
- * forms "A#/$defs/B" and "A#/$defs/B/$defs/C". Visually - these are now
- * obviously related - but we need to expose the functionalty to make that
- * check happen (that "/$defs/B/$defs/C" starts with "/$defs/B").
- */
- bool starts_with(Pointer const & other) const {
- return other.tokens_.size() <= tokens_.size() &&
- std::equal(other.tokens_.begin(), other.tokens_.end(), tokens_.begin());
- }
- /**
- * @brief A corollary function to starts_with, create a "relative"
- * JSON-Pointer to some parent. Relative pointers are only partially supported
- * (e.g. if you tried to print it it would still emit the leading slash), so
- * the standard use case of this function is to either use it when choosing
- * a URI or Anchor that is a closer parent:
- * `Reference(uri, anchor, ptr.relative_to(other))`
- * or immediately concatenating it onto another absolute pointer:
- * `abs /= ptr.relative_to(other)`
- */
- Pointer relative_to(Pointer const & other) const {
- assert(starts_with(other));
- return Pointer(std::vector(tokens_.begin() + other.tokens_.size(), tokens_.end()));
- }
- Pointer parent(size_t i = 1) const { return Pointer({tokens_.begin(), tokens_.end() - i}); }
- Pointer & operator/=(Pointer const & relative) {
- tokens_.insert(tokens_.end(), relative.tokens_.begin(), relative.tokens_.end());
- return *this;
- }
- Pointer operator/(Pointer const & relative) const { return Pointer(*this) /= relative; }
- Pointer & operator/=(parent_t) {
- tokens_.pop_back();
- return *this;
- }
- Pointer operator/(parent_t) const { return parent(); }
- Pointer & operator/=(std::string_view key) {
- tokens_.emplace_back(std::string(key));
- return *this;
- }
- Pointer operator/(std::string_view key) const { return Pointer(*this) /= key; }
- Pointer & operator/=(size_t index) {
- tokens_.emplace_back(index);
- return *this;
- }
- Pointer operator/(size_t index) const { return Pointer(*this) /= index; }
- friend std::ostream & operator<<(std::ostream & os, Pointer const & self) {
- for (auto const & elem : self.tokens_) {
- std::visit([&os](auto const & v) { os << '/' << v; }, elem);
- }
- return os;
- }
- auto operator<=>(Pointer const &) const = default;
- private:
- std::vector<std::variant<std::string, size_t>> tokens_{};
- };
- }
|