schemurai 1.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c7e69c0dbcafac0bb00bc78aa315932cfd59a141d0727e1a6e4366f80dc416b
4
- data.tar.gz: 1ddb9269a448dc6c088a7bf22f341e064c2bb3cd0712da6750fc29545379b268
3
+ metadata.gz: f1b157d6e772537cab2886ef6d89bd547992c7359b73a13a49b9d9cfcdcc9808
4
+ data.tar.gz: f0e8e28536c58c95cf1848db949dad8007df0c7a6550dd8b0fd76439841617cd
5
5
  SHA512:
6
- metadata.gz: 5f201d9a44db39000dc36081d9212e221e3e1ba69d4650dcdd929f9709a401cbb6de1412587344246ed736bec0da0e576b19daadc9d91cda5eb4a4d9924d00a8
7
- data.tar.gz: 604dd071010fffc73639338b7f84eefa9328f7c574156cd52077f1d6f6ae9c6ebe0ff1360c2c80c29328659242547b3a3652f0165d926ac944ab1b1fcb025c41
6
+ metadata.gz: b6f8206124aab29ae13908951efdd011e276ca347a75c0abf79da9fb3a9988f91a0a59b0ec559b85a88f01d5975b9ea251f9f14bb7aba634b7c8f3625c288167
7
+ data.tar.gz: 3fcd551c456cb81b53842c0736ae9e629676ca8c7c0e43017ee43991b00b995fde7a530f1c0cb38f81d5e7bb640b68a8ad8ad388934af73b06b4ef77c4e46a34
data/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Schemurai
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/schemurai.svg?icon=si%3Arubygems)](https://badge.fury.io/rb/schemurai)
4
+ [![test / rspec](https://github.com/oakcask/schemurai/actions/workflows/test--rspec.yaml/badge.svg)](https://github.com/oakcask/schemurai/actions/workflows/test--rspec.yaml)
5
+ [![lint / rubocop](https://github.com/oakcask/schemurai/actions/workflows/lint--rubocop.yaml/badge.svg)](https://github.com/oakcask/schemurai/actions/workflows/lint--rubocop.yaml)
6
+
3
7
  A small, light-weight-dependency JSON Schema validator for Ruby supporting Draft 7,
4
8
  Draft 2019-09, and Draft 2020-12. It covers the required cases in the official
5
9
  JSON-Schema-Test-Suite, as well as the applicable optional tests for numeric
@@ -93,6 +97,11 @@ validator. Repeatedly compiling the same schema object with one registry reuses
93
97
  its compiled schema graph. `Schemurai.validate` and `.valid?` continue
94
98
  to accept raw JSON-like schemas and perform compilation internally.
95
99
 
100
+ Schema inputs are checked recursively before compilation. They must use the
101
+ built-in JSON-shaped Ruby classes described in
102
+ [`docs/compatibility-domain.md`](docs/compatibility-domain.md); rejected values
103
+ are never retained by a registry.
104
+
96
105
  To resolve external references, pass a mapping of URIs to schemas using
97
106
  `schemas:`.
98
107
 
@@ -111,7 +120,39 @@ validation for `contentEncoding` and `contentMediaType` with `content: true`.
111
120
  Enable optional format assertions with `format: true`; support for each format is
112
121
  listed separately below.
113
122
 
114
- ### Thread / Ractor native feature
123
+ ### Validation errors
124
+
125
+ ```ruby
126
+ result = Schemurai.validate(
127
+ { "$ref" => "https://example.test/positive" },
128
+ -1,
129
+ schemas: { "https://example.test/positive" => { "type" => "integer", "minimum" => 1 } }
130
+ )
131
+
132
+ validation_error = result.errors[0]
133
+ p validation_error.keyword # => "minimum"
134
+ p validation_error.instance_path # => ""
135
+ p validation_error.schema_path # => "/$ref/minimum"
136
+ p validation_error.message # => "number must be greater than or equal to 1"
137
+ ```
138
+
139
+ `Result#errors` contains `Schemurai::ValidationError` objects.
140
+ Check the following attributes to investigate schema errors:
141
+
142
+ - `keyword` identifies the failed JSON Schema keyword. Schemurai uses
143
+ `falseSchema` for a boolean `false` schema, which has no keyword of its own.
144
+ - `instance_path` is a JSON Pointer to the value that failed validation.
145
+ - `schema_path` is a JSON Pointer to the schema location that produced the
146
+ error.
147
+ - `message` is a human-readable `String` describing the failure.
148
+
149
+ An empty path points to the instance or schema root. Error order is stable.
150
+ The presence and type of `message` are part of the public API, but its exact
151
+ wording is not. Use `keyword`, `instance_path`, and `schema_path`, rather than
152
+ matching `message`, when handling errors programmatically. `ValidationError#to_h`
153
+ returns these four attributes as a Hash.
154
+
155
+ ### Thread / Ractor support
115
156
 
116
157
  To share a registry between threads or Ractors, finish registering schemas and
117
158
  make the registry shareable first. This eagerly compiles every registered
@@ -122,7 +163,9 @@ registry = Schemurai::SchemaRegistry.new(
122
163
  schemas: {
123
164
  "https://example.test/positive" => { "type" => "integer", "minimum" => 1 },
124
165
  "https://example.test/value" => { "$ref" => "https://example.test/positive" }
125
- }
166
+ },
167
+ # enable virtual machine backend that is faster for iterative validation.
168
+ backend: :vm
126
169
  )
127
170
  registry.make_shareable
128
171
 
@@ -134,7 +177,46 @@ validator = registry.validator_for("https://example.test/value")
134
177
  `ResolutionError` if a reference cannot be resolved. After it returns,
135
178
  `validator_for` is read-only and may be called concurrently, while `compile` is
136
179
  no longer available. A `Validator` contains per-validation mutable state and
137
- must not be shared between threads or Ractors.
180
+ must not be shared between threads or Ractors. With the VM backend, the registry
181
+ compiles and shares its bytecode before this transition, so those validators do
182
+ not compile separate instruction streams in each Ractor.
183
+
184
+ ### Meta-schema validation
185
+
186
+ Meta-schema validation is opt-in so ordinary compilation does not pay its
187
+ additional time and memory cost. Set `validate_schema: true` when creating a
188
+ `SchemaRegistry` to validate every schema registered through `schemas:` and
189
+ every schema passed to `compile`. The convenience `compile`, `validate`, and
190
+ `valid?` methods accept the same option and apply it to their internal registry.
191
+ Schemas without `$schema` use the Draft 7 meta-schema. An invalid schema raises
192
+ `Schemurai::InvalidSchemaError`, whose `result` contains the detailed validation
193
+ errors.
194
+
195
+ ```ruby
196
+ Schemurai.compile(schema, validate_schema: true)
197
+
198
+ registry = Schemurai::SchemaRegistry.new(
199
+ schemas: external_schemas,
200
+ validate_schema: true
201
+ )
202
+ validator = registry.compile(schema)
203
+
204
+ result = Schemurai.validate_schema(schema)
205
+ result.valid? # whether the schema itself is valid
206
+ ```
207
+
208
+ `SchemaRegistry#validate_schema` and `#valid_schema?` perform explicit checks
209
+ regardless of the registry option and reuse meta-schema graph data within that
210
+ registry. The stateful validator used for a check is temporary; no global
211
+ validator is retained.
212
+
213
+ ### Backend selection
214
+
215
+ `Schemurai.backend`, `SchemaRegistry#backend`, and `Validator#backend` expose
216
+ the actual backend. Pass `backend: :ruby` to force the Ruby oracle or
217
+ `backend: :vm` to ahead-of-time compile schemas for the validator virtual machine.
218
+ Schema graph construction remains shared Ruby infrastructure. Environment-based selection
219
+ is documented in [`docs/backend-selection.md`](docs/backend-selection.md).
138
220
 
139
221
  ## JSON Schema conformance
140
222
 
@@ -195,6 +277,16 @@ Run the test suite with:
195
277
  bundle exec rspec
196
278
  ```
197
279
 
280
+ Verify the reviewed official-suite classifications with:
281
+
282
+ ```sh
283
+ ruby script/oracle-cases --summary
284
+ ```
285
+
286
+ The serialized oracle tools are `script/oracle-runner` and
287
+ `script/oracle-compare`. Ruby interpreter and YJIT baseline instructions are in
288
+ [`benchmark/baselines/README.md`](benchmark/baselines/README.md).
289
+
198
290
  Run the linter with:
199
291
 
200
292
  ```sh
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Schemurai
4
+ module Backend
5
+ VM_FEATURE = "vm/evaluator"
6
+ CHOICES = %i[default ruby vm].freeze
7
+
8
+ module_function def requested
9
+ value = ENV.fetch("SCHEMURAI_BACKEND", "default").to_sym
10
+ return value if CHOICES.include?(value)
11
+
12
+ raise Error, "unknown Schemurai backend #{value.inspect}"
13
+ end
14
+
15
+ module_function def resolve(selection = requested)
16
+ selection = selection.to_sym
17
+ raise Error, "unknown Schemurai backend #{selection.inspect}" unless CHOICES.include?(selection)
18
+
19
+ selection = production_default if selection == :default
20
+ load_vm! if selection == :vm
21
+ selection
22
+ end
23
+
24
+ module_function def production_default
25
+ :ruby
26
+ end
27
+
28
+ module_function def load_vm!
29
+ require_relative VM_FEATURE
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Schemurai
4
+ module Internal
5
+ # Builds validation messages shared by every evaluator backend.
6
+ module ErrorMessage
7
+ module_function def false_schema = "value is not allowed by this schema"
8
+
9
+ module_function def type(expected, value)
10
+ expected = Array(expected).map { |name| name.inspect }.join(" or ")
11
+ "value has type #{json_type(value).inspect}, but must have type #{expected}"
12
+ end
13
+
14
+ module_function def enum = "value must equal one of the values defined by `enum`"
15
+ module_function def const = "value must equal the value defined by `const`"
16
+
17
+ module_function def numeric_limit(keyword, limit)
18
+ comparison = {
19
+ "maximum" => "less than or equal to",
20
+ "minimum" => "greater than or equal to",
21
+ "exclusiveMaximum" => "less than",
22
+ "exclusiveMinimum" => "greater than"
23
+ }.fetch(keyword)
24
+ "number must be #{comparison} #{display_number(limit)}"
25
+ end
26
+
27
+ module_function def multiple_of(divisor) = "number must be a multiple of #{display_number(divisor)}"
28
+
29
+ module_function def size(keyword, limit, actual)
30
+ subject, unit, comparison = {
31
+ "maxLength" => ["string", "characters", "at most"],
32
+ "minLength" => ["string", "characters", "at least"],
33
+ "maxItems" => ["array", "items", "at most"],
34
+ "minItems" => ["array", "items", "at least"],
35
+ "maxProperties" => ["object", "properties", "at most"],
36
+ "minProperties" => ["object", "properties", "at least"]
37
+ }.fetch(keyword)
38
+ unit = {"characters" => "character", "items" => "item", "properties" => "property"}.fetch(unit) if limit == 1
39
+ "#{subject} must contain #{comparison} #{limit} #{unit} (found #{actual})"
40
+ end
41
+
42
+ module_function def pattern(pattern) = "string must match pattern #{pattern.inspect}"
43
+ module_function def invalid_pattern(pattern) = "schema pattern #{pattern.inspect} is not a valid regular expression"
44
+ module_function def format(name) = "string must match the #{name} format"
45
+ module_function def content_encoding = "string must be valid base64"
46
+ module_function def content_media_type = "string must contain valid JSON"
47
+ module_function def unique_items = "array items must be unique"
48
+
49
+ module_function def contains(actual, minimum, maximum)
50
+ expected = if maximum.infinite?
51
+ "at least #{minimum}"
52
+ elsif minimum == maximum
53
+ "exactly #{minimum}"
54
+ elsif minimum.zero?
55
+ "at most #{maximum}"
56
+ else
57
+ "between #{minimum} and #{maximum}"
58
+ end
59
+ "array must contain #{expected} items matching `contains` (found #{actual})"
60
+ end
61
+
62
+ module_function def required(name) = "object is missing required property #{name.inspect}"
63
+
64
+ module_function def dependent_required(name, required_name)
65
+ "property #{required_name.inspect} is required when property #{name.inspect} is present"
66
+ end
67
+
68
+ module_function def any_of = "value must match at least one subschema"
69
+ module_function def one_of(matches) = "value must match exactly one subschema (matched #{matches})"
70
+ module_function def not = "value must not match the subschema"
71
+
72
+ module_function def json_type(value)
73
+ case value
74
+ when nil then "null"
75
+ when true, false then "boolean"
76
+ when Hash then "object"
77
+ when Array then "array"
78
+ when String then "string"
79
+ when Numeric
80
+ if value.is_a?(Complex)
81
+ value.class.name
82
+ elsif value.finite? && value.to_i == value
83
+ "integer"
84
+ else
85
+ "number"
86
+ end
87
+ else
88
+ value.class.name
89
+ end
90
+ end
91
+ private_class_method :json_type
92
+
93
+ module_function def display_number(value)
94
+ rational = value.is_a?(Rational) ? value : Rational(value.to_s)
95
+ return rational.numerator.to_s if rational.denominator == 1
96
+
97
+ rational.to_f.to_s
98
+ end
99
+ private_class_method :display_number
100
+ end
101
+ end
102
+ end
@@ -3,12 +3,15 @@
3
3
  require "json"
4
4
  require "base64"
5
5
  require_relative "evaluation"
6
+ require_relative "error_message"
6
7
 
7
8
  module Schemurai
8
9
  module Internal
9
10
  class Evaluator
10
11
  MISSING_SEGMENT = Object.new.freeze
11
12
 
13
+ def backend = :ruby
14
+
12
15
  def initialize(graph, root, content: false, format: false)
13
16
  @validate_content = content
14
17
  @validate_format = format
@@ -28,6 +31,7 @@ module Schemurai
28
31
  @error_callback = nil
29
32
  @error_count = 0
30
33
  @track_dynamic_scope = @graph.dynamic_scope?
34
+ @dynamic_scope = nil
31
35
  @instance_path = nil
32
36
  @schema_path = nil
33
37
  evaluate_valid(@root, instance)
@@ -37,6 +41,7 @@ module Schemurai
37
41
  @error_callback = callback
38
42
  @error_count = 0
39
43
  @track_dynamic_scope = @graph.dynamic_scope?
44
+ @dynamic_scope = nil
40
45
  @instance_path = []
41
46
  @schema_path = []
42
47
  evaluate(@root, instance)
@@ -329,7 +334,7 @@ module Schemurai
329
334
  schema = node.schema
330
335
  return Evaluation.valid if schema == true
331
336
  if schema == false
332
- add_error("falseSchema", "boolean schema is false", append_keyword: false)
337
+ add_error("falseSchema", append_keyword: false) { ErrorMessage.false_schema }
333
338
  return Evaluation.invalid
334
339
  end
335
340
  return Evaluation.valid unless schema.is_a?(Hash)
@@ -411,7 +416,7 @@ module Schemurai
411
416
  target = @graph.resolve(node, reference)
412
417
  return target unless reference.to_s.end_with?("#") && target.schema.is_a?(Hash) && target.schema["$recursiveAnchor"] == true
413
418
 
414
- @dynamic_scope.filter_map { |resource| resource.root if resource.root.schema.is_a?(Hash) && resource.root.schema["$recursiveAnchor"] == true }.first || target
419
+ Array(@dynamic_scope).filter_map { |resource| resource.root if resource.root.schema.is_a?(Hash) && resource.root.schema["$recursiveAnchor"] == true }.first || target
415
420
  end
416
421
 
417
422
  private def dynamic_target(node, reference)
@@ -420,7 +425,7 @@ module Schemurai
420
425
  return target if raw_fragment.nil? || raw_fragment.empty? || raw_fragment.start_with?("/")
421
426
  return target unless target.schema.is_a?(Hash) && target.schema["$dynamicAnchor"] == raw_fragment
422
427
 
423
- @dynamic_scope.each do |resource|
428
+ Array(@dynamic_scope).each do |resource|
424
429
  dynamic = @graph.dynamic_anchor(resource, raw_fragment)
425
430
  return dynamic if dynamic
426
431
  end
@@ -438,15 +443,15 @@ module Schemurai
438
443
  types = Array(schema["type"])
439
444
  return if types.any? { |type| type?(value, type) }
440
445
 
441
- add_error("type", "expected #{types.join(" or ")}")
446
+ add_error("type") { ErrorMessage.type(types, value) }
442
447
  end
443
448
 
444
449
  private def check_enum(schema, value)
445
450
  if schema.key?("enum") && !schema["enum"].any? { |candidate| json_equal?(candidate, value) }
446
- add_error("enum", "value is not in enum")
451
+ add_error("enum") { ErrorMessage.enum }
447
452
  end
448
453
  if schema.key?("const") && !json_equal?(schema["const"], value)
449
- add_error("const", "value does not equal const")
454
+ add_error("const") { ErrorMessage.const }
450
455
  end
451
456
  end
452
457
 
@@ -468,7 +473,7 @@ module Schemurai
468
473
  matches << result if result.valid?
469
474
  end
470
475
  if matches.empty?
471
- add_error("anyOf", "no subschema matched")
476
+ add_error("anyOf") { ErrorMessage.any_of }
472
477
  else
473
478
  matches.each { |result| evaluation = evaluation.merge(result) }
474
479
  end
@@ -483,12 +488,12 @@ module Schemurai
483
488
  if matches.length == 1
484
489
  evaluation = evaluation.merge(matches.first)
485
490
  else
486
- add_error("oneOf", "expected exactly one match, got #{matches.length}")
491
+ add_error("oneOf") { ErrorMessage.one_of(matches.length) }
487
492
  end
488
493
  end
489
494
 
490
495
  if schema.key?("not") && trial_at(node.child("not"), value, MISSING_SEGMENT, "not").valid?
491
- add_error("not", "subschema matched")
496
+ add_error("not") { ErrorMessage.not }
492
497
  end
493
498
 
494
499
  if schema.key?("if")
@@ -512,14 +517,14 @@ module Schemurai
512
517
 
513
518
  divisor = decimal(schema["multipleOf"])
514
519
  valid = divisor.positive? && decimal(value).remainder(divisor).zero?
515
- add_error("multipleOf", "number is not a multiple") unless valid
520
+ add_error("multipleOf") { ErrorMessage.multiple_of(schema["multipleOf"]) } unless valid
516
521
  end
517
522
 
518
523
  private def compare(schema, keyword, value)
519
524
  return unless schema.key?(keyword)
520
525
  return if yield(decimal(value), decimal(schema[keyword]))
521
526
 
522
- add_error(keyword, "numeric limit was exceeded")
527
+ add_error(keyword) { ErrorMessage.numeric_limit(keyword, schema[keyword]) }
523
528
  end
524
529
 
525
530
  private def check_string(node, value)
@@ -530,14 +535,14 @@ module Schemurai
530
535
 
531
536
  if schema.key?("pattern")
532
537
  matched = ecma_regexp(schema["pattern"]).match?(value)
533
- add_error("pattern", "string does not match pattern") unless matched
538
+ add_error("pattern") { ErrorMessage.pattern(schema["pattern"]) } unless matched
534
539
  end
535
540
  if format_asserted?(node)
536
- add_error("format", "string is not a valid #{node.format.name}") unless node.format.call(value)
541
+ add_error("format") { ErrorMessage.format(node.format.name) } unless node.format.call(value)
537
542
  end
538
543
  check_content(schema, value) if @validate_content
539
544
  rescue RegexpError
540
- add_error("pattern", "invalid regular expression")
545
+ add_error("pattern") { ErrorMessage.invalid_pattern(schema["pattern"]) }
541
546
  end
542
547
 
543
548
  private def format_asserted?(node)
@@ -554,7 +559,7 @@ module Schemurai
554
559
  duplicate = value.each_with_index.any? do |item, index|
555
560
  value[0...index].any? { |previous| json_equal?(previous, item) }
556
561
  end
557
- add_error("uniqueItems", "array items are not unique") if duplicate
562
+ add_error("uniqueItems") { ErrorMessage.unique_items } if duplicate
558
563
  end
559
564
 
560
565
  prefix_items = schema["prefixItems"]
@@ -596,7 +601,7 @@ module Schemurai
596
601
  minimum = schema.fetch("minContains", 1)
597
602
  maximum = schema.fetch("maxContains", Float::INFINITY)
598
603
  unless matched.length.between?(minimum, maximum)
599
- add_error("contains", "matched #{matched.length} array items")
604
+ add_error("contains") { ErrorMessage.contains(matched.length, minimum, maximum) }
600
605
  end
601
606
  evaluated.concat(matched)
602
607
  end
@@ -619,7 +624,7 @@ module Schemurai
619
624
  limit(schema, "minProperties", value.length) { |actual, expected| actual >= expected }
620
625
 
621
626
  Array(schema["required"]).each do |name|
622
- add_error("required", "required property #{name.inspect} is missing") unless value.key?(name)
627
+ add_error("required") { ErrorMessage.required(name) } unless value.key?(name)
623
628
  end
624
629
 
625
630
  properties = schema.fetch("properties", {})
@@ -653,7 +658,7 @@ module Schemurai
653
658
  next unless value.key?(name)
654
659
  if dependency.is_a?(Array)
655
660
  dependency.each do |required_name|
656
- add_error("dependencies", "property #{required_name.inspect} is required by #{name.inspect}") unless value.key?(required_name)
661
+ add_error("dependencies") { ErrorMessage.dependent_required(name, required_name) } unless value.key?(required_name)
657
662
  end
658
663
  else
659
664
  result = evaluate_at(node.child("dependencies", name), value, MISSING_SEGMENT, "dependencies", name)
@@ -665,7 +670,7 @@ module Schemurai
665
670
  next unless value.key?(name)
666
671
  required_names.each do |required_name|
667
672
  unless value.key?(required_name)
668
- add_error("dependentRequired", "property #{required_name.inspect} is required by #{name.inspect}")
673
+ add_error("dependentRequired") { ErrorMessage.dependent_required(name, required_name) }
669
674
  end
670
675
  end
671
676
  end
@@ -703,7 +708,7 @@ module Schemurai
703
708
  return unless schema.key?(keyword)
704
709
  return if yield(actual, schema[keyword])
705
710
 
706
- add_error(keyword, "size limit was exceeded")
711
+ add_error(keyword) { ErrorMessage.size(keyword, schema[keyword], actual) }
707
712
  end
708
713
 
709
714
  private def type?(value, type)
@@ -799,10 +804,12 @@ module Schemurai
799
804
  JSON.parse(decoded)
800
805
  rescue ArgumentError, JSON::ParserError
801
806
  keyword = (schema["contentEncoding"] == "base64") ? "contentEncoding" : "contentMediaType"
802
- add_error(keyword, "string content is invalid")
807
+ add_error(keyword) do
808
+ (keyword == "contentEncoding") ? ErrorMessage.content_encoding : ErrorMessage.content_media_type
809
+ end
803
810
  end
804
811
 
805
- private def add_error(keyword, message, append_keyword: true)
812
+ private def add_error(keyword, message = nil, append_keyword: true)
806
813
  @error_count += 1
807
814
  if @error_callback
808
815
  schema_keyword = append_keyword ? keyword : MISSING_SEGMENT
@@ -811,7 +818,7 @@ module Schemurai
811
818
  keyword: keyword,
812
819
  instance_path: pointer(@instance_path),
813
820
  schema_path: pointer(@schema_path, schema_keyword),
814
- message: message
821
+ message: message || yield
815
822
  )
816
823
  )
817
824
  end
@@ -13,8 +13,47 @@ module Schemurai
13
13
  "$recursiveAnchor" => true,
14
14
  "type" => ["object", "boolean"],
15
15
  "properties" => {
16
+ "$id" => {"type" => "string"},
17
+ "$schema" => {"type" => "string"},
18
+ "$ref" => {"type" => "string"},
19
+ "$anchor" => {"type" => "string", "pattern" => "^[A-Za-z][-A-Za-z0-9.:_]*$"},
20
+ "$recursiveRef" => {"type" => "string"},
21
+ "$recursiveAnchor" => {"type" => "boolean"},
22
+ "$vocabulary" => {"type" => "object", "additionalProperties" => {"type" => "boolean"}},
23
+ "$comment" => {"type" => "string"},
16
24
  "$defs" => {"type" => "object", "additionalProperties" => {"$recursiveRef" => "#"}},
17
25
  "definitions" => {"type" => "object", "additionalProperties" => {"$recursiveRef" => "#"}},
26
+ "items" => {
27
+ "anyOf" => [
28
+ {"$recursiveRef" => "#"},
29
+ {"type" => "array", "minItems" => 1, "items" => {"$recursiveRef" => "#"}}
30
+ ]
31
+ },
32
+ "additionalItems" => {"$recursiveRef" => "#"},
33
+ "contains" => {"$recursiveRef" => "#"},
34
+ "additionalProperties" => {"$recursiveRef" => "#"},
35
+ "unevaluatedItems" => {"$recursiveRef" => "#"},
36
+ "unevaluatedProperties" => {"$recursiveRef" => "#"},
37
+ "properties" => {"type" => "object", "additionalProperties" => {"$recursiveRef" => "#"}},
38
+ "patternProperties" => {"type" => "object", "additionalProperties" => {"$recursiveRef" => "#"}},
39
+ "dependentSchemas" => {"type" => "object", "additionalProperties" => {"$recursiveRef" => "#"}},
40
+ "dependencies" => {
41
+ "type" => "object",
42
+ "additionalProperties" => {
43
+ "anyOf" => [
44
+ {"$recursiveRef" => "#"},
45
+ {"type" => "array", "items" => {"type" => "string"}, "uniqueItems" => true}
46
+ ]
47
+ }
48
+ },
49
+ "propertyNames" => {"$recursiveRef" => "#"},
50
+ "if" => {"$recursiveRef" => "#"},
51
+ "then" => {"$recursiveRef" => "#"},
52
+ "else" => {"$recursiveRef" => "#"},
53
+ "allOf" => {"type" => "array", "minItems" => 1, "items" => {"$recursiveRef" => "#"}},
54
+ "anyOf" => {"type" => "array", "minItems" => 1, "items" => {"$recursiveRef" => "#"}},
55
+ "oneOf" => {"type" => "array", "minItems" => 1, "items" => {"$recursiveRef" => "#"}},
56
+ "not" => {"$recursiveRef" => "#"},
18
57
  "type" => {
19
58
  "anyOf" => [
20
59
  {"enum" => ["null", "boolean", "object", "array", "number", "integer", "string"]},
@@ -26,20 +65,82 @@ module Schemurai
26
65
  }
27
66
  ]
28
67
  },
68
+ "enum" => {"type" => "array"},
69
+ "multipleOf" => {"type" => "number", "exclusiveMinimum" => 0},
70
+ "maximum" => {"type" => "number"},
71
+ "exclusiveMaximum" => {"type" => "number"},
72
+ "minimum" => {"type" => "number"},
73
+ "exclusiveMinimum" => {"type" => "number"},
29
74
  "minLength" => {"type" => "integer", "minimum" => 0},
30
75
  "maxLength" => {"type" => "integer", "minimum" => 0},
76
+ "pattern" => {"type" => "string"},
31
77
  "minItems" => {"type" => "integer", "minimum" => 0},
32
78
  "maxItems" => {"type" => "integer", "minimum" => 0},
79
+ "uniqueItems" => {"type" => "boolean"},
33
80
  "minContains" => {"type" => "integer", "minimum" => 0},
34
81
  "maxContains" => {"type" => "integer", "minimum" => 0},
35
82
  "minProperties" => {"type" => "integer", "minimum" => 0},
36
- "maxProperties" => {"type" => "integer", "minimum" => 0}
83
+ "maxProperties" => {"type" => "integer", "minimum" => 0},
84
+ "required" => {
85
+ "type" => "array",
86
+ "items" => {"type" => "string"},
87
+ "uniqueItems" => true
88
+ },
89
+ "dependentRequired" => {
90
+ "type" => "object",
91
+ "additionalProperties" => {
92
+ "type" => "array",
93
+ "items" => {"type" => "string"},
94
+ "uniqueItems" => true
95
+ }
96
+ },
97
+ "title" => {"type" => "string"},
98
+ "description" => {"type" => "string"},
99
+ "deprecated" => {"type" => "boolean"},
100
+ "readOnly" => {"type" => "boolean"},
101
+ "writeOnly" => {"type" => "boolean"},
102
+ "examples" => {"type" => "array"},
103
+ "format" => {"type" => "string"},
104
+ "contentEncoding" => {"type" => "string"},
105
+ "contentMediaType" => {"type" => "string"},
106
+ "contentSchema" => {"$recursiveRef" => "#"}
37
107
  }
38
108
  }.freeze
39
109
 
40
110
  MetaSchemas.register(META_SCHEMA_URI, SCHEMA)
41
111
 
42
- private_constant :META_SCHEMA_URI, :SCHEMA
112
+ COMPONENT_KEYWORDS = {
113
+ "core" => %w[
114
+ $id $schema $ref $anchor $recursiveRef $recursiveAnchor $vocabulary $comment $defs
115
+ ],
116
+ "applicator" => %w[
117
+ additionalItems unevaluatedItems items contains additionalProperties
118
+ unevaluatedProperties properties patternProperties dependentSchemas propertyNames
119
+ if then else allOf anyOf oneOf not
120
+ ],
121
+ "validation" => %w[
122
+ type const enum multipleOf maximum exclusiveMaximum minimum exclusiveMinimum
123
+ maxLength minLength pattern maxItems minItems uniqueItems maxContains minContains
124
+ maxProperties minProperties required dependentRequired
125
+ ],
126
+ "meta-data" => %w[title description default deprecated readOnly writeOnly examples],
127
+ "format" => %w[format],
128
+ "content" => %w[contentEncoding contentMediaType contentSchema]
129
+ }.freeze
130
+
131
+ COMPONENT_KEYWORDS.each do |name, keywords|
132
+ uri = "https://json-schema.org/draft/2019-09/meta/#{name}"
133
+ component = {
134
+ "$schema" => META_SCHEMA_URI,
135
+ "$id" => uri,
136
+ "$recursiveAnchor" => true,
137
+ "type" => ["object", "boolean"],
138
+ "properties" => SCHEMA.fetch("properties").slice(*keywords)
139
+ }.freeze
140
+ MetaSchemas.register(uri, component)
141
+ end
142
+
143
+ private_constant :META_SCHEMA_URI, :SCHEMA, :COMPONENT_KEYWORDS
43
144
  end
44
145
  end
45
146
  end