#pragma once #include #include #include #include #include #include #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 {}; constexpr parent_t parent; class Pointer { public: Pointer() = default; Pointer(std::vector> 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), 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(from_str(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], '\0'}; in.replace(i, 3, 1, from_str(enc, 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 '/'. EXPECT_M(path.starts_with('/'), "Missing leading '/' in JSON Pointer: " << path); 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> tokens_{}; }; }