schema.h 8.5 KB

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