schema.h 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  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/detail/reference_manager.h>
  15. #include <jvalidate/detail/version.h>
  16. #include <jvalidate/document_cache.h>
  17. #include <jvalidate/enum.h>
  18. #include <jvalidate/forward.h>
  19. namespace jvalidate::schema {
  20. class Node {
  21. private:
  22. std::string description_;
  23. std::unique_ptr<adapter::Const const> default_{nullptr};
  24. std::optional<std::string> rejects_all_;
  25. std::optional<schema::Node const *> reference_{};
  26. std::unordered_map<std::string, std::unique_ptr<constraint::Constraint>> constraints_{};
  27. std::unordered_map<std::string, std::unique_ptr<constraint::Constraint>> post_constraints_{};
  28. public:
  29. Node() = default;
  30. Node(std::string const & rejection_reason) : rejects_all_(rejection_reason) {}
  31. template <Adapter A> void construct(detail::ParserContext<A> context);
  32. bool is_pure_reference() const {
  33. return reference_ && constraints_.empty() && post_constraints_.empty() && not default_;
  34. }
  35. bool accepts_all() const {
  36. return not reference_ && constraints_.empty() && post_constraints_.empty();
  37. }
  38. std::optional<std::string> const & rejects_all() const { return rejects_all_; }
  39. std::optional<schema::Node const *> reference_schema() const { return reference_; }
  40. bool requires_result_context() const { return not post_constraints_.empty(); }
  41. auto const & constraints() const { return constraints_; }
  42. auto const & post_constraints() const { return post_constraints_; }
  43. adapter::Const const * default_value() const { return default_.get(); }
  44. private:
  45. template <Adapter A> detail::OnBlockExit resolve_anchor(detail::ParserContext<A> const & context);
  46. template <Adapter A> bool resolve_reference(detail::ParserContext<A> const & context);
  47. };
  48. }
  49. namespace jvalidate {
  50. class Schema : public schema::Node {
  51. private:
  52. friend class schema::Node;
  53. template <Adapter A> friend class detail::ParserContext;
  54. private:
  55. schema::Node accept_;
  56. schema::Node reject_{"always false"};
  57. // An owning cache of all created schemas. Avoids storing duplicates such as
  58. // the "always-true" schema, "always-false" schema, and schemas whose only
  59. // meaningful field is "$ref", "$recursiveRef", or "$dynamicRef".
  60. std::map<detail::Reference, schema::Node> cache_;
  61. // A non-owning cache of all schemas, including duplcates where multiple
  62. // References map to the same underlying schema.
  63. std::map<detail::Reference, schema::Node const *> alias_cache_;
  64. public:
  65. /**
  66. * @brief Construct a new schema. All other constructors of this type may be
  67. * considered syntactic sugar for this constructor.
  68. *
  69. * As such, the true signature of this class's contructor is:
  70. *
  71. * Schema(Adapter| JSON
  72. * [, schema::Version]
  73. * [, URIResolver<A> | DocumentCache<A> &]
  74. * [, ConstraintFactory<A> const &])
  75. *
  76. * as long as the order of arguments is preserved - the constructor will work
  77. * no matter which arguments are ignored. The only required argument being
  78. * the JSON object/Adapter.
  79. *
  80. * @param json An adapter to a json object
  81. *
  82. * @param version The json-schema draft version that all schemas will prefer
  83. *
  84. * @param external An object capable of resolving URIs, and turning them into
  85. * Adapter objects. Holds a cache and so must be mutable.
  86. *
  87. * @param factory An object that manuafactures constraints - allows the user
  88. * to provide custom extensions or even modify the behavior of existing
  89. * keywords by overridding the virtual accessor function(s).
  90. */
  91. template <Adapter A>
  92. Schema(A const & json, schema::Version version, DocumentCache<A> & external,
  93. ConstraintFactory<A> const & factory = {}) {
  94. // Prevent unintialized data caches
  95. if (version >= schema::Version::Draft06 && json.type() == adapter::Type::Boolean) {
  96. schema::Node::operator=(std::move(json.as_boolean() ? accept_ : reject_));
  97. return;
  98. }
  99. detail::ReferenceManager<A> ref(external, json, version, factory);
  100. detail::ParserContext<A> root{*this, json, version, factory, ref};
  101. root.where = root.dynamic_where = ref.canonicalize({}, {}, false);
  102. construct(root);
  103. }
  104. /**
  105. * @param json An adapter to a json schema
  106. *
  107. * @param version The json-schema draft version that all schemas will prefer
  108. *
  109. * @param external An object capable of resolving URIs, and turning them into
  110. * Adapter objects. Holds a cache and so must be mutable. If this constructor
  111. * is called, then it means that the cache is a one-off object, and will not
  112. * be reused.
  113. */
  114. template <Adapter A, typename... Args>
  115. Schema(A const & json, schema::Version version, DocumentCache<A> && external, Args &&... args)
  116. : Schema(json, version, external, std::forward<Args>(args)...) {}
  117. /**
  118. * @param json An adapter to a json schema
  119. *
  120. * @param version The json-schema draft version that all schemas will prefer
  121. *
  122. * @param resolve A function capable of resolving URIs, and storing the
  123. * contents in a provided concrete JSON object.
  124. */
  125. template <Adapter A, typename... Args>
  126. Schema(A const & json, schema::Version version, URIResolver<A> resolve, Args &&... args)
  127. : Schema(json, version, DocumentCache<A>(resolve), std::forward<Args>(args)...) {}
  128. /**
  129. * @param json An adapter to a json schema
  130. *
  131. * @param version The json-schema draft version that all schemas will prefer
  132. */
  133. template <Adapter A, Not<DocumentCache<A>>... Args>
  134. Schema(A const & json, schema::Version version, Args &&... args)
  135. : Schema(json, version, DocumentCache<A>(), std::forward<Args>(args)...) {}
  136. /**
  137. * @param json An adapter to a json schema
  138. */
  139. template <Adapter A, Not<schema::Version>... Args>
  140. explicit Schema(A const & json, Args &&... args)
  141. : Schema(json, detail::version(json), std::forward<Args>(args)...) {}
  142. /**
  143. * @param json Any non-adapter (JSON) object. Will be immedately converted
  144. * into an Adapter object to allow us to walk through it w/o specialization.
  145. */
  146. template <typename JSON, typename... Args>
  147. explicit Schema(JSON const & json, Args &&... args)
  148. : Schema(adapter::AdapterFor<JSON const>(json), std::forward<Args>(args)...) {}
  149. private:
  150. schema::Node const * alias(detail::Reference const & where, schema::Node const * schema) {
  151. alias_cache_.emplace(where, schema);
  152. return schema;
  153. }
  154. std::optional<schema::Node const *> from_cache(detail::Reference const & ref) {
  155. if (auto it = alias_cache_.find(ref); it != alias_cache_.end()) {
  156. return it->second;
  157. }
  158. return std::nullopt;
  159. }
  160. template <Adapter A>
  161. schema::Node const * resolve(detail::Reference const & ref,
  162. detail::ParserContext<A> const & context, bool dynamic_reference) {
  163. detail::Reference lexical = context.ref.canonicalize(ref, context.where, dynamic_reference);
  164. detail::Reference dynamic = dynamic_reference ? lexical : context.dynamic_where / "$ref";
  165. if (std::optional cached = from_cache(dynamic)) {
  166. return *cached;
  167. }
  168. if (std::optional root = context.ref.load(lexical, context.version)) {
  169. return fetch_schema(context.rebind(*root, lexical, dynamic));
  170. }
  171. std::string error = "URIResolver could not resolve " + std::string(lexical.uri());
  172. return alias(dynamic, &cache_.try_emplace(dynamic, error).first->second);
  173. }
  174. template <Adapter A> schema::Node const * fetch_schema(detail::ParserContext<A> const & context) {
  175. // TODO(samjaffe): No longer promises uniqueness - instead track unique URI's
  176. if (std::optional cached = from_cache(context.dynamic_where)) {
  177. return *cached;
  178. }
  179. adapter::Type const type = context.schema.type();
  180. if (type == adapter::Type::Boolean && context.version >= schema::Version::Draft06) {
  181. return alias(context.dynamic_where, context.schema.as_boolean() ? &accept_ : &reject_);
  182. }
  183. EXPECT_M(type == adapter::Type::Object, "invalid schema at " << context.dynamic_where);
  184. if (context.schema.object_size() == 0) {
  185. return alias(context.dynamic_where, &accept_);
  186. }
  187. auto [it, created] = cache_.try_emplace(context.dynamic_where);
  188. EXPECT_M(created, "creating duplicate schema at... " << context.dynamic_where);
  189. // Do this here first in order to protect from infinite loops
  190. alias(context.dynamic_where, &it->second);
  191. it->second.construct(context);
  192. return &it->second;
  193. }
  194. };
  195. }
  196. namespace jvalidate::detail {
  197. template <Adapter A> schema::Node const * ParserContext<A>::node() const {
  198. return root.fetch_schema(*this);
  199. }
  200. template <Adapter A> schema::Node const * ParserContext<A>::always() const {
  201. return fixed_schema(schema.as_boolean());
  202. }
  203. template <Adapter A> schema::Node const * ParserContext<A>::fixed_schema(bool accept) const {
  204. return accept ? &root.accept_ : &root.reject_;
  205. }
  206. }
  207. namespace jvalidate::schema {
  208. template <Adapter A>
  209. detail::OnBlockExit Node::resolve_anchor(detail::ParserContext<A> const & context) {
  210. auto const schema = context.schema.as_object();
  211. if (context.version < schema::Version::Draft2019_09 || not schema.contains("$id")) {
  212. return nullptr;
  213. }
  214. return context.ref.dynamic_scope(context.where);
  215. }
  216. template <Adapter A> bool Node::resolve_reference(detail::ParserContext<A> const & context) {
  217. auto const schema = context.schema.as_object();
  218. if (schema.contains("$ref")) {
  219. detail::Reference ref(schema["$ref"].as_string());
  220. reference_ = context.root.resolve(ref, context, false);
  221. return true;
  222. }
  223. if (context.version < Version::Draft2019_09) {
  224. return false;
  225. }
  226. std::string const dyn_ref =
  227. context.version > schema::Version::Draft2019_09 ? "$dynamicRef" : "$recursiveRef";
  228. if (schema.contains(dyn_ref)) {
  229. detail::Reference ref(schema[dyn_ref].as_string());
  230. reference_ = context.root.resolve(ref, context, true);
  231. return true;
  232. }
  233. return false;
  234. }
  235. template <Adapter A> void Node::construct(detail::ParserContext<A> context) {
  236. EXPECT(context.schema.type() == adapter::Type::Object);
  237. auto const schema = context.schema.as_object();
  238. if (schema.contains("$schema")) {
  239. // At any point in the schema, we're allowed to change versions
  240. // This means that we're not version-locked to the latest grammar
  241. // (which is especially important for some breaking changes)
  242. context.version = detail::version(context.schema);
  243. }
  244. auto _ = resolve_anchor(context);
  245. bool const has_reference = resolve_reference(context);
  246. if (schema.contains("default")) {
  247. default_ = schema["default"].freeze();
  248. }
  249. if (schema.contains("description")) {
  250. description_ = schema["description"].as_string();
  251. }
  252. // Prior to Draft 2019-09, reference keywords take precedence over everything
  253. // else (instead of allowing direct extensions).
  254. if (has_reference && context.version < Version::Draft2019_09) {
  255. return;
  256. }
  257. for (auto const & [key, subschema] : schema) {
  258. // Using a constraint store allows overriding certain rules, or the creation
  259. // of user-defined extention vocabularies.
  260. auto make_constraint = context.factory(key, context.version);
  261. if (not make_constraint) {
  262. continue;
  263. }
  264. // A constraint may return null if it is not applicable - but otherwise
  265. // well-formed. For example, before Draft-06 "exclusiveMaximum" was a
  266. // modifier property for "maximum", and not a unique constaint on its own.
  267. // Therefore, we parse it alongside parsing "maximum", and could return
  268. // nullptr when requesting a constraint pointer for "exclusiveMaximum".
  269. auto constraint = make_constraint(context.child(subschema, key));
  270. if (not constraint) {
  271. continue;
  272. }
  273. if (context.factory.is_post_constraint(key)) {
  274. post_constraints_.emplace(key, std::move(constraint));
  275. } else {
  276. constraints_.emplace(key, std::move(constraint));
  277. }
  278. }
  279. }
  280. }