schemurai 1.0.0 → 2.0.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: f2b340649fb4d070d05390ec257487c1f37e45287ef16325f0cfe3e4fb10ae72
4
+ data.tar.gz: e9e93301abe43e21ff297b66a3d9cb4d43376c8ac2b80ee4e650c1705f5dbc99
5
5
  SHA512:
6
- metadata.gz: 5f201d9a44db39000dc36081d9212e221e3e1ba69d4650dcdd929f9709a401cbb6de1412587344246ed736bec0da0e576b19daadc9d91cda5eb4a4d9924d00a8
7
- data.tar.gz: 604dd071010fffc73639338b7f84eefa9328f7c574156cd52077f1d6f6ae9c6ebe0ff1360c2c80c29328659242547b3a3652f0165d926ac944ab1b1fcb025c41
6
+ metadata.gz: a134b68cc4c6a2fc991822d63f841e6bec085a1c33109aca32e75feebd4cd95ca28dad001835591b200515170aa44bbf2c32275a610b2f7acc878d41f8a46bab
7
+ data.tar.gz: f9fd8b9be4432b61d153be039e39e59b0749ada777e15ebbebf007c69d237d356f5b6feea1c790d8e51201e6bedda0cc26e0c533f499e49062e461f78fa03bd3
data/README.md CHANGED
@@ -93,6 +93,11 @@ validator. Repeatedly compiling the same schema object with one registry reuses
93
93
  its compiled schema graph. `Schemurai.validate` and `.valid?` continue
94
94
  to accept raw JSON-like schemas and perform compilation internally.
95
95
 
96
+ Schema inputs are checked recursively before compilation. They must use the
97
+ built-in JSON-shaped Ruby classes described in
98
+ [`docs/compatibility-domain.md`](docs/compatibility-domain.md); rejected values
99
+ are never retained by a registry.
100
+
96
101
  To resolve external references, pass a mapping of URIs to schemas using
97
102
  `schemas:`.
98
103
 
@@ -111,7 +116,7 @@ validation for `contentEncoding` and `contentMediaType` with `content: true`.
111
116
  Enable optional format assertions with `format: true`; support for each format is
112
117
  listed separately below.
113
118
 
114
- ### Thread / Ractor native feature
119
+ ### Thread / Ractor support
115
120
 
116
121
  To share a registry between threads or Ractors, finish registering schemas and
117
122
  make the registry shareable first. This eagerly compiles every registered
@@ -122,7 +127,9 @@ registry = Schemurai::SchemaRegistry.new(
122
127
  schemas: {
123
128
  "https://example.test/positive" => { "type" => "integer", "minimum" => 1 },
124
129
  "https://example.test/value" => { "$ref" => "https://example.test/positive" }
125
- }
130
+ },
131
+ # enable virtual machine backend that is faster for iterative validation.
132
+ backend: :vm
126
133
  )
127
134
  registry.make_shareable
128
135
 
@@ -134,7 +141,46 @@ validator = registry.validator_for("https://example.test/value")
134
141
  `ResolutionError` if a reference cannot be resolved. After it returns,
135
142
  `validator_for` is read-only and may be called concurrently, while `compile` is
136
143
  no longer available. A `Validator` contains per-validation mutable state and
137
- must not be shared between threads or Ractors.
144
+ must not be shared between threads or Ractors. With the VM backend, the registry
145
+ compiles and shares its bytecode before this transition, so those validators do
146
+ not compile separate instruction streams in each Ractor.
147
+
148
+ ### Meta-schema validation
149
+
150
+ Meta-schema validation is opt-in so ordinary compilation does not pay its
151
+ additional time and memory cost. Set `validate_schema: true` when creating a
152
+ `SchemaRegistry` to validate every schema registered through `schemas:` and
153
+ every schema passed to `compile`. The convenience `compile`, `validate`, and
154
+ `valid?` methods accept the same option and apply it to their internal registry.
155
+ Schemas without `$schema` use the Draft 7 meta-schema. An invalid schema raises
156
+ `Schemurai::InvalidSchemaError`, whose `result` contains the detailed validation
157
+ errors.
158
+
159
+ ```ruby
160
+ Schemurai.compile(schema, validate_schema: true)
161
+
162
+ registry = Schemurai::SchemaRegistry.new(
163
+ schemas: external_schemas,
164
+ validate_schema: true
165
+ )
166
+ validator = registry.compile(schema)
167
+
168
+ result = Schemurai.validate_schema(schema)
169
+ result.valid? # whether the schema itself is valid
170
+ ```
171
+
172
+ `SchemaRegistry#validate_schema` and `#valid_schema?` perform explicit checks
173
+ regardless of the registry option and reuse meta-schema graph data within that
174
+ registry. The stateful validator used for a check is temporary; no global
175
+ validator is retained.
176
+
177
+ ### Backend selection
178
+
179
+ `Schemurai.backend`, `SchemaRegistry#backend`, and `Validator#backend` expose
180
+ the actual backend. Pass `backend: :ruby` to force the Ruby oracle or
181
+ `backend: :vm` to ahead-of-time compile schemas for the validator virtual machine.
182
+ Schema graph construction remains shared Ruby infrastructure. Environment-based selection
183
+ is documented in [`docs/backend-selection.md`](docs/backend-selection.md).
138
184
 
139
185
  ## JSON Schema conformance
140
186
 
@@ -195,6 +241,16 @@ Run the test suite with:
195
241
  bundle exec rspec
196
242
  ```
197
243
 
244
+ Verify the reviewed official-suite classifications with:
245
+
246
+ ```sh
247
+ ruby script/oracle-cases --summary
248
+ ```
249
+
250
+ The serialized oracle tools are `script/oracle-runner` and
251
+ `script/oracle-compare`. Ruby interpreter and YJIT baseline instructions are in
252
+ [`benchmark/baselines/README.md`](benchmark/baselines/README.md).
253
+
198
254
  Run the linter with:
199
255
 
200
256
  ```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
@@ -9,6 +9,8 @@ module Schemurai
9
9
  class Evaluator
10
10
  MISSING_SEGMENT = Object.new.freeze
11
11
 
12
+ def backend = :ruby
13
+
12
14
  def initialize(graph, root, content: false, format: false)
13
15
  @validate_content = content
14
16
  @validate_format = format
@@ -28,6 +30,7 @@ module Schemurai
28
30
  @error_callback = nil
29
31
  @error_count = 0
30
32
  @track_dynamic_scope = @graph.dynamic_scope?
33
+ @dynamic_scope = nil
31
34
  @instance_path = nil
32
35
  @schema_path = nil
33
36
  evaluate_valid(@root, instance)
@@ -37,6 +40,7 @@ module Schemurai
37
40
  @error_callback = callback
38
41
  @error_count = 0
39
42
  @track_dynamic_scope = @graph.dynamic_scope?
43
+ @dynamic_scope = nil
40
44
  @instance_path = []
41
45
  @schema_path = []
42
46
  evaluate(@root, instance)
@@ -411,7 +415,7 @@ module Schemurai
411
415
  target = @graph.resolve(node, reference)
412
416
  return target unless reference.to_s.end_with?("#") && target.schema.is_a?(Hash) && target.schema["$recursiveAnchor"] == true
413
417
 
414
- @dynamic_scope.filter_map { |resource| resource.root if resource.root.schema.is_a?(Hash) && resource.root.schema["$recursiveAnchor"] == true }.first || target
418
+ Array(@dynamic_scope).filter_map { |resource| resource.root if resource.root.schema.is_a?(Hash) && resource.root.schema["$recursiveAnchor"] == true }.first || target
415
419
  end
416
420
 
417
421
  private def dynamic_target(node, reference)
@@ -420,7 +424,7 @@ module Schemurai
420
424
  return target if raw_fragment.nil? || raw_fragment.empty? || raw_fragment.start_with?("/")
421
425
  return target unless target.schema.is_a?(Hash) && target.schema["$dynamicAnchor"] == raw_fragment
422
426
 
423
- @dynamic_scope.each do |resource|
427
+ Array(@dynamic_scope).each do |resource|
424
428
  dynamic = @graph.dynamic_anchor(resource, raw_fragment)
425
429
  return dynamic if dynamic
426
430
  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
@@ -13,8 +13,46 @@ module Schemurai
13
13
  "$dynamicAnchor" => "meta",
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
+ "$dynamicRef" => {"type" => "string"},
21
+ "$dynamicAnchor" => {"type" => "string", "pattern" => "^[A-Za-z_][-A-Za-z0-9._]*$"},
22
+ "$recursiveRef" => {"type" => "string"},
23
+ "$recursiveAnchor" => {"type" => "string", "pattern" => "^[A-Za-z_][-A-Za-z0-9._]*$"},
24
+ "$vocabulary" => {"type" => "object", "additionalProperties" => {"type" => "boolean"}},
25
+ "$comment" => {"type" => "string"},
16
26
  "$defs" => {"type" => "object", "additionalProperties" => {"$dynamicRef" => "#meta"}},
17
27
  "definitions" => {"type" => "object", "additionalProperties" => {"$dynamicRef" => "#meta"}},
28
+ "prefixItems" => {
29
+ "type" => "array",
30
+ "minItems" => 1,
31
+ "items" => {"$dynamicRef" => "#meta"}
32
+ },
33
+ "items" => {"$dynamicRef" => "#meta"},
34
+ "contains" => {"$dynamicRef" => "#meta"},
35
+ "additionalProperties" => {"$dynamicRef" => "#meta"},
36
+ "properties" => {"type" => "object", "additionalProperties" => {"$dynamicRef" => "#meta"}},
37
+ "patternProperties" => {"type" => "object", "additionalProperties" => {"$dynamicRef" => "#meta"}},
38
+ "dependentSchemas" => {"type" => "object", "additionalProperties" => {"$dynamicRef" => "#meta"}},
39
+ "dependencies" => {
40
+ "type" => "object",
41
+ "additionalProperties" => {
42
+ "anyOf" => [
43
+ {"$dynamicRef" => "#meta"},
44
+ {"type" => "array", "items" => {"type" => "string"}, "uniqueItems" => true}
45
+ ]
46
+ }
47
+ },
48
+ "propertyNames" => {"$dynamicRef" => "#meta"},
49
+ "if" => {"$dynamicRef" => "#meta"},
50
+ "then" => {"$dynamicRef" => "#meta"},
51
+ "else" => {"$dynamicRef" => "#meta"},
52
+ "allOf" => {"type" => "array", "minItems" => 1, "items" => {"$dynamicRef" => "#meta"}},
53
+ "anyOf" => {"type" => "array", "minItems" => 1, "items" => {"$dynamicRef" => "#meta"}},
54
+ "oneOf" => {"type" => "array", "minItems" => 1, "items" => {"$dynamicRef" => "#meta"}},
55
+ "not" => {"$dynamicRef" => "#meta"},
18
56
  "type" => {
19
57
  "anyOf" => [
20
58
  {"enum" => ["null", "boolean", "object", "array", "number", "integer", "string"]},
@@ -26,20 +64,83 @@ module Schemurai
26
64
  }
27
65
  ]
28
66
  },
67
+ "enum" => {"type" => "array"},
68
+ "multipleOf" => {"type" => "number", "exclusiveMinimum" => 0},
69
+ "maximum" => {"type" => "number"},
70
+ "exclusiveMaximum" => {"type" => "number"},
71
+ "minimum" => {"type" => "number"},
72
+ "exclusiveMinimum" => {"type" => "number"},
29
73
  "minLength" => {"type" => "integer", "minimum" => 0},
30
74
  "maxLength" => {"type" => "integer", "minimum" => 0},
75
+ "pattern" => {"type" => "string"},
31
76
  "minItems" => {"type" => "integer", "minimum" => 0},
32
77
  "maxItems" => {"type" => "integer", "minimum" => 0},
78
+ "uniqueItems" => {"type" => "boolean"},
33
79
  "minContains" => {"type" => "integer", "minimum" => 0},
34
80
  "maxContains" => {"type" => "integer", "minimum" => 0},
35
81
  "minProperties" => {"type" => "integer", "minimum" => 0},
36
- "maxProperties" => {"type" => "integer", "minimum" => 0}
82
+ "maxProperties" => {"type" => "integer", "minimum" => 0},
83
+ "required" => {
84
+ "type" => "array",
85
+ "items" => {"type" => "string"},
86
+ "uniqueItems" => true
87
+ },
88
+ "dependentRequired" => {
89
+ "type" => "object",
90
+ "additionalProperties" => {
91
+ "type" => "array",
92
+ "items" => {"type" => "string"},
93
+ "uniqueItems" => true
94
+ }
95
+ },
96
+ "title" => {"type" => "string"},
97
+ "description" => {"type" => "string"},
98
+ "deprecated" => {"type" => "boolean"},
99
+ "readOnly" => {"type" => "boolean"},
100
+ "writeOnly" => {"type" => "boolean"},
101
+ "examples" => {"type" => "array"},
102
+ "format" => {"type" => "string"},
103
+ "contentEncoding" => {"type" => "string"},
104
+ "contentMediaType" => {"type" => "string"},
105
+ "contentSchema" => {"$dynamicRef" => "#meta"}
37
106
  }
38
107
  }.freeze
39
108
 
40
109
  MetaSchemas.register(META_SCHEMA_URI, SCHEMA)
41
110
 
42
- private_constant :META_SCHEMA_URI, :SCHEMA
111
+ COMPONENT_KEYWORDS = {
112
+ "core" => %w[
113
+ $id $schema $ref $anchor $dynamicRef $dynamicAnchor $vocabulary $comment $defs
114
+ ],
115
+ "applicator" => %w[
116
+ prefixItems items contains additionalProperties properties patternProperties
117
+ dependentSchemas propertyNames if then else allOf anyOf oneOf not
118
+ ],
119
+ "unevaluated" => %w[unevaluatedItems unevaluatedProperties],
120
+ "validation" => %w[
121
+ type const enum multipleOf maximum exclusiveMaximum minimum exclusiveMinimum
122
+ maxLength minLength pattern maxItems minItems uniqueItems maxContains minContains
123
+ maxProperties minProperties required dependentRequired
124
+ ],
125
+ "meta-data" => %w[title description default deprecated readOnly writeOnly examples],
126
+ "format-annotation" => %w[format],
127
+ "format-assertion" => %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/2020-12/meta/#{name}"
133
+ component = {
134
+ "$schema" => META_SCHEMA_URI,
135
+ "$id" => uri,
136
+ "$dynamicAnchor" => "meta",
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
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Schemurai
4
+ module Internal
5
+ module SchemaDomain
6
+ module_function def validate!(schema, label: "schema")
7
+ unless schema.equal?(true) || schema.equal?(false) || schema.instance_of?(Hash)
8
+ invalid!(label, "must be a boolean or an object")
9
+ end
10
+
11
+ validate_value!(schema, label, {})
12
+ schema
13
+ end
14
+
15
+ module_function def validate_registry!(schemas)
16
+ invalid!("schemas", "must be an object") unless schemas.instance_of?(Hash)
17
+
18
+ schemas.each do |uri, schema|
19
+ invalid!("schemas", "contains a non-string URI key") unless uri.instance_of?(String)
20
+
21
+ validate!(schema, label: "schemas[#{uri.inspect}]")
22
+ end
23
+ schemas
24
+ end
25
+
26
+ module_function def validate_value!(value, path, ancestors)
27
+ case value
28
+ when nil, true, false
29
+ nil
30
+ when String, Integer
31
+ invalid!(path, "uses a subclass of #{value.class.superclass}") unless supported_exact_scalar?(value)
32
+ when Float
33
+ invalid!(path, "must contain only finite numbers") unless value.finite?
34
+ invalid!(path, "uses a Float subclass") unless value.instance_of?(Float)
35
+ when Array
36
+ invalid!(path, "uses an Array subclass") unless value.instance_of?(Array)
37
+ descend!(value, path, ancestors) do
38
+ value.each_with_index { |item, index| validate_value!(item, pointer(path, index), ancestors) }
39
+ end
40
+ when Hash
41
+ invalid!(path, "uses a Hash subclass") unless value.instance_of?(Hash)
42
+ descend!(value, path, ancestors) do
43
+ value.each do |key, item|
44
+ invalid!(path, "contains a non-string object key") unless key.instance_of?(String)
45
+
46
+ validate_value!(item, pointer(path, key), ancestors)
47
+ end
48
+ end
49
+ else
50
+ invalid!(path, "contains unsupported #{value.class}")
51
+ end
52
+ end
53
+ private_class_method :validate_value!
54
+
55
+ module_function def supported_exact_scalar?(value)
56
+ value.instance_of?(String) || value.instance_of?(Integer)
57
+ end
58
+ private_class_method :supported_exact_scalar?
59
+
60
+ module_function def descend!(value, path, ancestors)
61
+ invalid!(path, "contains a recursive container") if ancestors.key?(value.object_id)
62
+
63
+ ancestors[value.object_id] = true
64
+ yield
65
+ ensure
66
+ ancestors.delete(value.object_id)
67
+ end
68
+ private_class_method :descend!
69
+
70
+ module_function def pointer(path, segment)
71
+ escaped = segment.to_s.gsub("~", "~0").gsub("/", "~1")
72
+ "#{path}/#{escaped}"
73
+ end
74
+ private_class_method :pointer
75
+
76
+ module_function def invalid!(path, reason)
77
+ raise Error, "invalid JSON-shaped #{path}: #{reason}"
78
+ end
79
+ private_class_method :invalid!
80
+ end
81
+ end
82
+ end
@@ -8,6 +8,7 @@ require_relative "meta_schemas/draft7"
8
8
  require_relative "meta_schemas/draft2019_09"
9
9
  require_relative "meta_schemas/draft2020_12"
10
10
  require_relative "schema_node"
11
+ require_relative "schema_domain"
11
12
 
12
13
  module Schemurai
13
14
  module Internal
@@ -43,9 +44,11 @@ module Schemurai
43
44
  attr_reader :resources, :uri_registry, :nodes
44
45
 
45
46
  def initialize(schemas: {}, dialect: Dialect.resolve)
47
+ SchemaDomain.validate_registry!(schemas)
46
48
  @external_schemas = schemas.dup
47
49
  @indexed_external_schemas = nil
48
50
  @compiled_roots = {}.compare_by_identity
51
+ @meta_schema_roots = {}
49
52
  @resources = {}
50
53
  @uri_registry = {}
51
54
  @nodes = []
@@ -58,10 +61,10 @@ module Schemurai
58
61
  # evaluators sharing this graph may reach the dynamic reference.
59
62
  @dynamic_scope = !schemas.empty?
60
63
  @default_dialect = dialect
61
- @shareable = false
62
64
  end
63
65
 
64
66
  def compile(schema, base_uri: nil, dialect: @default_dialect)
67
+ SchemaDomain.validate!(schema)
65
68
  roots = @compiled_roots.fetch(schema) { @compiled_roots[schema] = {} }
66
69
  cache_key = [base_uri.to_s, dialect]
67
70
  roots.fetch(cache_key) do
@@ -72,12 +75,24 @@ module Schemurai
72
75
  end
73
76
  end
74
77
 
78
+ def meta_schema_root(schema)
79
+ SchemaDomain.validate!(schema)
80
+ uri = if schema.is_a?(Hash) && schema.key?("$schema")
81
+ schema["$schema"].to_s
82
+ else
83
+ @default_dialect.uri
84
+ end
85
+ root = (@meta_schema_roots[uri] ||= compile({"$ref" => uri}))
86
+ resolve(root, uri)
87
+ root
88
+ end
89
+
75
90
  def resolve(node, reference)
76
91
  if (resolved = @resolved_refs&.[](node))&.key?(reference)
77
92
  return resolved[reference]
78
93
  end
79
94
 
80
- if @shareable
95
+ if Ractor.shareable?(self)
81
96
  raise ResolutionError, "reference #{reference.inspect} was not resolved before sharing"
82
97
  end
83
98
 
@@ -94,7 +109,7 @@ module Schemurai
94
109
  end
95
110
 
96
111
  def make_shareable
97
- return self if @shareable
112
+ return self if Ractor.shareable?(self)
98
113
 
99
114
  @external_schemas.each_key do |uri|
100
115
  index_external(strip_fragment(uri.to_s), @default_dialect)
@@ -113,7 +128,6 @@ module Schemurai
113
128
  end
114
129
 
115
130
  @resolved_refs ||= {}
116
- @shareable = true
117
131
  Ractor.make_shareable(self)
118
132
  end
119
133
 
@@ -336,9 +350,10 @@ module Schemurai
336
350
  private def index_external(document_uri, fallback_dialect)
337
351
  return if @indexed_external_schemas&.[](document_uri)
338
352
 
339
- if (dialect = Dialect.resolve(document_uri)) && (meta_schema = MetaSchemas.resolve(document_uri))
353
+ if (meta_schema = MetaSchemas.resolve(document_uri))
354
+ dialect = Dialect.resolve(meta_schema["$schema"]) || fallback_dialect
340
355
  compile_atomically do |changes|
341
- compile_document(meta_schema, dialect.uri, dialect, changes)
356
+ compile_document(meta_schema, document_uri, dialect, changes)
342
357
  end
343
358
  (@indexed_external_schemas ||= {})[document_uri] = true
344
359
  return
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Schemurai
4
- VERSION = "1.0.0"
4
+ # The installed Schemurai version.
5
+ VERSION = "2.0.0"
5
6
  end