#pragma once #include #include #include #include #include // IWYU pragma: keep #include #include #include // IWYU pragma: keep #include #include #include #include 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 {}; // NOLINT(readability-identifier-naming) constexpr parent_t parent; // NOLINT(readability-identifier-naming) class Pointer { private: class iterator; // NOLINT(readability-identifier-naming) public: Pointer() = default; /** * @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) */ explicit(false) Pointer(std::string_view path); static std::string deserialize(std::string_view view) { std::string in(view); 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] == '%') { std::string_view const enc = std::string_view(in).substr(i + 1, 2); // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) in.replace(i, 3, 1, from_str(enc, 16)); continue; } 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, '/'); } else { JVALIDATE_THROW(std::runtime_error, "Illegal ~ code"); } } return in; } /** * @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; /** * @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; bool empty() const { return value_.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 value_.starts_with(other.value_); } /** * @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)); Pointer rval; rval.value_ = value_.substr(other.value_.size()); return rval; } Pointer parent(size_t levels = 1) const; Pointer & operator/=(Pointer const & relative) { value_ += relative.value_; return *this; } Pointer operator/(Pointer const & relative) const { return Pointer(*this) /= relative; } Pointer & operator/=(parent_t); Pointer operator/(parent_t) const { return parent(); } Pointer & operator/=(std::string_view key) { value_ += '/'; value_ += std::string(key); return *this; } Pointer operator/(std::string_view key) const { return Pointer(*this) /= key; } Pointer & operator/=(size_t index) { value_ += '/'; value_ += std::to_string(index); return *this; } Pointer operator/(size_t index) const { return Pointer(*this) /= index; } iterator begin() const; iterator end() const; explicit operator std::string const &() const { return value_; } friend std::ostream & operator<<(std::ostream & os, Pointer const & self) { return os << self.value_; } auto operator<=>(Pointer const &) const = default; private: std::string value_; }; class Pointer::iterator { public: using value_type = std::string_view; using reference = std::string_view; using pointer = void; using difference_type = std::ptrdiff_t; using iterator_category = std::bidirectional_iterator_tag; explicit iterator(std::string_view view, size_t position = std::string_view::npos) : view_(view) { if (position < view.size()) { curr_ = position; next_ = view_.find('/', curr_ + 1); } } std::string_view operator*() const { if (next_ == std::string_view::npos) { return view_.substr(curr_ + 1); } return view_.substr(curr_ + 1, next_ - curr_ - 1); } iterator & operator++() { curr_ = next_; if (curr_ != std::string_view::npos) { next_ = view_.find('/', curr_ + 1); } return *this; } iterator & operator--() { next_ = curr_; if (next_ == std::string_view::npos) { curr_ = view_.rfind('/'); } else if (next_ != 0) { curr_ = view_.rfind('/', next_ - 1); } return *this; } friend bool operator==(iterator const & lhs, iterator const & rhs) = default; private: friend class Pointer; std::string_view view_; size_t curr_ = std::string_view::npos; size_t next_ = std::string_view::npos; }; inline auto Pointer::begin() const -> iterator { return iterator(value_, 0); } inline auto Pointer::end() const -> iterator { return iterator(value_); } inline std::string Pointer::back() const { return std::string(*--end()); } inline Pointer Pointer::parent(size_t levels) const { if (levels == 0) { return *this; } iterator it = end(); std::advance(it, -levels); if (it.curr_ > value_.size()) { return {}; } Pointer rval = *this; rval.value_.resize(it.curr_); return rval; } inline Pointer & Pointer::operator/=(parent_t) { iterator it = --end(); value_.resize(it.curr_ > value_.size() ? 0 : it.curr_); return *this; } inline Pointer::Pointer(std::string_view path) : value_(path) { if (path.empty()) { return; } // JSON-Pointers are required to start with a '/'. EXPECT_M(path.starts_with('/'), "Missing leading '/' in JSON Pointer: " << path); // 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 (std::string_view token : *this) { deserialize(token); } } inline auto Pointer::walk(Adapter auto document) const { for (std::string_view token : *this) { if (document.type() == adapter::Type::Array) { document = document[from_str(token)]; continue; } document = document[deserialize(token)]; } return document; } } template <> struct std::hash { auto operator()(jvalidate::detail::Pointer const & value) const { return std::hash()(static_cast(value)); } };