#pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace jvalidate::detail { template class ReferenceManager { public: using Keywords = std::unordered_map>; private: DocumentCache & external_; ReferenceCache references_; std::map roots_; ContextStack active_dynamic_anchors_; std::map> dynamic_anchors_; public: ReferenceManager(DocumentCache & external, A const & root, schema::Version version, Keywords const & keywords) : external_(external), roots_{{{}, root}} { prime(root, {}, version, keywords); } auto dynamic_scope(Reference const & ref) { URI const uri = ref.pointer().empty() ? ref.uri() : references_.relative_to_nearest_anchor(ref).uri(); return active_dynamic_anchors_.scope(uri, dynamic_anchors_[uri]); } std::optional load(Reference const & ref, ParserContext const & context) { if (auto it = roots_.find(ref.root()); it != roots_.end()) { return ref.pointer().walk(it->second); } std::optional external = external_.try_load(ref.uri()); if (not external) { return std::nullopt; } // TODO(samjaffe): Change Versions if needed... references_.emplace(ref.uri()); prime(*external, ref, context.version, context.factory.keywords(context.version)); // May have a sub-id that we map to if (auto it = roots_.find(ref.root()); it != roots_.end()) { return ref.pointer().walk(it->second); } // Will get called if the external schema does not declare a root document id? return ref.pointer().walk(*external); } Reference canonicalize(Reference const & ref, Reference const & parent, inout dynamic_reference) { URI const uri = [this, &ref, &parent]() { if (ref.uri().empty() && parent.uri().empty()) { return references_.actual_parent_uri(parent); } URI uri = ref.uri().empty() ? parent.uri() : ref.uri(); if (not uri.is_rootless()) { return uri; } URI base = references_.actual_parent_uri(parent); EXPECT_M(base.resource().rfind('/') != std::string::npos, "Unable to deduce root for relative uri " << uri << " (" << base << ")"); if (not uri.is_relative()) { return base.root() / uri; } if (auto br = base.resource(), ur = uri.resource(); br.ends_with(ur) && br[br.size() - ur.size() - 1] == '/') { return base; } return base.parent() / uri; }(); // TODO(samjaffe): Clean up this block too... URI const dyn_uri = ref.uri().empty() ? ref.uri() : uri; OnBlockExit scope; if (not ref.uri().empty() && dynamic_reference && active_dynamic_anchors_.contains(ref.anchor())) { scope = dynamic_scope(Reference(dyn_uri)); } if (std::optional dynamic = active_dynamic_anchors_.lookup(dyn_uri, ref.anchor())) { if (dynamic_reference) { return *dynamic; } dynamic_reference = ref.uri().empty() && ref.pointer().empty(); } if (active_dynamic_anchors_.empty()) { dynamic_reference = true; } // Relative URI, not in the HEREDOC (or we set an $id) if (ref.uri().empty() and ref.anchor().empty()) { return Reference(references_.relative_to_nearest_anchor(parent).root(), ref.pointer()); } return Reference(uri, ref.anchor(), ref.pointer()); } private: void prime(Adapter auto const & json, Reference where, schema::Version version, Keywords const & keywords) { if (json.type() != adapter::Type::Object) { return; } canonicalize(where, version, json); for (auto const & [key, value] : json.as_object()) { auto vit = keywords.find(key); if (vit == keywords.end()) { continue; } if (vit->second.contains(schema::Wraps::Array) && value.type() == adapter::Type::Array) { size_t index = 0; for (auto const & elem : value.as_array()) { prime(elem, where / key / index, version, keywords); ++index; } } else if (vit->second.contains(schema::Wraps::Object) && value.type() == adapter::Type::Object) { for (auto const & [prop, elem] : value.as_object()) { prime(elem, where / key / prop, version, keywords); } } else if (vit->second.contains(schema::Wraps::Schema)) { prime(value, where / key, version, keywords); } } } void canonicalize(Reference & where, schema::Version version, A const & json) { std::string const id = version <= schema::Version::Draft04 ? "id" : "$id"; auto const schema = json.as_object(); RootReference root = where.root(); if (schema.contains(id)) { root = RootReference(schema[id].as_string()); if (root.uri().empty()) { root = RootReference(where.uri(), root.anchor()); } else if (not root.uri().is_rootless() || where.uri().empty()) { // By definition - rootless URIs cannot be relative } else if (root.uri().is_relative()) { root = RootReference(where.uri().parent() / root.uri(), root.anchor()); } else { root = RootReference(where.uri().root() / root.uri(), root.anchor()); } roots_.emplace(root, json); where = references_.emplace(where, root); } // $anchor and its related keywords were introduced in Draft 2019-09 if (version < schema::Version::Draft2019_09) { return; } if (schema.contains("$anchor")) { root = RootReference(root.uri(), Anchor(schema["$anchor"].as_string())); roots_.emplace(root, json); where = references_.emplace(where, root); } if (version == schema::Version::Draft2019_09 && schema.contains("$recursiveAnchor") && schema["$recursiveAnchor"].as_boolean()) { Anchor anchor; root = RootReference(root.uri(), anchor); roots_.emplace(root, json); where = references_.emplace(where, root); if (Reference & dynamic = dynamic_anchors_[root.uri()][anchor]; dynamic == Reference() || where < dynamic) { dynamic = where; } } if (schema.contains("$dynamicAnchor") && version > schema::Version::Draft2019_09) { Anchor anchor(schema["$dynamicAnchor"].as_string()); root = RootReference(root.uri(), anchor); roots_.emplace(root, json); where = references_.emplace(where, root); if (Reference & dynamic = dynamic_anchors_[root.uri()][anchor]; dynamic == Reference() || where < dynamic) { dynamic = where; } } } }; }