|
|
@@ -24,7 +24,7 @@ private:
|
|
|
std::unique_ptr<adapter::Const const> default_{nullptr};
|
|
|
|
|
|
detail::Reference uri_;
|
|
|
- bool rejects_all_{false};
|
|
|
+ std::optional<std::string> rejects_all_;
|
|
|
std::optional<schema::Node const *> reference_{};
|
|
|
std::unordered_map<std::string, std::unique_ptr<constraint::Constraint>> constraints_{};
|
|
|
std::unordered_map<std::string, std::unique_ptr<constraint::Constraint>> post_constraints_{};
|
|
|
@@ -35,13 +35,14 @@ protected:
|
|
|
static Version schema_version(Adapter auto const & json, Version default_version);
|
|
|
|
|
|
public:
|
|
|
- Node(bool rejects_all = false) : rejects_all_(rejects_all) {}
|
|
|
+ Node() = default;
|
|
|
+ Node(std::string const & rejection_reason) : rejects_all_(rejection_reason) {}
|
|
|
template <Adapter A> Node(detail::ParserContext<A> context);
|
|
|
|
|
|
bool is_pure_reference() const {
|
|
|
return reference_ && constraints_.empty() && post_constraints_.empty() && not default_;
|
|
|
}
|
|
|
- bool rejects_all() const { return rejects_all_; }
|
|
|
+ std::optional<std::string> const & rejects_all() const { return rejects_all_; }
|
|
|
std::optional<schema::Node const *> reference_schema() const { return reference_; }
|
|
|
|
|
|
bool requires_result_context() const { return not post_constraints_.empty(); }
|
|
|
@@ -57,16 +58,19 @@ private:
|
|
|
|
|
|
inline Version Node::schema_version(std::string_view url) {
|
|
|
static std::map<std::string_view, Version> const g_schema_ids{
|
|
|
- {"http://json-schema.org/draft-04/schema", Version::Draft04},
|
|
|
- {"http://json-schema.org/draft-06/schema", Version::Draft06},
|
|
|
- {"http://json-schema.org/draft-07/schema", Version::Draft07},
|
|
|
- {"http://json-schema.org/draft/2019-09/schema", Version::Draft2019_09},
|
|
|
- {"http://json-schema.org/draft/2020-12/schema", Version::Draft2020_12},
|
|
|
+ {"json-schema.org/draft-04/schema", Version::Draft04},
|
|
|
+ {"json-schema.org/draft-06/schema", Version::Draft06},
|
|
|
+ {"json-schema.org/draft-07/schema", Version::Draft07},
|
|
|
+ {"json-schema.org/draft/2019-09/schema", Version::Draft2019_09},
|
|
|
+ {"json-schema.org/draft/2020-12/schema", Version::Draft2020_12},
|
|
|
};
|
|
|
|
|
|
if (url.ends_with('#')) {
|
|
|
url.remove_suffix(1);
|
|
|
}
|
|
|
+ if (url.starts_with("http://") || url.starts_with("https://")) {
|
|
|
+ url.remove_prefix(url.find(':') + 3);
|
|
|
+ }
|
|
|
|
|
|
auto it = g_schema_ids.find(url);
|
|
|
EXPECT_T(it != g_schema_ids.end(), std::invalid_argument, url);
|
|
|
@@ -110,8 +114,8 @@ private:
|
|
|
};
|
|
|
|
|
|
private:
|
|
|
- schema::Node accept_{false};
|
|
|
- schema::Node reject_{true};
|
|
|
+ schema::Node accept_;
|
|
|
+ schema::Node reject_{"always false"};
|
|
|
|
|
|
// A map of (URI, Anchor) => (URI, Pointer), binding an anchor reference
|
|
|
// to it's fully resolved path.
|
|
|
@@ -164,8 +168,14 @@ public:
|
|
|
template <Adapter A>
|
|
|
Schema(A const & json, schema::Version version, DocumentCache<A> & external,
|
|
|
ConstraintFactory<A> const & factory = {}) {
|
|
|
- detail::ParserContext<A> root{*this, json, version, factory, external};
|
|
|
// Prevent unintialized data caches
|
|
|
+ if (version >= schema::Version::Draft06 && json.type() == adapter::Type::Boolean) {
|
|
|
+ schema::Node::operator=(std::move(json.as_boolean() ? accept_ : reject_));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ external.cache_reference(URI(), json);
|
|
|
+ detail::ParserContext<A> root{*this, json, version, factory, external};
|
|
|
schema::Node::operator=(root);
|
|
|
}
|
|
|
|
|
|
@@ -266,33 +276,33 @@ private:
|
|
|
template <Adapter A>
|
|
|
schema::Node const * resolve(detail::Reference ref, detail::ParserContext<A> const & context) {
|
|
|
// Special case if the root-level document does not have an $id property
|
|
|
- if (ref == detail::Reference() && ref.anchor() == context.where.anchor()) {
|
|
|
+ if (ref == detail::Reference() && context.where.uri().empty()) {
|
|
|
return this;
|
|
|
}
|
|
|
+ if (ref.uri().empty()) {
|
|
|
+ ref = detail::Reference(context.where.uri(), ref.anchor(), ref.pointer());
|
|
|
+ }
|
|
|
|
|
|
if (std::optional cached = from_cache(ref)) {
|
|
|
return *cached;
|
|
|
}
|
|
|
|
|
|
// SPECIAL RULE: Resolve this URI into the context of the calling URI
|
|
|
- if (ref.uri().scheme().empty()) {
|
|
|
+ if (not ref.uri().empty() && ref.uri().scheme().empty()) {
|
|
|
URI const & relative_to = context.where.uri();
|
|
|
EXPECT_M(relative_to.resource().rfind('/') != std::string::npos,
|
|
|
- "Relative URIs require that the current context has a resolved URI");
|
|
|
+ "Unable to deduce root for relative uri " << ref.uri() << " (" << relative_to
|
|
|
+ << ")");
|
|
|
ref = detail::Reference(relative_to.parent() / ref.uri(), ref.anchor(), ref.pointer());
|
|
|
}
|
|
|
|
|
|
- EXPECT_M(context.external, "Unable to resolve external reference(s) without a URIResolver");
|
|
|
-
|
|
|
- std::optional schema = context.external.try_load(ref.uri());
|
|
|
- EXPECT_M(schema.has_value(), "URIResolver could not resolve " << ref.uri());
|
|
|
-
|
|
|
- (void)fetch_schema(context.rebind(*schema, ref.uri()));
|
|
|
- std::optional referenced_node = from_cache(ref);
|
|
|
- EXPECT_M(referenced_node.has_value(),
|
|
|
- "Could not locate reference '" << ref << "' within external schema.");
|
|
|
+ std::optional schema = context.external.try_load(ref);
|
|
|
+ if (not schema.has_value()) {
|
|
|
+ std::string error = "URIResolver could not resolve " + std::string(ref.uri());
|
|
|
+ return alias(context.where, &cache_.try_emplace(context.where, error).first->second);
|
|
|
+ }
|
|
|
|
|
|
- return *referenced_node;
|
|
|
+ return fetch_schema(context.rebind(*schema, ref));
|
|
|
}
|
|
|
|
|
|
schema::Node const * resolve_dynamic(detail::Anchor const & ref) {
|
|
|
@@ -302,30 +312,37 @@ private:
|
|
|
}
|
|
|
|
|
|
template <Adapter A> schema::Node const * fetch_schema(detail::ParserContext<A> const & context) {
|
|
|
+ // TODO(samjaffe): No longer promises uniqueness - instead track unique URI's
|
|
|
+ if (std::optional cached = from_cache(context.where)) {
|
|
|
+ return *cached;
|
|
|
+ }
|
|
|
+
|
|
|
adapter::Type const type = context.schema.type();
|
|
|
if (type == adapter::Type::Boolean && context.version >= schema::Version::Draft06) {
|
|
|
return alias(context.where, context.schema.as_boolean() ? &accept_ : &reject_);
|
|
|
}
|
|
|
|
|
|
- EXPECT(type == adapter::Type::Object);
|
|
|
+ EXPECT_M(type == adapter::Type::Object, "invalid schema at " << context.where);
|
|
|
if (context.schema.object_size() == 0) {
|
|
|
return alias(context.where, &accept_);
|
|
|
}
|
|
|
|
|
|
- auto [it, created] = cache_.try_emplace(context.where, context);
|
|
|
- EXPECT_M(created, "more than one schema found with uri " << context.where);
|
|
|
+ auto [it, created] = cache_.try_emplace(context.where);
|
|
|
+ EXPECT_M(created, "creating duplicate schema at... " << context.where);
|
|
|
+
|
|
|
+ // Do this here first in order to protect from infinite loops
|
|
|
+ alias(context.where, &it->second);
|
|
|
+ it->second = schema::Node(context);
|
|
|
+ if (not it->second.is_pure_reference()) {
|
|
|
+ return &it->second;
|
|
|
+ }
|
|
|
|
|
|
- schema::Node const * node = &it->second;
|
|
|
// Special Case - if the only is the reference constraint, then we don't need
|
|
|
// to store it uniquely. Draft2019_09 supports directly extending a $ref schema
|
|
|
// in the same schema, instead of requiring an allOf clause.
|
|
|
- if (node->is_pure_reference()) {
|
|
|
- node = *node->reference_schema();
|
|
|
- cache_.erase(it);
|
|
|
- return alias(context.where, node);
|
|
|
- }
|
|
|
-
|
|
|
- return alias(context.where, node);
|
|
|
+ schema::Node const * node = *it->second.reference_schema();
|
|
|
+ cache_.erase(it);
|
|
|
+ return alias_cache_[context.where] = node;
|
|
|
}
|
|
|
};
|
|
|
}
|
|
|
@@ -336,7 +353,11 @@ template <Adapter A> schema::Node const * ParserContext<A>::node() const {
|
|
|
}
|
|
|
|
|
|
template <Adapter A> schema::Node const * ParserContext<A>::always() const {
|
|
|
- return schema.as_boolean() ? &root.accept_ : &root.reject_;
|
|
|
+ return fixed_schema(schema.as_boolean());
|
|
|
+}
|
|
|
+
|
|
|
+template <Adapter A> schema::Node const * ParserContext<A>::fixed_schema(bool accept) const {
|
|
|
+ return accept ? &root.accept_ : &root.reject_;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -416,7 +437,15 @@ template <Adapter A> Node::Node(detail::ParserContext<A> context) {
|
|
|
}
|
|
|
|
|
|
if (schema.contains("$id")) {
|
|
|
- context.root.alias(detail::Reference(schema["$id"].as_string(), false), this);
|
|
|
+ detail::Reference id(schema["$id"].as_string(), false);
|
|
|
+ if (id.uri().scheme().empty() and not context.where.uri().empty()) {
|
|
|
+ id = detail::Reference(context.where.uri().parent() / id.uri(), {}, id.pointer());
|
|
|
+ }
|
|
|
+
|
|
|
+ if (id != context.where) {
|
|
|
+ context.external.cache_reference(id.uri(), context.schema);
|
|
|
+ context.root.alias(context.where = id, this);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
[[maybe_unused]] auto _ = resolve_anchor(context);
|