schema.h 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. #pragma once
  2. #include <memory>
  3. #include <type_traits>
  4. #include <unordered_map>
  5. #include <vector>
  6. #include <jvalidate/adapter.h>
  7. #include <jvalidate/constraint.h>
  8. #include <jvalidate/detail/anchor.h>
  9. #include <jvalidate/detail/expect.h>
  10. #include <jvalidate/detail/on_block_exit.h>
  11. #include <jvalidate/detail/parser_context.h>
  12. #include <jvalidate/detail/pointer.h>
  13. #include <jvalidate/detail/reference.h>
  14. #include <jvalidate/document_cache.h>
  15. #include <jvalidate/enum.h>
  16. #include <jvalidate/forward.h>
  17. namespace jvalidate::schema {
  18. class Node {
  19. private:
  20. std::string description_;
  21. std::unique_ptr<adapter::Const const> default_{nullptr};
  22. detail::Reference uri_;
  23. bool rejects_all_{false};
  24. std::optional<schema::Node const *> reference_{};
  25. std::unordered_map<std::string, std::unique_ptr<constraint::Constraint>> constraints_{};
  26. std::unordered_map<std::string, std::unique_ptr<constraint::Constraint>> post_constraints_{};
  27. protected:
  28. static Version schema_version(std::string_view url);
  29. static Version schema_version(Adapter auto const & json);
  30. static Version schema_version(Adapter auto const & json, Version default_version);
  31. public:
  32. Node(bool rejects_all = false) : rejects_all_(rejects_all) {}
  33. template <Adapter A> Node(detail::ParserContext<A> context);
  34. bool is_pure_reference() const {
  35. return reference_ && constraints_.empty() && post_constraints_.empty() && not default_;
  36. }
  37. bool rejects_all() const { return rejects_all_; }
  38. std::optional<schema::Node const *> reference_schema() const { return reference_; }
  39. bool requires_result_context() const { return not post_constraints_.empty(); }
  40. auto const & constraints() const { return constraints_; }
  41. auto const & post_constraints() const { return constraints_; }
  42. adapter::Const const * default_value() const { return default_.get(); }
  43. private:
  44. template <Adapter A> detail::OnBlockExit resolve_anchor(detail::ParserContext<A> & context);
  45. template <Adapter A> bool resolve_reference(detail::ParserContext<A> const & context);
  46. };
  47. inline Version Node::schema_version(std::string_view url) {
  48. static std::map<std::string_view, Version> const g_schema_ids{
  49. {"http://json-schema.org/draft-04/schema", Version::Draft04},
  50. {"http://json-schema.org/draft-06/schema", Version::Draft06},
  51. {"http://json-schema.org/draft-07/schema", Version::Draft07},
  52. {"http://json-schema.org/draft/2019-09/schema", Version::Draft2019_09},
  53. {"http://json-schema.org/draft/2020-12/schema", Version::Draft2020_12},
  54. };
  55. if (url.ends_with('#')) {
  56. url.remove_suffix(1);
  57. }
  58. auto it = g_schema_ids.find(url);
  59. EXPECT_T(it != g_schema_ids.end(), std::invalid_argument, url);
  60. return it->second;
  61. }
  62. Version Node::schema_version(Adapter auto const & json) {
  63. EXPECT(json.type() == adapter::Type::Object);
  64. EXPECT(json.as_object().contains("$schema"));
  65. auto const & schema = json.as_object()["$schema"];
  66. EXPECT(schema.type() == adapter::Type::String);
  67. return schema_version(schema.as_string());
  68. }
  69. Version Node::schema_version(Adapter auto const & json, Version default_version) {
  70. RETURN_UNLESS(json.type() == adapter::Type::Object, default_version);
  71. RETURN_UNLESS(json.as_object().contains("$schema"), default_version);
  72. auto const & schema = json.as_object()["$schema"];
  73. RETURN_UNLESS(schema.type() == adapter::Type::String, default_version);
  74. return schema_version(schema.as_string());
  75. }
  76. }
  77. namespace jvalidate {
  78. class Schema : public schema::Node {
  79. private:
  80. friend class schema::Node;
  81. template <Adapter A> friend class detail::ParserContext;
  82. struct DynamicRef {
  83. template <typename F>
  84. DynamicRef(detail::Reference const & where, F const & reconstruct)
  85. : where(where), reconstruct(reconstruct) {}
  86. detail::Reference where;
  87. std::function<schema::Node const *()> reconstruct;
  88. };
  89. private:
  90. schema::Node accept_{true};
  91. schema::Node reject_{false};
  92. std::map<detail::Reference, detail::Reference> anchors_;
  93. std::map<detail::Anchor, DynamicRef> dynamic_anchors_;
  94. std::map<detail::Reference, schema::Node> cache_;
  95. std::map<detail::Reference, schema::Node const *> alias_cache_;
  96. public:
  97. /**
  98. * @brief Construct a new schema. All other constructors of this type may be
  99. * considered syntactic sugar for this constructor.
  100. *
  101. * As such, the true signature of this class's contructor is:
  102. *
  103. * Schema(Adapter | JSON [, schema::Version]
  104. * [, URIResolver & | URIResolver &&]
  105. * [, ConstraintFactory<A> const &])
  106. *
  107. * as long as the order of arguments is preserved - the constructor will work
  108. * no matter which arguments are ignored. The only required argument being
  109. * the JSON object/Adapter.
  110. *
  111. * @param json An adapter to a json object
  112. *
  113. * @param version The json-schema draft version that all schemas will prefer
  114. *
  115. * @param external An object capable of resolving URIs, and turning them into
  116. * Adapter objects. Holds a cache and so must be mutable.
  117. *
  118. * @param factory An object that manuafactures constraints - allows the user
  119. * to provide custom extensions or even modify the behavior of existing
  120. * keywords by overridding the virtual accessor function(s).
  121. */
  122. template <Adapter A>
  123. Schema(A const & json, schema::Version version, DocumentCache<A> & external,
  124. ConstraintFactory<A> const & factory = {})
  125. : schema::Node(detail::ParserContext<A>{*this, json, version, factory, external}) {}
  126. /**
  127. * @param json An adapter to a json schema
  128. *
  129. * @param version The json-schema draft version that all schemas will prefer
  130. *
  131. * @param external An object capable of resolving URIs, and turning them into
  132. * Adapter objects. Holds a cache and so must be mutable. If this constructor
  133. * is called, then it means that the cache is a one-off object, and will not
  134. * be reused.
  135. */
  136. template <Adapter A, typename... Args>
  137. Schema(A const & json, schema::Version version, DocumentCache<A> && external, Args &&... args)
  138. : Schema(json, version, external, std::forward<Args>(args)...) {}
  139. /**
  140. * @param json An adapter to a json schema
  141. *
  142. * @param version The json-schema draft version that all schemas will prefer
  143. *
  144. * @param resolve A function capable of resolving URIs, and storing the
  145. * contents in a provided concrete JSON object.
  146. */
  147. template <Adapter A, typename... Args>
  148. Schema(A const & json, schema::Version version, URIResolver<A> resolve, Args &&... args)
  149. : Schema(json, version, DocumentCache<A>(resolve), std::forward<Args>(args)...) {}
  150. /**
  151. * @param json An adapter to a json schema
  152. *
  153. * @param version The json-schema draft version that all schemas will prefer
  154. */
  155. template <Adapter A, Not<DocumentCache<A>>... Args>
  156. Schema(A const & json, schema::Version version, Args &&... args)
  157. : Schema(json, version, DocumentCache<A>(), std::forward<Args>(args)...) {}
  158. /**
  159. * @param json An adapter to a json schema
  160. */
  161. template <Adapter A, Not<schema::Version>... Args>
  162. explicit Schema(A const & json, Args &&... args)
  163. : Schema(json, schema_version(json), std::forward<Args>(args)...) {}
  164. /**
  165. * @param json Any non-adapter (JSON) object. Will be immedately converted
  166. * into an Adapter object to allow us to walk through it w/o specialization.
  167. */
  168. template <typename JSON, typename... Args>
  169. explicit Schema(JSON const & json, Args &&... args)
  170. : Schema(adapter::AdapterFor<JSON const>(json), std::forward<Args>(args)...) {}
  171. private:
  172. void anchor(detail::Reference const & anchor, detail::Reference const & from) {
  173. EXPECT_M(anchors_.try_emplace(anchor.root(), from).second,
  174. "more than one anchor found for uri " << anchor);
  175. }
  176. template <Adapter A>
  177. void dynamic_anchor(detail::Anchor const & anchor, detail::ParserContext<A> const & context) {
  178. dynamic_anchors_.try_emplace(anchor, context.where,
  179. [this, context]() { return fetch_schema(context); });
  180. }
  181. void remove_dynamic_anchor(detail::Anchor const & anchor, detail::Reference const & where) {
  182. if (auto it = dynamic_anchors_.find(anchor);
  183. it != dynamic_anchors_.end() && it->second.where == where) {
  184. dynamic_anchors_.erase(it);
  185. }
  186. }
  187. schema::Node const * alias(detail::Reference const & where, schema::Node const * schema) {
  188. EXPECT_M(alias_cache_.try_emplace(where, schema).second,
  189. "more than one schema found with uri " << where);
  190. return schema;
  191. }
  192. std::optional<schema::Node const *> from_cache(detail::Reference ref) {
  193. if (auto it = anchors_.find(ref.root()); it != anchors_.end()) {
  194. ref = it->second / ref.pointer();
  195. }
  196. if (auto it = alias_cache_.find(ref); it != alias_cache_.end()) {
  197. return it->second;
  198. }
  199. return std::nullopt;
  200. }
  201. template <Adapter A>
  202. schema::Node const * resolve(detail::Reference ref, detail::ParserContext<A> const & context) {
  203. // Special case if the root-level document does not have an $id property
  204. if (ref == detail::Reference() && ref.anchor() == context.where.anchor()) {
  205. return this;
  206. }
  207. if (std::optional cached = from_cache(ref)) {
  208. return *cached;
  209. }
  210. // SPECIAL RULE: Resolve this URI into the context of the calling URI
  211. if (ref.uri().scheme().empty()) {
  212. URI const & relative_to = context.where.uri();
  213. EXPECT_M(relative_to.resource().rfind('/') != std::string::npos,
  214. "Relative URIs require that the current context has a resolved URI");
  215. ref = detail::Reference(relative_to.parent() / ref.uri(), ref.anchor(), ref.pointer());
  216. }
  217. EXPECT_M(context.external, "Unable to resolve external reference(s) without a URIResolver");
  218. std::optional schema = context.external.try_load(ref.uri());
  219. EXPECT_M(schema.has_value(), "URIResolver could not resolve " << ref.uri());
  220. (void)fetch_schema(context.rebind(*schema, ref.uri()));
  221. std::optional referenced_node = from_cache(ref);
  222. EXPECT_M(referenced_node.has_value(),
  223. "Could not locate reference '" << ref << "' within external schema.");
  224. return *referenced_node;
  225. }
  226. schema::Node const * resolve_dynamic(detail::Anchor const & ref) {
  227. auto it = dynamic_anchors_.find(ref);
  228. EXPECT_M(it != dynamic_anchors_.end(), "Unmatched $dynamicRef '" << ref << "'");
  229. return it->second.reconstruct();
  230. }
  231. template <Adapter A> schema::Node const * fetch_schema(detail::ParserContext<A> const & context) {
  232. adapter::Type const type = context.schema.type();
  233. if (type == adapter::Type::Boolean && context.version >= schema::Version::Draft06) {
  234. return alias(context.where, context.schema.as_boolean() ? &accept_ : &reject_);
  235. }
  236. EXPECT(type == adapter::Type::Object);
  237. if (context.schema.object_size() == 0) {
  238. return alias(context.where, &accept_);
  239. }
  240. auto [it, created] = cache_.try_emplace(context.where, context);
  241. EXPECT_M(created, "more than one schema found with uri " << context.where);
  242. schema::Node const * node = &it->second;
  243. // Special Case - if the only is the reference constraint, then we don't need
  244. // to store it uniquely. Draft2019_09 supports directly extending a $ref schema
  245. // in the same schema, instead of requiring an allOf clause.
  246. if (node->is_pure_reference()) {
  247. node = *node->reference_schema();
  248. cache_.erase(it);
  249. return alias(context.where, node);
  250. }
  251. return alias(context.where, node);
  252. }
  253. };
  254. }
  255. namespace jvalidate::detail {
  256. template <Adapter A> schema::Node const * ParserContext<A>::node() const {
  257. return root.fetch_schema(*this);
  258. }
  259. template <Adapter A> schema::Node const * ParserContext<A>::always() const {
  260. return schema.as_boolean() ? &root.accept_ : &root.reject_;
  261. }
  262. }
  263. namespace jvalidate::schema {
  264. template <Adapter A> detail::OnBlockExit Node::resolve_anchor(detail::ParserContext<A> & context) {
  265. auto const schema = context.schema.as_object();
  266. if (schema.contains("$anchor")) {
  267. // Create an anchor mapping using the current document and the anchor
  268. // string. There's no need for special validation/chaining here, because
  269. // {@see Schema::resolve} will turn all $ref/$dynamicRef anchors into
  270. // their fully-qualified path.
  271. detail::Anchor anchor(schema["$anchor"].as_string());
  272. context.root.anchor(detail::Reference(context.where.uri(), anchor), context.where);
  273. return nullptr;
  274. }
  275. if (context.version == Version::Draft2019_09 && schema.contains("$recursiveAnchor")) {
  276. EXPECT_M(schema["$recursiveAnchor"].as_boolean(), "$recursiveAnchor MUST be 'true'");
  277. context.root.dynamic_anchor(detail::Anchor(), context);
  278. return [&context]() { context.root.remove_dynamic_anchor(detail::Anchor(), context.where); };
  279. }
  280. if (context.version > Version::Draft2019_09 && schema.contains("$dynamicAnchor")) {
  281. detail::Anchor anchor(schema["$dynamicAnchor"].as_string());
  282. context.root.dynamic_anchor(anchor, context);
  283. return [&context, anchor]() { context.root.remove_dynamic_anchor(anchor, context.where); };
  284. }
  285. }
  286. template <Adapter A> bool Node::resolve_reference(detail::ParserContext<A> const & context) {
  287. auto const schema = context.schema.as_object();
  288. if (schema.contains("$ref")) {
  289. detail::Reference ref(schema["$ref"].as_string());
  290. reference_ = context.root.resolve(ref, context);
  291. return true;
  292. }
  293. if (context.version < Version::Draft2019_09) {
  294. return false;
  295. }
  296. if (context.version == Version::Draft2019_09 && schema.contains("$recursiveRef")) {
  297. detail::Reference ref(schema["$recursiveRef"].as_string());
  298. EXPECT_M(ref == detail::Reference(), "Only the root schema is permitted as a $recursiveRef");
  299. reference_ = context.root.resolve_dynamic(detail::Anchor());
  300. return true;
  301. }
  302. if (context.version > Version::Draft2019_09 && schema.contains("$dynamicRef")) {
  303. detail::Reference ref(schema["$dynamicRef"].as_string());
  304. reference_ = context.root.resolve_dynamic(ref.anchor());
  305. return true;
  306. }
  307. return false;
  308. }
  309. template <Adapter A> Node::Node(detail::ParserContext<A> context) {
  310. EXPECT(context.schema.type() == adapter::Type::Object);
  311. auto const schema = context.schema.as_object();
  312. if (schema.contains("$schema")) {
  313. // At any point in the schema, we're allowed to change versions
  314. // This means that we're not version-locked to the latest grammar
  315. // (which is especially important for some breaking changes)
  316. context.version = schema_version(context.schema);
  317. }
  318. if (schema.contains("$id")) {
  319. context.root.alias(detail::Reference(schema["$id"].as_string(), false), this);
  320. }
  321. [[maybe_unused]] auto _ = resolve_anchor(context);
  322. bool const has_reference = resolve_reference(context);
  323. if (schema.contains("default")) {
  324. default_ = schema["default"].freeze();
  325. }
  326. if (schema.contains("description")) {
  327. description_ = schema["description"].as_string();
  328. }
  329. for (auto [key, subschema] : schema) {
  330. // Using a constraint store allows overriding certain rules, or the creation
  331. // of user-defined extention vocabularies.
  332. if (auto make_constraint = context.factory(key, context.version)) {
  333. EXPECT_M(not has_reference || context.version >= Version::Draft2019_09,
  334. "Cannot directly extend $ref schemas before Draft2019-09");
  335. // A constraint may return null if it is not applicable - but otherwise
  336. // well-formed. For example, before Draft-06 "exclusiveMaximum" was a
  337. // modifier property for "maximum", and not a unique constaint on its own.
  338. // Therefore, we parse it alongside parsing "maximum", and could return
  339. // nullptr when requesting a constraint pointer for "exclusiveMaximum".
  340. if (auto constraint = make_constraint(context.child(subschema, key))) {
  341. auto & into = context.factory.is_post_constraint(key) ? post_constraints_ : constraints_;
  342. into.emplace(key, std::move(constraint));
  343. }
  344. }
  345. }
  346. }
  347. }