schema.h 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. #pragma once
  2. #include <memory>
  3. #include <unordered_map>
  4. #include <vector>
  5. #include <jvalidate/adapter.h>
  6. #include <jvalidate/constraint.h>
  7. #include <jvalidate/detail/anchor.h>
  8. #include <jvalidate/detail/expect.h>
  9. #include <jvalidate/detail/on_block_exit.h>
  10. #include <jvalidate/detail/parser_context.h>
  11. #include <jvalidate/detail/pointer.h>
  12. #include <jvalidate/detail/reference.h>
  13. #include <jvalidate/enum.h>
  14. #include <jvalidate/forward.h>
  15. namespace jvalidate::schema {
  16. class Node {
  17. private:
  18. std::string description_;
  19. std::unique_ptr<adapter::Const const> default_{nullptr};
  20. detail::Reference uri_;
  21. bool rejects_all_{false};
  22. std::optional<schema::Node const *> reference_{};
  23. std::unordered_map<std::string, std::unique_ptr<constraint::Constraint>> constraints_{};
  24. std::unordered_map<std::string, std::unique_ptr<constraint::Constraint>> post_constraints_{};
  25. protected:
  26. static Version schema_version(std::string_view url);
  27. static Version schema_version(Adapter auto const & json);
  28. static Version schema_version(Adapter auto const & json, Version default_version);
  29. public:
  30. Node(bool rejects_all = false) : rejects_all_(rejects_all) {}
  31. template <Adapter A> Node(detail::ParserContext<A> context);
  32. bool is_pure_reference() const {
  33. return reference_ && constraints_.empty() && post_constraints_.empty() && not default_;
  34. }
  35. bool rejects_all() const { return rejects_all_; }
  36. std::optional<schema::Node const *> reference_schema() const { return reference_; }
  37. bool requires_result_context() const { return not post_constraints_.empty(); }
  38. auto const & constraints() const { return constraints_; }
  39. auto const & post_constraints() const { return constraints_; }
  40. adapter::Const const * default_value() const { return default_.get(); }
  41. private:
  42. template <Adapter A> detail::OnBlockExit resolve_anchor(detail::ParserContext<A> & context);
  43. template <Adapter A> bool resolve_reference(detail::ParserContext<A> const & context);
  44. };
  45. inline Version Node::schema_version(std::string_view url) {
  46. static std::map<std::string_view, Version> const g_schema_ids{
  47. {"http://json-schema.org/draft-04/schema", Version::Draft04},
  48. {"http://json-schema.org/draft-06/schema", Version::Draft06},
  49. {"http://json-schema.org/draft-07/schema", Version::Draft07},
  50. {"http://json-schema.org/draft/2019-09/schema", Version::Draft2019_09},
  51. {"http://json-schema.org/draft/2020-12/schema", Version::Draft2020_12},
  52. };
  53. if (url.ends_with('#')) {
  54. url.remove_suffix(1);
  55. }
  56. auto it = g_schema_ids.find(url);
  57. EXPECT_T(it != g_schema_ids.end(), std::invalid_argument, url);
  58. return it->second;
  59. }
  60. Version Node::schema_version(Adapter auto const & json) {
  61. EXPECT(json.type() == adapter::Type::Object);
  62. EXPECT(json.as_object().contains("$schema"));
  63. auto const & schema = json.as_object()["$schema"];
  64. EXPECT(schema.type() == adapter::Type::String);
  65. return schema_version(schema.as_string());
  66. }
  67. Version Node::schema_version(Adapter auto const & json, Version default_version) {
  68. RETURN_UNLESS(json.type() == adapter::Type::Object, default_version);
  69. RETURN_UNLESS(json.as_object().contains("$schema"), default_version);
  70. auto const & schema = json.as_object()["$schema"];
  71. RETURN_UNLESS(schema.type() == adapter::Type::String, default_version);
  72. return schema_version(schema.as_string());
  73. }
  74. }
  75. namespace jvalidate {
  76. class Schema : public schema::Node {
  77. private:
  78. friend class schema::Node;
  79. template <Adapter A> friend class detail::ParserContext;
  80. struct DynamicRef {
  81. template <typename F>
  82. DynamicRef(detail::Reference const & where, F const & reconstruct)
  83. : where(where), reconstruct(reconstruct) {}
  84. detail::Reference where;
  85. std::function<schema::Node const *()> reconstruct;
  86. };
  87. private:
  88. schema::Node accept_{true};
  89. schema::Node reject_{false};
  90. std::map<detail::Reference, detail::Reference> anchors_;
  91. std::map<detail::Anchor, DynamicRef> dynamic_anchors_;
  92. std::map<detail::Reference, schema::Node> cache_;
  93. std::map<detail::Reference, schema::Node const *> alias_cache_;
  94. public:
  95. template <Adapter A>
  96. explicit Schema(A const & json, ConstraintFactory<A> const & factory = {})
  97. : Schema(json, schema_version(json), factory) {}
  98. template <Adapter A>
  99. Schema(A const & json, schema::Version version, ConstraintFactory<A> const & factory = {})
  100. : schema::Node(detail::ParserContext<A>{
  101. .root = *this, .schema = json, .version = version, .factory = factory}) {}
  102. template <typename JSON, typename... Args>
  103. explicit Schema(JSON const & json, Args &&... args)
  104. : Schema(adapter::AdapterFor<JSON const>(json), std::forward<Args>(args)...) {}
  105. private:
  106. void anchor(detail::Reference const & anchor, detail::Reference const & from) {
  107. EXPECT_M(anchors_.try_emplace(anchor.root(), from).second,
  108. "more than one anchor found for uri " << anchor);
  109. }
  110. template <Adapter A>
  111. void dynamic_anchor(detail::Anchor const & anchor, detail::ParserContext<A> const & context) {
  112. dynamic_anchors_.try_emplace(anchor, context.where,
  113. [this, context]() { return fetch_schema(context); });
  114. }
  115. void remove_dynamic_anchor(detail::Anchor const & anchor, detail::Reference const & where) {
  116. if (auto it = dynamic_anchors_.find(anchor);
  117. it != dynamic_anchors_.end() && it->second.where == where) {
  118. dynamic_anchors_.erase(it);
  119. }
  120. }
  121. schema::Node const * alias(detail::Reference const & where, schema::Node const * schema) {
  122. EXPECT_M(alias_cache_.try_emplace(where, schema).second,
  123. "more than one schema found with uri " << where);
  124. return schema;
  125. }
  126. template <Adapter A>
  127. schema::Node const * resolve(detail::Reference ref, detail::Reference const & from,
  128. schema::Version default_version) {
  129. // Special case if the root-level document does not have an $id property
  130. if (ref == detail::Reference() && ref.anchor() == from.anchor()) {
  131. return this;
  132. }
  133. if (auto it = anchors_.find(ref.root()); it != anchors_.end()) {
  134. ref = it->second / ref.pointer();
  135. }
  136. if (auto it = alias_cache_.find(ref); it != alias_cache_.end()) {
  137. return it->second;
  138. }
  139. throw;
  140. }
  141. schema::Node const * resolve_dynamic(detail::Anchor const & ref) {
  142. auto it = dynamic_anchors_.find(ref);
  143. EXPECT_M(it != dynamic_anchors_.end(), "Unmatched $dynamicRef '" << ref << "'");
  144. return it->second.reconstruct();
  145. }
  146. template <Adapter A> schema::Node const * fetch_schema(detail::ParserContext<A> const & context) {
  147. adapter::Type const type = context.schema.type();
  148. if (type == adapter::Type::Boolean && context.version >= schema::Version::Draft06) {
  149. return alias(context.where, context.schema.as_boolean() ? &accept_ : &reject_);
  150. }
  151. EXPECT(type == adapter::Type::Object);
  152. if (context.schema.object_size() == 0) {
  153. return alias(context.where, &accept_);
  154. }
  155. auto [it, created] = cache_.try_emplace(context.where, context);
  156. EXPECT_M(created, "more than one schema found with uri " << context.where);
  157. schema::Node const * node = &it->second;
  158. // Special Case - if the only is the reference constraint, then we don't need
  159. // to store it uniquely. Draft2019_09 supports directly extending a $ref schema
  160. // in the same schema, instead of requiring an allOf clause.
  161. if (node->is_pure_reference()) {
  162. node = *node->reference_schema();
  163. cache_.erase(it);
  164. return alias(context.where, node);
  165. }
  166. return alias(context.where, node);
  167. }
  168. };
  169. }
  170. namespace jvalidate::detail {
  171. template <Adapter A> schema::Node const * ParserContext<A>::node() const {
  172. return root.fetch_schema(*this);
  173. }
  174. template <Adapter A> schema::Node const * ParserContext<A>::always() const {
  175. return schema.as_boolean() ? &root.accept_ : &root.reject_;
  176. }
  177. }
  178. namespace jvalidate::schema {
  179. template <Adapter A> detail::OnBlockExit Node::resolve_anchor(detail::ParserContext<A> & context) {
  180. auto const schema = context.schema.as_object();
  181. if (schema.contains("$anchor")) {
  182. // Create an anchor mapping using the current document and the anchor
  183. // string. There's no need for special validation/chaining here, because
  184. // {@see Schema::resolve} will turn all $ref/$dynamicRef anchors into
  185. // their fully-qualified path.
  186. detail::Anchor anchor(schema["$anchor"].as_string());
  187. context.root.anchor(detail::Reference(context.where.uri(), anchor), context.where);
  188. return nullptr;
  189. }
  190. if (context.version == Version::Draft2019_09 && schema.contains("$recursiveAnchor")) {
  191. EXPECT_M(schema["$recursiveAnchor"].as_boolean(), "$recursiveAnchor MUST be 'true'");
  192. context.root.dynamic_anchor(detail::Anchor(), context);
  193. return [&context]() { context.root.remove_dynamic_anchor(detail::Anchor(), context.where); };
  194. }
  195. if (context.version > Version::Draft2019_09 && schema.contains("$dynamicAnchor")) {
  196. detail::Anchor anchor(schema["$dynamicAnchor"].as_string());
  197. context.root.dynamic_anchor(anchor, context);
  198. return [&context, anchor]() { context.root.remove_dynamic_anchor(anchor, context.where); };
  199. }
  200. }
  201. template <Adapter A> bool Node::resolve_reference(detail::ParserContext<A> const & context) {
  202. auto const schema = context.schema.as_object();
  203. if (schema.contains("$ref")) {
  204. detail::Reference ref(schema["$ref"].as_string());
  205. reference_ = context.root.template resolve<A>(ref, context.where, context.version);
  206. return true;
  207. }
  208. if (context.version < Version::Draft2019_09) {
  209. return false;
  210. }
  211. if (context.version == Version::Draft2019_09 && schema.contains("$recursiveRef")) {
  212. detail::Reference ref(schema["$recursiveRef"].as_string());
  213. EXPECT_M(ref == detail::Reference(), "Only the root schema is permitted as a $recursiveRef");
  214. reference_ = context.root.resolve_dynamic(detail::Anchor());
  215. return true;
  216. }
  217. if (context.version > Version::Draft2019_09 && schema.contains("$dynamicRef")) {
  218. detail::Reference ref(schema["$dynamicRef"].as_string());
  219. reference_ = context.root.resolve_dynamic(ref.anchor());
  220. return true;
  221. }
  222. return false;
  223. }
  224. template <Adapter A> Node::Node(detail::ParserContext<A> context) {
  225. EXPECT(context.schema.type() == adapter::Type::Object);
  226. auto const schema = context.schema.as_object();
  227. if (schema.contains("$schema")) {
  228. // At any point in the schema, we're allowed to change versions
  229. // This means that we're not version-locked to the latest grammar
  230. // (which is especially important for some breaking changes)
  231. context.version = schema_version(context.schema);
  232. }
  233. if (schema.contains("$id")) {
  234. context.root.alias(detail::Reference(schema["$id"].as_string(), false), this);
  235. }
  236. [[maybe_unused]] auto _ = resolve_anchor(context);
  237. bool const has_reference = resolve_reference(context);
  238. if (schema.contains("default")) {
  239. default_ = schema["default"].freeze();
  240. }
  241. if (schema.contains("description")) {
  242. description_ = schema["description"].as_string();
  243. }
  244. for (auto [key, subschema] : schema) {
  245. // Using a constraint store allows overriding certain rules, or the creation
  246. // of user-defined extention vocabularies.
  247. if (auto make_constraint = context.factory(key, context.version)) {
  248. EXPECT_M(not has_reference || context.version >= Version::Draft2019_09,
  249. "Cannot directly extend $ref schemas before Draft2019-09");
  250. // A constraint may return null if it is not applicable - but otherwise
  251. // well-formed. For example, before Draft-06 "exclusiveMaximum" was a
  252. // modifier property for "maximum", and not a unique constaint on its own.
  253. // Therefore, we parse it alongside parsing "maximum", and could return
  254. // nullptr when requesting a constraint pointer for "exclusiveMaximum".
  255. if (auto constraint = make_constraint(context.child(subschema, key))) {
  256. auto & into = context.factory.is_post_constraint(key) ? post_constraints_ : constraints_;
  257. into.emplace(key, std::move(constraint));
  258. }
  259. }
  260. }
  261. }
  262. }