schemurai 1.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +206 -0
- data/lib/schemurai/dialect.rb +118 -0
- data/lib/schemurai/dialect_keywords.rb +91 -0
- data/lib/schemurai/dialects/draft2019_09.rb +27 -0
- data/lib/schemurai/dialects/draft2020_12.rb +27 -0
- data/lib/schemurai/dialects/draft7.rb +27 -0
- data/lib/schemurai/evaluation.rb +46 -0
- data/lib/schemurai/evaluator.rb +857 -0
- data/lib/schemurai/formats.rb +403 -0
- data/lib/schemurai/meta_schemas/draft2019_09.rb +48 -0
- data/lib/schemurai/meta_schemas/draft2020_12.rb +48 -0
- data/lib/schemurai/meta_schemas/draft7.rb +121 -0
- data/lib/schemurai/meta_schemas.rb +28 -0
- data/lib/schemurai/schema_graph.rb +481 -0
- data/lib/schemurai/schema_node.rb +55 -0
- data/lib/schemurai/version.rb +5 -0
- data/lib/schemurai.rb +110 -0
- metadata +76 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6c7e69c0dbcafac0bb00bc78aa315932cfd59a141d0727e1a6e4366f80dc416b
|
|
4
|
+
data.tar.gz: 1ddb9269a448dc6c088a7bf22f341e064c2bb3cd0712da6750fc29545379b268
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5f201d9a44db39000dc36081d9212e221e3e1ba69d4650dcdd929f9709a401cbb6de1412587344246ed736bec0da0e576b19daadc9d91cda5eb4a4d9924d00a8
|
|
7
|
+
data.tar.gz: 604dd071010fffc73639338b7f84eefa9328f7c574156cd52077f1d6f6ae9c6ebe0ff1360c2c80c29328659242547b3a3652f0165d926ac944ab1b1fcb025c41
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kuya Kohara
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Schemurai
|
|
2
|
+
|
|
3
|
+
A small, light-weight-dependency JSON Schema validator for Ruby supporting Draft 7,
|
|
4
|
+
Draft 2019-09, and Draft 2020-12. It covers the required cases in the official
|
|
5
|
+
JSON-Schema-Test-Suite, as well as the applicable optional tests for numeric
|
|
6
|
+
precision, ECMA-262 regular expressions, content validation, anchors, and dynamic
|
|
7
|
+
references. The dialect is selected from the schema's `$schema` URI; schemas that
|
|
8
|
+
omit `$schema` use Draft 7 for compatibility.
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
require "schemurai"
|
|
12
|
+
|
|
13
|
+
schema = {
|
|
14
|
+
"$schema" => "https://json-schema.org/draft/2020-12/schema",
|
|
15
|
+
"type" => "object",
|
|
16
|
+
"properties" => {
|
|
17
|
+
"order_id" => { "type" => "string", "pattern" => "^ord_[0-9]{6}$" },
|
|
18
|
+
"customer" => { "$ref" => "#/$defs/customer" },
|
|
19
|
+
"items" => {
|
|
20
|
+
"type" => "array",
|
|
21
|
+
"minItems" => 1,
|
|
22
|
+
"items" => { "$ref" => "#/$defs/line_item" }
|
|
23
|
+
},
|
|
24
|
+
"created_at" => { "type" => "string", "format" => "date-time" }
|
|
25
|
+
},
|
|
26
|
+
"required" => %w[order_id customer items created_at],
|
|
27
|
+
"additionalProperties" => false,
|
|
28
|
+
"$defs" => {
|
|
29
|
+
"customer" => {
|
|
30
|
+
"type" => "object",
|
|
31
|
+
"properties" => {
|
|
32
|
+
"id" => { "type" => "integer", "minimum" => 1 },
|
|
33
|
+
"name" => { "type" => "string", "minLength" => 1 },
|
|
34
|
+
"tier" => { "enum" => %w[standard premium] }
|
|
35
|
+
},
|
|
36
|
+
"required" => %w[id name],
|
|
37
|
+
"additionalProperties" => false
|
|
38
|
+
},
|
|
39
|
+
"line_item" => {
|
|
40
|
+
"type" => "object",
|
|
41
|
+
"properties" => {
|
|
42
|
+
"sku" => { "type" => "string", "pattern" => "^SKU-[A-Z0-9]{8}$" },
|
|
43
|
+
"quantity" => { "type" => "integer", "minimum" => 1 },
|
|
44
|
+
"unit_price" => { "type" => "number", "minimum" => 0 }
|
|
45
|
+
},
|
|
46
|
+
"required" => %w[sku quantity unit_price],
|
|
47
|
+
"additionalProperties" => false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
order = {
|
|
53
|
+
"order_id" => "ord_123456",
|
|
54
|
+
"customer" => { "id" => 42, "name" => "Ada Lovelace", "tier" => "premium" },
|
|
55
|
+
"items" => [
|
|
56
|
+
{ "sku" => "SKU-ABC12345", "quantity" => 2, "unit_price" => 19.95 }
|
|
57
|
+
],
|
|
58
|
+
"created_at" => "2026-08-31T10:15:00+09:00"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
validator = Schemurai.compile(schema, format: true)
|
|
62
|
+
validator.valid?(order) # => true
|
|
63
|
+
|
|
64
|
+
invalid_order = order.merge(
|
|
65
|
+
"items" => [order.fetch("items").first.merge("quantity" => 0)]
|
|
66
|
+
)
|
|
67
|
+
result = validator.validate(invalid_order)
|
|
68
|
+
result.valid? # => false
|
|
69
|
+
result.errors.each do |error|
|
|
70
|
+
puts "#{error.instance_path}: #{error.message}"
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
For repeated validation, compile the schema once and reuse the validator. A
|
|
75
|
+
schema registry owns the compiled resource graph, so schemas compiled by the
|
|
76
|
+
same registry also share their compiled external references.
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
registry = Schemurai::SchemaRegistry.new(
|
|
80
|
+
schemas: {
|
|
81
|
+
"https://example.test/positive" => { "type" => "integer", "minimum" => 1 }
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
validator = registry.compile({"$ref" => "https://example.test/positive"})
|
|
85
|
+
|
|
86
|
+
validator.valid?(1) # => true
|
|
87
|
+
validator.valid?(0) # => false
|
|
88
|
+
validator.validate(0).errors # detailed errors, without recompiling the schema
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`Schemurai.compile` is a convenience for compiling a standalone
|
|
92
|
+
validator. Repeatedly compiling the same schema object with one registry reuses
|
|
93
|
+
its compiled schema graph. `Schemurai.validate` and `.valid?` continue
|
|
94
|
+
to accept raw JSON-like schemas and perform compilation internally.
|
|
95
|
+
|
|
96
|
+
To resolve external references, pass a mapping of URIs to schemas using
|
|
97
|
+
`schemas:`.
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
Schemurai.valid?(
|
|
101
|
+
{ "$ref" => "https://example.test/positive" },
|
|
102
|
+
3,
|
|
103
|
+
schemas: { "https://example.test/positive" => { "type" => "integer", "minimum" => 1 } }
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Draft 2019-09 and Draft 2020-12 support their dialect-specific keywords, including
|
|
108
|
+
`$recursiveRef` / `$dynamicRef`, `$defs`, `dependentSchemas`, `dependentRequired`,
|
|
109
|
+
`minContains`, `maxContains`, and the `unevaluated*` applicators. Enable optional
|
|
110
|
+
validation for `contentEncoding` and `contentMediaType` with `content: true`.
|
|
111
|
+
Enable optional format assertions with `format: true`; support for each format is
|
|
112
|
+
listed separately below.
|
|
113
|
+
|
|
114
|
+
### Thread / Ractor native feature
|
|
115
|
+
|
|
116
|
+
To share a registry between threads or Ractors, finish registering schemas and
|
|
117
|
+
make the registry shareable first. This eagerly compiles every registered
|
|
118
|
+
schema, resolves all references, and makes the registry deeply immutable.
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
registry = Schemurai::SchemaRegistry.new(
|
|
122
|
+
schemas: {
|
|
123
|
+
"https://example.test/positive" => { "type" => "integer", "minimum" => 1 },
|
|
124
|
+
"https://example.test/value" => { "$ref" => "https://example.test/positive" }
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
registry.make_shareable
|
|
128
|
+
|
|
129
|
+
# Each thread or Ractor creates and owns its validator.
|
|
130
|
+
validator = registry.validator_for("https://example.test/value")
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`make_shareable` calls `Ractor.make_shareable` internally. It raises a
|
|
134
|
+
`ResolutionError` if a reference cannot be resolved. After it returns,
|
|
135
|
+
`validator_for` is read-only and may be called concurrently, while `compile` is
|
|
136
|
+
no longer available. A `Validator` contains per-validation mutable state and
|
|
137
|
+
must not be shared between threads or Ractors.
|
|
138
|
+
|
|
139
|
+
## JSON Schema conformance
|
|
140
|
+
|
|
141
|
+
| Capability | Draft 7 | Draft 2019-09 | Draft 2020-12 |
|
|
142
|
+
| --- | --- | --- | --- |
|
|
143
|
+
| Required JSON-Schema-Test-Suite cases | Supported | Supported | Supported |
|
|
144
|
+
| Dialect-specific references | `$ref` | `$ref`, `$recursiveRef` | `$ref`, `$dynamicRef` |
|
|
145
|
+
| `unevaluatedItems` / `unevaluatedProperties` | Not applicable | Supported | Supported |
|
|
146
|
+
| `contentEncoding` / `contentMediaType` assertions | Opt-in[^content] | Opt-in[^content] | Opt-in[^content] |
|
|
147
|
+
|
|
148
|
+
The required-suite row covers every required case for the listed dialect in the
|
|
149
|
+
[official JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite).
|
|
150
|
+
The applicable top-level optional cases are also tested, including arbitrary
|
|
151
|
+
precision numbers, ECMA-262 regular expressions, anchors, cross-draft references,
|
|
152
|
+
and dynamic references.
|
|
153
|
+
|
|
154
|
+
### Format assertion support
|
|
155
|
+
|
|
156
|
+
Formats are annotations by default in each standard dialect. Pass `format: true`
|
|
157
|
+
to enable the supported assertions below.[^format]
|
|
158
|
+
|
|
159
|
+
| Format | Assertion support |
|
|
160
|
+
| --- | --- |
|
|
161
|
+
| `date` | Supported |
|
|
162
|
+
| `time` | Supported |
|
|
163
|
+
| `date-time` | Supported |
|
|
164
|
+
| `duration` | Supported |
|
|
165
|
+
| `email` | Not supported |
|
|
166
|
+
| `idn-email` | Not supported |
|
|
167
|
+
| `hostname` | Not supported |
|
|
168
|
+
| `idn-hostname` | Not supported |
|
|
169
|
+
| `ipv4` | Supported |
|
|
170
|
+
| `ipv6` | Supported |
|
|
171
|
+
| `uri` | Not supported |
|
|
172
|
+
| `uri-reference` | Not supported |
|
|
173
|
+
| `iri` | Not supported |
|
|
174
|
+
| `iri-reference` | Not supported |
|
|
175
|
+
| `uuid` | Supported |
|
|
176
|
+
| `uri-template` | Not supported |
|
|
177
|
+
| `json-pointer` | Supported |
|
|
178
|
+
| `relative-json-pointer` | Supported |
|
|
179
|
+
| `regex` | Not supported |
|
|
180
|
+
|
|
181
|
+
Unsupported and unknown formats remain annotations when assertion is enabled by
|
|
182
|
+
the caller.
|
|
183
|
+
|
|
184
|
+
[^content]: Pass `content: true` to assert Base64 `contentEncoding` and JSON
|
|
185
|
+
`contentMediaType`. Other encodings and media types remain annotations.
|
|
186
|
+
[^format]: A custom Draft 2020-12 meta-schema that declares the Format-Assertion
|
|
187
|
+
vocabulary can assert supported formats without the option and rejects
|
|
188
|
+
unsupported formats during schema compilation.
|
|
189
|
+
|
|
190
|
+
## Development
|
|
191
|
+
|
|
192
|
+
Run the test suite with:
|
|
193
|
+
|
|
194
|
+
```sh
|
|
195
|
+
bundle exec rspec
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Run the linter with:
|
|
199
|
+
|
|
200
|
+
```sh
|
|
201
|
+
bundle exec rubocop
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## AI Disclosure
|
|
205
|
+
|
|
206
|
+
Large part of this work is generated by OpenAI Codex.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Schemurai
|
|
4
|
+
module Internal
|
|
5
|
+
class Dialect
|
|
6
|
+
TYPE = 1
|
|
7
|
+
ENUM = 2
|
|
8
|
+
COMBINER = 4
|
|
9
|
+
NUMBER = 8
|
|
10
|
+
STRING = 16
|
|
11
|
+
ARRAY = 32
|
|
12
|
+
OBJECT = 64
|
|
13
|
+
|
|
14
|
+
Keyword = Data.define(:mask, :subschema_shape) do
|
|
15
|
+
def initialize(mask:, subschema_shape: nil)
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
def register(dialect, default: false)
|
|
22
|
+
registry[normalize_uri(dialect.uri)] = dialect
|
|
23
|
+
@default = dialect if default
|
|
24
|
+
dialect
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def resolve(uri = nil)
|
|
28
|
+
return @default if uri.nil?
|
|
29
|
+
|
|
30
|
+
registry[normalize_uri(uri)]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private def registry
|
|
34
|
+
@registry ||= {}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private def normalize_uri(uri)
|
|
38
|
+
uri.to_s.delete_suffix("#")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
attr_reader :name, :uri, :keywords
|
|
43
|
+
|
|
44
|
+
def initialize(name:, uri:, keywords:, ref_siblings:, format_assertion: false)
|
|
45
|
+
@name = name
|
|
46
|
+
@uri = uri
|
|
47
|
+
@keywords = keywords.freeze
|
|
48
|
+
@ref_siblings = ref_siblings
|
|
49
|
+
@format_assertion = format_assertion
|
|
50
|
+
freeze
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def ref_siblings?
|
|
54
|
+
@ref_siblings
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def format_assertion?
|
|
58
|
+
@format_assertion
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def draft7?
|
|
62
|
+
name == :draft7
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def draft2019?
|
|
66
|
+
name == :draft2019_09
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def draft2020?
|
|
70
|
+
name == :draft2020_12
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def keyword_mask(schema)
|
|
74
|
+
schema.each_key.reduce(0) do |mask, keyword|
|
|
75
|
+
mask | (keywords[keyword]&.mask || 0)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def each_subschema(schema)
|
|
80
|
+
return enum_for(__method__, schema) unless block_given?
|
|
81
|
+
return unless schema.is_a?(Hash)
|
|
82
|
+
return if schema.key?("$ref") && !ref_siblings?
|
|
83
|
+
|
|
84
|
+
schema.each do |keyword, value|
|
|
85
|
+
specification = keywords[keyword]
|
|
86
|
+
next unless specification&.subschema_shape
|
|
87
|
+
|
|
88
|
+
case specification.subschema_shape
|
|
89
|
+
when :single
|
|
90
|
+
yield value, [keyword] if schema?(value)
|
|
91
|
+
when :single_or_list
|
|
92
|
+
if value.is_a?(Array)
|
|
93
|
+
value.each_with_index { |child, index| yield child, [keyword, index] if schema?(child) }
|
|
94
|
+
elsif schema?(value)
|
|
95
|
+
yield value, [keyword]
|
|
96
|
+
end
|
|
97
|
+
when :list
|
|
98
|
+
Array(value).each_with_index { |child, index| yield child, [keyword, index] if schema?(child) }
|
|
99
|
+
when :map
|
|
100
|
+
value.each { |name, child| yield child, [keyword, name] if schema?(child) } if value.is_a?(Hash)
|
|
101
|
+
when :dependencies
|
|
102
|
+
if value.is_a?(Hash)
|
|
103
|
+
value.each do |name, child|
|
|
104
|
+
yield child, [keyword, name] if schema?(child)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private def schema?(value)
|
|
112
|
+
value == true || value == false || value.is_a?(Hash)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private_constant :Internal
|
|
118
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "dialect"
|
|
4
|
+
|
|
5
|
+
module Schemurai
|
|
6
|
+
module Internal
|
|
7
|
+
module DialectKeywords
|
|
8
|
+
COMMON = {
|
|
9
|
+
"type" => Dialect::Keyword.new(mask: Dialect::TYPE),
|
|
10
|
+
"enum" => Dialect::Keyword.new(mask: Dialect::ENUM),
|
|
11
|
+
"const" => Dialect::Keyword.new(mask: Dialect::ENUM),
|
|
12
|
+
"allOf" => Dialect::Keyword.new(mask: Dialect::COMBINER, subschema_shape: :list),
|
|
13
|
+
"anyOf" => Dialect::Keyword.new(mask: Dialect::COMBINER, subschema_shape: :list),
|
|
14
|
+
"oneOf" => Dialect::Keyword.new(mask: Dialect::COMBINER, subschema_shape: :list),
|
|
15
|
+
"not" => Dialect::Keyword.new(mask: Dialect::COMBINER, subschema_shape: :single),
|
|
16
|
+
"if" => Dialect::Keyword.new(mask: Dialect::COMBINER, subschema_shape: :single),
|
|
17
|
+
"then" => Dialect::Keyword.new(mask: 0, subschema_shape: :single),
|
|
18
|
+
"else" => Dialect::Keyword.new(mask: 0, subschema_shape: :single),
|
|
19
|
+
"maximum" => Dialect::Keyword.new(mask: Dialect::NUMBER),
|
|
20
|
+
"minimum" => Dialect::Keyword.new(mask: Dialect::NUMBER),
|
|
21
|
+
"exclusiveMaximum" => Dialect::Keyword.new(mask: Dialect::NUMBER),
|
|
22
|
+
"exclusiveMinimum" => Dialect::Keyword.new(mask: Dialect::NUMBER),
|
|
23
|
+
"multipleOf" => Dialect::Keyword.new(mask: Dialect::NUMBER),
|
|
24
|
+
"maxLength" => Dialect::Keyword.new(mask: Dialect::STRING),
|
|
25
|
+
"minLength" => Dialect::Keyword.new(mask: Dialect::STRING),
|
|
26
|
+
"pattern" => Dialect::Keyword.new(mask: Dialect::STRING),
|
|
27
|
+
"contentEncoding" => Dialect::Keyword.new(mask: Dialect::STRING),
|
|
28
|
+
"contentMediaType" => Dialect::Keyword.new(mask: Dialect::STRING),
|
|
29
|
+
"maxItems" => Dialect::Keyword.new(mask: Dialect::ARRAY),
|
|
30
|
+
"minItems" => Dialect::Keyword.new(mask: Dialect::ARRAY),
|
|
31
|
+
"uniqueItems" => Dialect::Keyword.new(mask: Dialect::ARRAY),
|
|
32
|
+
"contains" => Dialect::Keyword.new(mask: Dialect::ARRAY, subschema_shape: :single),
|
|
33
|
+
"maxProperties" => Dialect::Keyword.new(mask: Dialect::OBJECT),
|
|
34
|
+
"minProperties" => Dialect::Keyword.new(mask: Dialect::OBJECT),
|
|
35
|
+
"required" => Dialect::Keyword.new(mask: Dialect::OBJECT),
|
|
36
|
+
"properties" => Dialect::Keyword.new(mask: Dialect::OBJECT, subschema_shape: :map),
|
|
37
|
+
"patternProperties" => Dialect::Keyword.new(mask: Dialect::OBJECT, subschema_shape: :map),
|
|
38
|
+
"additionalProperties" => Dialect::Keyword.new(mask: Dialect::OBJECT, subschema_shape: :single),
|
|
39
|
+
"propertyNames" => Dialect::Keyword.new(mask: Dialect::OBJECT, subschema_shape: :single)
|
|
40
|
+
}.freeze
|
|
41
|
+
|
|
42
|
+
LEGACY = {
|
|
43
|
+
"dependencies" => Dialect::Keyword.new(mask: Dialect::OBJECT, subschema_shape: :dependencies),
|
|
44
|
+
"definitions" => Dialect::Keyword.new(mask: 0, subschema_shape: :map)
|
|
45
|
+
}.freeze
|
|
46
|
+
|
|
47
|
+
MODERN = {
|
|
48
|
+
"$defs" => Dialect::Keyword.new(mask: 0, subschema_shape: :map),
|
|
49
|
+
"$recursiveRef" => Dialect::Keyword.new(mask: Dialect::COMBINER),
|
|
50
|
+
"dependentSchemas" => Dialect::Keyword.new(mask: Dialect::OBJECT, subschema_shape: :map),
|
|
51
|
+
"dependentRequired" => Dialect::Keyword.new(mask: Dialect::OBJECT),
|
|
52
|
+
"minContains" => Dialect::Keyword.new(mask: Dialect::ARRAY),
|
|
53
|
+
"maxContains" => Dialect::Keyword.new(mask: Dialect::ARRAY),
|
|
54
|
+
"unevaluatedItems" => Dialect::Keyword.new(mask: Dialect::ARRAY, subschema_shape: :single),
|
|
55
|
+
"unevaluatedProperties" => Dialect::Keyword.new(mask: Dialect::OBJECT, subschema_shape: :single),
|
|
56
|
+
"contentSchema" => Dialect::Keyword.new(mask: Dialect::STRING, subschema_shape: :single)
|
|
57
|
+
}.freeze
|
|
58
|
+
|
|
59
|
+
private_constant :COMMON, :LEGACY, :MODERN
|
|
60
|
+
|
|
61
|
+
module_function def draft7
|
|
62
|
+
COMMON.merge(
|
|
63
|
+
LEGACY,
|
|
64
|
+
"items" => Dialect::Keyword.new(mask: Dialect::ARRAY, subschema_shape: :single_or_list),
|
|
65
|
+
"additionalItems" => Dialect::Keyword.new(mask: Dialect::ARRAY, subschema_shape: :single)
|
|
66
|
+
).freeze
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
module_function def draft2019_09
|
|
70
|
+
COMMON.merge(
|
|
71
|
+
LEGACY,
|
|
72
|
+
MODERN,
|
|
73
|
+
"items" => Dialect::Keyword.new(mask: Dialect::ARRAY, subschema_shape: :single_or_list),
|
|
74
|
+
"additionalItems" => Dialect::Keyword.new(mask: Dialect::ARRAY, subschema_shape: :single)
|
|
75
|
+
).freeze
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
module_function def draft2020_12
|
|
79
|
+
COMMON.merge(
|
|
80
|
+
LEGACY,
|
|
81
|
+
MODERN,
|
|
82
|
+
"$dynamicRef" => Dialect::Keyword.new(mask: Dialect::COMBINER),
|
|
83
|
+
"prefixItems" => Dialect::Keyword.new(mask: Dialect::ARRAY, subschema_shape: :list),
|
|
84
|
+
"items" => Dialect::Keyword.new(mask: Dialect::ARRAY, subschema_shape: :single)
|
|
85
|
+
).freeze
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private_constant :Internal
|
|
91
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../dialect_keywords"
|
|
4
|
+
|
|
5
|
+
module Schemurai
|
|
6
|
+
module Internal
|
|
7
|
+
module Dialects
|
|
8
|
+
module Draft201909
|
|
9
|
+
META_SCHEMA_URI = "https://json-schema.org/draft/2019-09/schema"
|
|
10
|
+
KEYWORDS = DialectKeywords.draft2019_09
|
|
11
|
+
|
|
12
|
+
DIALECT = Dialect.new(
|
|
13
|
+
name: :draft2019_09,
|
|
14
|
+
uri: META_SCHEMA_URI,
|
|
15
|
+
keywords: KEYWORDS,
|
|
16
|
+
ref_siblings: true
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
Dialect.register(DIALECT)
|
|
20
|
+
|
|
21
|
+
private_constant :META_SCHEMA_URI, :KEYWORDS
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private_constant :Internal
|
|
27
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../dialect_keywords"
|
|
4
|
+
|
|
5
|
+
module Schemurai
|
|
6
|
+
module Internal
|
|
7
|
+
module Dialects
|
|
8
|
+
module Draft202012
|
|
9
|
+
META_SCHEMA_URI = "https://json-schema.org/draft/2020-12/schema"
|
|
10
|
+
KEYWORDS = DialectKeywords.draft2020_12
|
|
11
|
+
|
|
12
|
+
DIALECT = Dialect.new(
|
|
13
|
+
name: :draft2020_12,
|
|
14
|
+
uri: META_SCHEMA_URI,
|
|
15
|
+
keywords: KEYWORDS,
|
|
16
|
+
ref_siblings: true
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
Dialect.register(DIALECT)
|
|
20
|
+
|
|
21
|
+
private_constant :META_SCHEMA_URI, :KEYWORDS
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private_constant :Internal
|
|
27
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../dialect_keywords"
|
|
4
|
+
|
|
5
|
+
module Schemurai
|
|
6
|
+
module Internal
|
|
7
|
+
module Dialects
|
|
8
|
+
module Draft7
|
|
9
|
+
META_SCHEMA_URI = "http://json-schema.org/draft-07/schema"
|
|
10
|
+
KEYWORDS = DialectKeywords.draft7
|
|
11
|
+
|
|
12
|
+
DIALECT = Dialect.new(
|
|
13
|
+
name: :draft7,
|
|
14
|
+
uri: META_SCHEMA_URI,
|
|
15
|
+
keywords: KEYWORDS,
|
|
16
|
+
ref_siblings: false
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
Dialect.register(DIALECT, default: true)
|
|
20
|
+
|
|
21
|
+
private_constant :META_SCHEMA_URI, :KEYWORDS
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private_constant :Internal
|
|
27
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Schemurai
|
|
4
|
+
# Internal result of evaluating one schema location. Draft 7 only needs the
|
|
5
|
+
# validity bit, but newer drafts also propagate the instance locations
|
|
6
|
+
# consumed by applicators for unevaluatedProperties and unevaluatedItems.
|
|
7
|
+
class Evaluation
|
|
8
|
+
EMPTY_LOCATIONS = [].freeze
|
|
9
|
+
|
|
10
|
+
attr_reader :evaluated_properties, :evaluated_items
|
|
11
|
+
|
|
12
|
+
def self.valid(evaluated_properties: EMPTY_LOCATIONS, evaluated_items: EMPTY_LOCATIONS)
|
|
13
|
+
if evaluated_properties.equal?(EMPTY_LOCATIONS) && evaluated_items.equal?(EMPTY_LOCATIONS)
|
|
14
|
+
return @valid ||= new(true, EMPTY_LOCATIONS, EMPTY_LOCATIONS)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
new(true, evaluated_properties, evaluated_items)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.invalid
|
|
21
|
+
@invalid ||= new(false, EMPTY_LOCATIONS, EMPTY_LOCATIONS)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initialize(valid, evaluated_properties, evaluated_items)
|
|
25
|
+
@valid = valid
|
|
26
|
+
@evaluated_properties = evaluated_properties.freeze
|
|
27
|
+
@evaluated_items = evaluated_items.freeze
|
|
28
|
+
freeze
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def valid?
|
|
32
|
+
@valid
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def merge(other)
|
|
36
|
+
return self.class.invalid unless valid? && other.valid?
|
|
37
|
+
|
|
38
|
+
self.class.valid(
|
|
39
|
+
evaluated_properties: (evaluated_properties | other.evaluated_properties),
|
|
40
|
+
evaluated_items: (evaluated_items | other.evaluated_items)
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private_constant :Evaluation
|
|
46
|
+
end
|