schema.h 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  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. #include <jvalidate/reference_handler.h>
  18. namespace jvalidate::schema {
  19. class Node {
  20. private:
  21. std::string description_;
  22. std::unique_ptr<adapter::Const const> default_{nullptr};
  23. std::optional<std::string> rejects_all_;
  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() = default;
  33. Node(std::string const & rejection_reason) : rejects_all_(rejection_reason) {}
  34. template <Adapter A> void construct(detail::ParserContext<A> context);
  35. bool is_pure_reference() const {
  36. return reference_ && constraints_.empty() && post_constraints_.empty() && not default_;
  37. }
  38. bool accepts_all() const {
  39. return not reference_ && constraints_.empty() && post_constraints_.empty();
  40. }
  41. std::optional<std::string> const & rejects_all() const { return rejects_all_; }
  42. std::optional<schema::Node const *> reference_schema() const { return reference_; }
  43. bool requires_result_context() const { return not post_constraints_.empty(); }
  44. auto const & constraints() const { return constraints_; }
  45. auto const & post_constraints() const { return post_constraints_; }
  46. adapter::Const const * default_value() const { return default_.get(); }
  47. private:
  48. template <Adapter A> detail::OnBlockExit resolve_anchor(detail::ParserContext<A> & context);
  49. template <Adapter A> bool resolve_reference(detail::ParserContext<A> const & context);
  50. };
  51. inline Version Node::schema_version(std::string_view url) {
  52. static std::map<std::string_view, Version> const g_schema_ids{
  53. {"json-schema.org/draft-04/schema", Version::Draft04},
  54. {"json-schema.org/draft-06/schema", Version::Draft06},
  55. {"json-schema.org/draft-07/schema", Version::Draft07},
  56. {"json-schema.org/draft/2019-09/schema", Version::Draft2019_09},
  57. {"json-schema.org/draft/2020-12/schema", Version::Draft2020_12},
  58. };
  59. if (url.ends_with('#')) {
  60. url.remove_suffix(1);
  61. }
  62. if (url.starts_with("http://") || url.starts_with("https://")) {
  63. url.remove_prefix(url.find(':') + 3);
  64. }
  65. auto it = g_schema_ids.find(url);
  66. EXPECT_T(it != g_schema_ids.end(), std::invalid_argument, url);
  67. return it->second;
  68. }
  69. Version Node::schema_version(Adapter auto const & json) {
  70. EXPECT(json.type() == adapter::Type::Object);
  71. EXPECT(json.as_object().contains("$schema"));
  72. auto const & schema = json.as_object()["$schema"];
  73. EXPECT(schema.type() == adapter::Type::String);
  74. return schema_version(schema.as_string());
  75. }
  76. Version Node::schema_version(Adapter auto const & json, Version default_version) {
  77. RETURN_UNLESS(json.type() == adapter::Type::Object, default_version);
  78. RETURN_UNLESS(json.as_object().contains("$schema"), default_version);
  79. auto const & schema = json.as_object()["$schema"];
  80. RETURN_UNLESS(schema.type() == adapter::Type::String, default_version);
  81. return schema_version(schema.as_string());
  82. }
  83. }
  84. namespace jvalidate {
  85. class Schema : public schema::Node {
  86. private:
  87. friend class schema::Node;
  88. template <Adapter A> friend class detail::ParserContext;
  89. private:
  90. schema::Node accept_;
  91. schema::Node reject_{"always false"};
  92. std::map<detail::Anchor, detail::Reference> dynamic_anchors_;
  93. // An owning cache of all created schemas. Avoids storing duplicates such as
  94. // the "always-true" schema, "always-false" schema, and schemas whose only
  95. // meaningful field is "$ref", "$recursiveRef", or "$dynamicRef".
  96. std::map<detail::Reference, schema::Node> cache_;
  97. // A non-owning cache of all schemas, including duplcates where multiple
  98. // References map to the same underlying schema.
  99. std::map<detail::Reference, schema::Node const *> alias_cache_;
  100. public:
  101. /**
  102. * @brief Construct a new schema. All other constructors of this type may be
  103. * considered syntactic sugar for this constructor.
  104. *
  105. * As such, the true signature of this class's contructor is:
  106. *
  107. * Schema(Adapter| JSON
  108. * [, schema::Version]
  109. * [, URIResolver<A> | DocumentCache<A> &]
  110. * [, ConstraintFactory<A> const &])
  111. *
  112. * as long as the order of arguments is preserved - the constructor will work
  113. * no matter which arguments are ignored. The only required argument being
  114. * the JSON object/Adapter.
  115. *
  116. * @param json An adapter to a json object
  117. *
  118. * @param version The json-schema draft version that all schemas will prefer
  119. *
  120. * @param external An object capable of resolving URIs, and turning them into
  121. * Adapter objects. Holds a cache and so must be mutable.
  122. *
  123. * @param factory An object that manuafactures constraints - allows the user
  124. * to provide custom extensions or even modify the behavior of existing
  125. * keywords by overridding the virtual accessor function(s).
  126. */
  127. template <Adapter A>
  128. Schema(A const & json, schema::Version version, DocumentCache<A> & external,
  129. ConstraintFactory<A> const & factory = {}) {
  130. // Prevent unintialized data caches
  131. if (version >= schema::Version::Draft06 && json.type() == adapter::Type::Boolean) {
  132. schema::Node::operator=(std::move(json.as_boolean() ? accept_ : reject_));
  133. return;
  134. }
  135. ReferenceManager<A> ref(external, json, version, factory.keywords(version));
  136. detail::ParserContext<A> root{*this, json, version, factory, ref};
  137. root.where = root.dynamic_where = ref.canonicalize({}, {});
  138. construct(root);
  139. }
  140. /**
  141. * @param json An adapter to a json schema
  142. *
  143. * @param version The json-schema draft version that all schemas will prefer
  144. *
  145. * @param external An object capable of resolving URIs, and turning them into
  146. * Adapter objects. Holds a cache and so must be mutable. If this constructor
  147. * is called, then it means that the cache is a one-off object, and will not
  148. * be reused.
  149. */
  150. template <Adapter A, typename... Args>
  151. Schema(A const & json, schema::Version version, DocumentCache<A> && external, Args &&... args)
  152. : Schema(json, version, external, std::forward<Args>(args)...) {}
  153. /**
  154. * @param json An adapter to a json schema
  155. *
  156. * @param version The json-schema draft version that all schemas will prefer
  157. *
  158. * @param resolve A function capable of resolving URIs, and storing the
  159. * contents in a provided concrete JSON object.
  160. */
  161. template <Adapter A, typename... Args>
  162. Schema(A const & json, schema::Version version, URIResolver<A> resolve, Args &&... args)
  163. : Schema(json, version, DocumentCache<A>(resolve), std::forward<Args>(args)...) {}
  164. /**
  165. * @param json An adapter to a json schema
  166. *
  167. * @param version The json-schema draft version that all schemas will prefer
  168. */
  169. template <Adapter A, Not<DocumentCache<A>>... Args>
  170. Schema(A const & json, schema::Version version, Args &&... args)
  171. : Schema(json, version, DocumentCache<A>(), std::forward<Args>(args)...) {}
  172. /**
  173. * @param json An adapter to a json schema
  174. */
  175. template <Adapter A, Not<schema::Version>... Args>
  176. explicit Schema(A const & json, Args &&... args)
  177. : Schema(json, schema_version(json), std::forward<Args>(args)...) {}
  178. /**
  179. * @param json Any non-adapter (JSON) object. Will be immedately converted
  180. * into an Adapter object to allow us to walk through it w/o specialization.
  181. */
  182. template <typename JSON, typename... Args>
  183. explicit Schema(JSON const & json, Args &&... args)
  184. : Schema(adapter::AdapterFor<JSON const>(json), std::forward<Args>(args)...) {}
  185. private:
  186. schema::Node const * alias(detail::Reference const & where, schema::Node const * schema) {
  187. alias_cache_.emplace(where, schema);
  188. return schema;
  189. }
  190. std::optional<schema::Node const *> from_cache(detail::Reference const & ref) {
  191. if (auto it = alias_cache_.find(ref); it != alias_cache_.end()) {
  192. return it->second;
  193. }
  194. return std::nullopt;
  195. }
  196. template <Adapter A>
  197. schema::Node const * resolve(detail::Reference const & ref,
  198. detail::ParserContext<A> const & context,
  199. bool is_dynamic_recursion = false) {
  200. detail::Reference lexical = context.ref.canonicalize(ref, context.where);
  201. detail::Reference dynamic =
  202. dynamic_anchors_.empty() || is_dynamic_recursion ? lexical : context.dynamic_where / "$ref";
  203. if (std::optional cached = from_cache(dynamic)) {
  204. return *cached;
  205. }
  206. // Special case if the root-level document does not have an $id property
  207. if (std::optional root = context.ref.load(lexical, context)) {
  208. return fetch_schema(context.rebind(*root, lexical, dynamic));
  209. }
  210. std::string error = "URIResolver could not resolve " + std::string(lexical.uri());
  211. return alias(dynamic, &cache_.try_emplace(dynamic, error).first->second);
  212. }
  213. template <Adapter A> schema::Node const * fetch_schema(detail::ParserContext<A> const & context) {
  214. // TODO(samjaffe): No longer promises uniqueness - instead track unique URI's
  215. if (std::optional cached = from_cache(context.dynamic_where)) {
  216. return *cached;
  217. }
  218. adapter::Type const type = context.schema.type();
  219. if (type == adapter::Type::Boolean && context.version >= schema::Version::Draft06) {
  220. return alias(context.dynamic_where, context.schema.as_boolean() ? &accept_ : &reject_);
  221. }
  222. EXPECT_M(type == adapter::Type::Object, "invalid schema at " << context.dynamic_where);
  223. if (context.schema.object_size() == 0) {
  224. return alias(context.dynamic_where, &accept_);
  225. }
  226. auto [it, created] = cache_.try_emplace(context.dynamic_where);
  227. EXPECT_M(created, "creating duplicate schema at... " << context.dynamic_where);
  228. // Do this here first in order to protect from infinite loops
  229. alias(context.dynamic_where, &it->second);
  230. it->second.construct(context);
  231. /* if (not it->second.is_pure_reference()) { */
  232. return &it->second;
  233. /* } */
  234. // Special Case - if the only is the reference constraint, then we don't need
  235. // to store it uniquely. Draft2019_09 supports directly extending a $ref schema
  236. // in the same schema, instead of requiring an allOf clause.
  237. /* schema::Node const * node = *it->second.reference_schema(); */
  238. /* cache_.erase(it); */
  239. /* return alias_cache_[context.where] = node; */
  240. }
  241. };
  242. }
  243. namespace jvalidate::detail {
  244. template <Adapter A> schema::Node const * ParserContext<A>::node() const {
  245. return root.fetch_schema(*this);
  246. }
  247. template <Adapter A> schema::Node const * ParserContext<A>::always() const {
  248. return fixed_schema(schema.as_boolean());
  249. }
  250. template <Adapter A> schema::Node const * ParserContext<A>::fixed_schema(bool accept) const {
  251. return accept ? &root.accept_ : &root.reject_;
  252. }
  253. }
  254. namespace jvalidate::schema {
  255. template <Adapter A> detail::OnBlockExit Node::resolve_anchor(detail::ParserContext<A> & context) {
  256. auto const schema = context.schema.as_object();
  257. if (schema.contains("$anchor") || context.version < schema::Version::Draft2019_09) {
  258. return nullptr;
  259. }
  260. if (context.version == schema::Version::Draft2019_09) {
  261. if (not schema.contains("$recursiveAnchor") || not schema["$recursiveAnchor"].as_boolean()) {
  262. if (not schema.contains("$id") && not schema.contains("$recursiveAnchor")) {
  263. return nullptr;
  264. }
  265. auto it = context.root.dynamic_anchors_.find(detail::Anchor());
  266. if (it == context.root.dynamic_anchors_.end()) {
  267. return nullptr;
  268. }
  269. detail::Reference where = it->second;
  270. context.root.dynamic_anchors_.erase(it);
  271. return [&root = context.root, where]() {
  272. root.dynamic_anchors_.emplace(detail::Anchor(), where);
  273. };
  274. } else if (context.root.dynamic_anchors_.emplace(detail::Anchor(), context.where).second) {
  275. return [&root = context.root]() { root.dynamic_anchors_.erase(detail::Anchor()); };
  276. }
  277. }
  278. if (context.version == schema::Version::Draft2019_09 && schema.contains("$recursiveAnchor") &&
  279. schema["$recursiveAnchor"].as_boolean()) {
  280. if (context.root.dynamic_anchors_.emplace(detail::Anchor(), context.where).second) {
  281. return [&root = context.root]() { root.dynamic_anchors_.erase(detail::Anchor()); };
  282. }
  283. }
  284. if (context.version > Version::Draft2019_09 && schema.contains("$dynamicAnchor")) {
  285. detail::Anchor anchor(schema["$dynamicAnchor"].as_string());
  286. if (context.root.dynamic_anchors_.emplace(anchor, context.where).second) {
  287. return [&root = context.root, anchor]() { root.dynamic_anchors_.erase(anchor); };
  288. }
  289. }
  290. return nullptr;
  291. }
  292. template <Adapter A> bool Node::resolve_reference(detail::ParserContext<A> const & context) {
  293. auto const schema = context.schema.as_object();
  294. if (schema.contains("$ref")) {
  295. detail::Reference ref(schema["$ref"].as_string());
  296. bool is_dynamic_ref_cludge = context.root.dynamic_anchors_.contains(ref.anchor()) &&
  297. ref.uri().empty() && ref.pointer().empty();
  298. reference_ = context.root.resolve(ref, context, is_dynamic_ref_cludge);
  299. return true;
  300. }
  301. if (context.version < Version::Draft2019_09) {
  302. return false;
  303. }
  304. std::string const dyn_ref =
  305. context.version > schema::Version::Draft2019_09 ? "$dynamicRef" : "$recursiveRef";
  306. if (schema.contains(dyn_ref)) {
  307. detail::Reference ref(schema[dyn_ref].as_string());
  308. // TODO(samjaffe): Relocate...
  309. if (auto it = context.root.dynamic_anchors_.find(ref.anchor());
  310. it != context.root.dynamic_anchors_.end()) {
  311. // TODO(samjaffe): This does not re-compute things...
  312. reference_ = context.root.resolve(it->second, context, true);
  313. } else {
  314. reference_ = context.root.resolve(ref, context);
  315. }
  316. return true;
  317. }
  318. return false;
  319. }
  320. template <Adapter A> void Node::construct(detail::ParserContext<A> context) {
  321. EXPECT(context.schema.type() == adapter::Type::Object);
  322. auto const schema = context.schema.as_object();
  323. if (schema.contains("$schema")) {
  324. // At any point in the schema, we're allowed to change versions
  325. // This means that we're not version-locked to the latest grammar
  326. // (which is especially important for some breaking changes)
  327. context.version = schema_version(context.schema);
  328. }
  329. [[maybe_unused]] auto _ = resolve_anchor(context);
  330. bool const has_reference = resolve_reference(context);
  331. if (schema.contains("default")) {
  332. default_ = schema["default"].freeze();
  333. }
  334. if (schema.contains("description")) {
  335. description_ = schema["description"].as_string();
  336. }
  337. // Prior to Draft 2019-09, reference keywords take precedence over everything
  338. // else (instead of allowing direct extensions).
  339. if (has_reference && context.version < Version::Draft2019_09) {
  340. return;
  341. }
  342. for (auto const & [key, subschema] : schema) {
  343. // Using a constraint store allows overriding certain rules, or the creation
  344. // of user-defined extention vocabularies.
  345. auto make_constraint = context.factory(key, context.version);
  346. if (not make_constraint) {
  347. continue;
  348. }
  349. // A constraint may return null if it is not applicable - but otherwise
  350. // well-formed. For example, before Draft-06 "exclusiveMaximum" was a
  351. // modifier property for "maximum", and not a unique constaint on its own.
  352. // Therefore, we parse it alongside parsing "maximum", and could return
  353. // nullptr when requesting a constraint pointer for "exclusiveMaximum".
  354. auto constraint = make_constraint(context.child(subschema, key));
  355. if (not constraint) {
  356. continue;
  357. }
  358. if (context.factory.is_post_constraint(key)) {
  359. post_constraints_.emplace(key, std::move(constraint));
  360. } else {
  361. constraints_.emplace(key, std::move(constraint));
  362. }
  363. }
  364. }
  365. }