|
|
@@ -6,7 +6,9 @@
|
|
|
|
|
|
#include <jvalidate/adapter.h>
|
|
|
#include <jvalidate/constraint.h>
|
|
|
+#include <jvalidate/detail/anchor.h>
|
|
|
#include <jvalidate/detail/expect.h>
|
|
|
+#include <jvalidate/detail/on_block_exit.h>
|
|
|
#include <jvalidate/detail/pointer.h>
|
|
|
#include <jvalidate/detail/reference.h>
|
|
|
#include <jvalidate/enum.h>
|
|
|
@@ -45,6 +47,10 @@ public:
|
|
|
auto const & post_constraints() const { return constraints_; }
|
|
|
|
|
|
adapter::Const const * default_value() const { return default_.get(); }
|
|
|
+
|
|
|
+private:
|
|
|
+ template <Adapter A> detail::OnBlockExit resolve_anchor(ParserContext<A> & context);
|
|
|
+ template <Adapter A> bool resolve_reference(ParserContext<A> const & context);
|
|
|
};
|
|
|
|
|
|
inline Version Node::schema_version(std::string_view url) {
|
|
|
@@ -92,12 +98,21 @@ class Schema : public schema::Node {
|
|
|
private:
|
|
|
friend class schema::Node;
|
|
|
template <Adapter A> friend class ParserContext;
|
|
|
+ struct DynamicRef {
|
|
|
+ template <typename F>
|
|
|
+ DynamicRef(detail::Reference const & where, F const & reconstruct)
|
|
|
+ : where(where), reconstruct(reconstruct) {}
|
|
|
+
|
|
|
+ detail::Reference where;
|
|
|
+ std::function<schema::Node const *()> reconstruct;
|
|
|
+ };
|
|
|
|
|
|
private:
|
|
|
schema::Node accept_{true};
|
|
|
schema::Node reject_{false};
|
|
|
|
|
|
- std::map<std::string, detail::Reference> anchors_;
|
|
|
+ std::map<detail::Reference, detail::Reference> anchors_;
|
|
|
+ std::map<detail::Anchor, DynamicRef> dynamic_anchors_;
|
|
|
std::map<detail::Reference, schema::Node> cache_;
|
|
|
|
|
|
std::map<detail::Reference, schema::Node const *> alias_cache_;
|
|
|
@@ -117,7 +132,23 @@ public:
|
|
|
: Schema(adapter::AdapterFor<JSON const>(json), std::forward<Args>(args)...) {}
|
|
|
|
|
|
private:
|
|
|
- void anchor(std::string anchor, detail::Reference const & from) {}
|
|
|
+ void anchor(detail::Reference const & anchor, detail::Reference const & from) {
|
|
|
+ EXPECT_M(anchors_.try_emplace(anchor.root(), from).second,
|
|
|
+ "more than one anchor found for uri " << anchor);
|
|
|
+ }
|
|
|
+
|
|
|
+ template <Adapter A>
|
|
|
+ void dynamic_anchor(detail::Anchor const & anchor, ParserContext<A> const & context) {
|
|
|
+ dynamic_anchors_.try_emplace(anchor, context.where,
|
|
|
+ [this, context]() { return fetch_schema(context); });
|
|
|
+ }
|
|
|
+
|
|
|
+ void remove_dynamic_anchor(detail::Anchor const & anchor, detail::Reference const & where) {
|
|
|
+ if (auto it = dynamic_anchors_.find(anchor);
|
|
|
+ it != dynamic_anchors_.end() && it->second.where == where) {
|
|
|
+ dynamic_anchors_.erase(it);
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
schema::Node const * alias(detail::Reference const & where, schema::Node const * schema) {
|
|
|
EXPECT_M(alias_cache_.try_emplace(where, schema).second,
|
|
|
@@ -133,10 +164,9 @@ private:
|
|
|
return this;
|
|
|
}
|
|
|
|
|
|
- if (auto it = anchors_.find(ref.anchor()); it != anchors_.end()) {
|
|
|
+ if (auto it = anchors_.find(ref.root()); it != anchors_.end()) {
|
|
|
ref = it->second / ref.pointer();
|
|
|
}
|
|
|
- EXPECT_M(ref.anchor().back() == '#', "Unmatched anchor: " << ref.anchor());
|
|
|
|
|
|
if (auto it = alias_cache_.find(ref); it != alias_cache_.end()) {
|
|
|
return it->second;
|
|
|
@@ -144,6 +174,12 @@ private:
|
|
|
throw;
|
|
|
}
|
|
|
|
|
|
+ schema::Node const * resolve_dynamic(detail::Anchor const & ref) {
|
|
|
+ auto it = dynamic_anchors_.find(ref);
|
|
|
+ EXPECT_M(it != dynamic_anchors_.end(), "Unmatched $dynamicRef '" << ref << "'");
|
|
|
+ return it->second.reconstruct();
|
|
|
+ }
|
|
|
+
|
|
|
template <Adapter A> schema::Node const * fetch_schema(ParserContext<A> const & context) {
|
|
|
adapter::Type const type = context.schema.type();
|
|
|
if (type == adapter::Type::Boolean && context.version >= schema::Version::Draft06) {
|
|
|
@@ -171,10 +207,6 @@ private:
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-template <Adapter A> schema::Node const * ParserContext<A>::resolve(std::string_view uri) const {
|
|
|
- return root.resolve<A>(detail::Reference(uri), where, version);
|
|
|
-}
|
|
|
-
|
|
|
template <Adapter A> schema::Node const * ParserContext<A>::node() const {
|
|
|
return root.fetch_schema(*this);
|
|
|
}
|
|
|
@@ -185,6 +217,67 @@ template <Adapter A> schema::Node const * ParserContext<A>::always() const {
|
|
|
}
|
|
|
|
|
|
namespace jvalidate::schema {
|
|
|
+template <Adapter A> detail::OnBlockExit Node::resolve_anchor(ParserContext<A> & context) {
|
|
|
+ auto const schema = context.schema.as_object();
|
|
|
+
|
|
|
+ // TODO(samjaffe): $recursiveAnchor, $dynamicAnchor, $recursiveRef, $dynamicRef
|
|
|
+ if (schema.contains("$anchor")) {
|
|
|
+ // Create an anchor mapping using the current document and the anchor
|
|
|
+ // string. There's no need for special validation/chaining here, because
|
|
|
+ // {@see Schema::resolve} will turn all $ref/$dynamicRef anchors into
|
|
|
+ // their fully-qualified path.
|
|
|
+ detail::Anchor anchor(schema["$anchor"].as_string());
|
|
|
+ context.root.anchor(detail::Reference(context.where.uri(), anchor), context.where);
|
|
|
+ return nullptr;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (context.version == Version::Draft2019_09 && schema.contains("$recursiveAnchor")) {
|
|
|
+ EXPECT_M(schema["$recursiveAnchor"].as_boolean(), "$recursiveAnchor MUST be 'true'");
|
|
|
+
|
|
|
+ context.root.dynamic_anchor(detail::Anchor(), context);
|
|
|
+ return [&context]() { context.root.remove_dynamic_anchor(detail::Anchor(), context.where); };
|
|
|
+ }
|
|
|
+
|
|
|
+ if (context.version > Version::Draft2019_09 && schema.contains("$dynamicAnchor")) {
|
|
|
+ detail::Anchor anchor(schema["$dynamicAnchor"].as_string());
|
|
|
+
|
|
|
+ context.root.dynamic_anchor(anchor, context);
|
|
|
+ return [&context, anchor]() { context.root.remove_dynamic_anchor(anchor, context.where); };
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+template <Adapter A> bool Node::resolve_reference(ParserContext<A> const & context) {
|
|
|
+ auto const schema = context.schema.as_object();
|
|
|
+
|
|
|
+ if (schema.contains("$ref")) {
|
|
|
+ detail::Reference ref(schema["$ref"].as_string());
|
|
|
+
|
|
|
+ reference_ = context.root.template resolve<A>(ref, context.where, context.version);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (context.version < Version::Draft2019_09) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (context.version == Version::Draft2019_09 && schema.contains("$recursiveRef")) {
|
|
|
+ detail::Reference ref(schema["$recursiveRef"].as_string());
|
|
|
+ EXPECT_M(ref == detail::Reference(), "Only the root schema is permitted as a $recursiveRef");
|
|
|
+
|
|
|
+ reference_ = context.root.resolve_dynamic(detail::Anchor());
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (context.version > Version::Draft2019_09 && schema.contains("$dynamicRef")) {
|
|
|
+ detail::Reference ref(schema["$dynamicRef"].as_string());
|
|
|
+
|
|
|
+ reference_ = context.root.resolve_dynamic(ref.anchor());
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+}
|
|
|
+
|
|
|
template <Adapter A> Node::Node(ParserContext<A> context) {
|
|
|
EXPECT(context.schema.type() == adapter::Type::Object);
|
|
|
|
|
|
@@ -201,21 +294,8 @@ template <Adapter A> Node::Node(ParserContext<A> context) {
|
|
|
context.root.alias(detail::Reference(schema["$id"].as_string(), false), this);
|
|
|
}
|
|
|
|
|
|
- // TODO(samjaffe): $recursiveAnchor, $dynamicAnchor, $recursiveRef, $dynamicRef
|
|
|
- if (schema.contains("$anchor")) {
|
|
|
- // Create an anchor mapping using the current document and the anchor
|
|
|
- // string. There's no need for special validation/chaining here, because
|
|
|
- // {@see Schema::resolve} will turn all $ref/$dynamicRef anchors into
|
|
|
- // their fully-qualified path.
|
|
|
- context.root.anchor(context.where.anchor() + schema["$anchor"].as_string(), context.where);
|
|
|
- }
|
|
|
-
|
|
|
- bool has_reference;
|
|
|
- if ((has_reference = schema.contains("$ref"))) {
|
|
|
- auto ref = schema["$ref"];
|
|
|
- EXPECT(ref.type() == adapter::Type::String);
|
|
|
- reference_ = context.resolve(ref.as_string());
|
|
|
- }
|
|
|
+ [[maybe_unused]] auto _ = resolve_anchor(context);
|
|
|
+ bool const has_reference = resolve_reference(context);
|
|
|
|
|
|
if (schema.contains("default")) {
|
|
|
default_ = schema["default"].freeze();
|