yaml-schema 1.2.0 → 1.3.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 +4 -4
- data/.github/workflows/ci.yml +2 -0
- data/README.md +56 -20
- data/lib/yaml-schema/compiler.rb +538 -0
- data/lib/yaml-schema/runtime.rb +82 -0
- data/lib/yaml-schema.rb +127 -77
- data/test/compiler_test.rb +210 -0
- data/test/validator_test.rb +152 -2
- data/yaml-schema.gemspec +1 -1
- metadata +8 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: df10b8348eda9fb9eb5d36af1119234e00b48a4d1e5ef7f104d9fade49f02493
|
|
4
|
+
data.tar.gz: 0d208c8b1883843586bd30c9de3cb5cafb6a65e8978eb495902d34e5c660462f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f345acb0e18034d923671d42507b3851a9735a41254e9bbd34caf2b6c9f89007e161fdf6b65844bd69528c40488db44ff341894aba73522622beae059d7c56c2
|
|
7
|
+
data.tar.gz: 847b358731e0a6acaa24964bed4d58849ccc3da6e6440b95ff143396e16f79adeadd58b0a1a00f1cbf7d61ce6ec7eaff231a18653a0693170f671d07d3abaf78
|
data/.github/workflows/ci.yml
CHANGED
data/README.md
CHANGED
|
@@ -11,14 +11,14 @@ A library for validating YAML documents against a schema.
|
|
|
11
11
|
### Basic Validation
|
|
12
12
|
|
|
13
13
|
```ruby
|
|
14
|
-
require
|
|
14
|
+
require "yaml-schema"
|
|
15
15
|
|
|
16
16
|
schema = {
|
|
17
|
-
type
|
|
18
|
-
properties
|
|
19
|
-
name
|
|
20
|
-
age
|
|
21
|
-
active
|
|
17
|
+
"type" => "object",
|
|
18
|
+
"properties" => {
|
|
19
|
+
"name" => { "type" => "string" },
|
|
20
|
+
"age" => { "type" => "integer" },
|
|
21
|
+
"active" => { "type" => "boolean" }
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -53,9 +53,9 @@ Specify multiple valid types for a field:
|
|
|
53
53
|
|
|
54
54
|
```ruby
|
|
55
55
|
schema = {
|
|
56
|
-
type
|
|
57
|
-
properties
|
|
58
|
-
value
|
|
56
|
+
"type" => "object",
|
|
57
|
+
"properties" => {
|
|
58
|
+
"value" => { "type" => ["null", "string"] }
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
```
|
|
@@ -64,17 +64,17 @@ schema = {
|
|
|
64
64
|
|
|
65
65
|
```ruby
|
|
66
66
|
schema = {
|
|
67
|
-
type
|
|
68
|
-
properties
|
|
69
|
-
users
|
|
70
|
-
type
|
|
71
|
-
items
|
|
72
|
-
type
|
|
73
|
-
properties
|
|
74
|
-
name
|
|
75
|
-
tags
|
|
76
|
-
type
|
|
77
|
-
items
|
|
67
|
+
"type" => "object",
|
|
68
|
+
"properties" => {
|
|
69
|
+
"users" => {
|
|
70
|
+
"type" => "array",
|
|
71
|
+
"items" => {
|
|
72
|
+
"type" => "object",
|
|
73
|
+
"properties" => {
|
|
74
|
+
"name" => { "type" => "string" },
|
|
75
|
+
"tags" => {
|
|
76
|
+
"type" => "array",
|
|
77
|
+
"items" => { "type" => "string" }
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
}
|
|
@@ -83,6 +83,42 @@ schema = {
|
|
|
83
83
|
}
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
+
### Compiled Validators
|
|
87
|
+
|
|
88
|
+
If you validate many documents against the same schema, you can compile the
|
|
89
|
+
schema into a validator class specialized for it, rather than interpreting
|
|
90
|
+
the schema on every call:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
PersonValidator = YAMLSchema::Compiler.compile(schema)
|
|
94
|
+
|
|
95
|
+
PersonValidator.validate(node) # raises on error, returns true otherwise
|
|
96
|
+
PersonValidator.invalid?(node) # returns the error, or false
|
|
97
|
+
PersonValidator.validate(node, aliases: false)
|
|
98
|
+
PersonValidator.new(node_info).validate(node)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Compiled validators raise the same exceptions with the same messages as
|
|
102
|
+
`YAMLSchema::Validator`. The schema itself is validated at compile time.
|
|
103
|
+
|
|
104
|
+
You can also generate Ruby source and check it into your project:
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
File.write "person_validator.rb",
|
|
108
|
+
YAMLSchema::Compiler.to_ruby(schema, class_name: "PersonValidator")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Pass `standalone: true` to generate a validator that doesn't depend on
|
|
112
|
+
yaml-schema at all. Stand-alone validators define their own exception classes
|
|
113
|
+
(`PersonValidator::UnexpectedType`, etc., all subclasses of
|
|
114
|
+
`PersonValidator::Exception`) instead of raising `YAMLSchema::Validator`
|
|
115
|
+
exceptions:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
File.write "person_validator.rb",
|
|
119
|
+
YAMLSchema::Compiler.to_ruby(schema, class_name: "PersonValidator", standalone: true)
|
|
120
|
+
```
|
|
121
|
+
|
|
86
122
|
## Error Handling
|
|
87
123
|
|
|
88
124
|
Validation errors include detailed path information:
|
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YAMLSchema
|
|
4
|
+
##
|
|
5
|
+
# Base class for validators generated by YAMLSchema::Compiler. Subclasses
|
|
6
|
+
# implement a private +_validate_root+ method that returns +nil+ when the
|
|
7
|
+
# node is valid, or an exception object describing the first error.
|
|
8
|
+
class CompiledValidator
|
|
9
|
+
UnexpectedType = Validator::UnexpectedType
|
|
10
|
+
UnexpectedProperty = Validator::UnexpectedProperty
|
|
11
|
+
UnexpectedTag = Validator::UnexpectedTag
|
|
12
|
+
UnexpectedValue = Validator::UnexpectedValue
|
|
13
|
+
UnexpectedAlias = Validator::UnexpectedAlias
|
|
14
|
+
InvalidString = Validator::InvalidString
|
|
15
|
+
InvalidPattern = Validator::InvalidPattern
|
|
16
|
+
MissingRequiredField = Validator::MissingRequiredField
|
|
17
|
+
|
|
18
|
+
##
|
|
19
|
+
# Validate +node+, raising an exception if it is invalid.
|
|
20
|
+
def self.validate node, aliases: true
|
|
21
|
+
self::INSTANCE.validate node, aliases: aliases
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
##
|
|
25
|
+
# Validate +node+, returning the error if it is invalid, otherwise false.
|
|
26
|
+
def self.invalid? node, aliases: true
|
|
27
|
+
self::INSTANCE.invalid? node, aliases: aliases
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize node_info = Validator::NodeInfo
|
|
31
|
+
@node_info = node_info
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
##
|
|
35
|
+
# Validate +node+, raising an exception if it is invalid.
|
|
36
|
+
def validate node, aliases: true
|
|
37
|
+
err = _validate_root(node, {}, [], aliases)
|
|
38
|
+
raise err if err
|
|
39
|
+
true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
##
|
|
43
|
+
# Validate +node+, returning the error if it is invalid, otherwise false.
|
|
44
|
+
def invalid? node, aliases: true
|
|
45
|
+
_validate_root(node, {}, [], aliases) || false
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
include Runtime
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
##
|
|
52
|
+
# Compiles a schema into a Ruby class specialized for validating that
|
|
53
|
+
# schema. The generated class behaves like YAMLSchema::Validator, but
|
|
54
|
+
# doesn't need to interpret the schema at validation time:
|
|
55
|
+
#
|
|
56
|
+
# validator = YAMLSchema::Compiler.compile(schema)
|
|
57
|
+
# validator.validate(node)
|
|
58
|
+
#
|
|
59
|
+
# The generated source can also be written to a file:
|
|
60
|
+
#
|
|
61
|
+
# File.write "my_validator.rb", YAMLSchema::Compiler.to_ruby(schema, class_name: "MyValidator")
|
|
62
|
+
class Compiler
|
|
63
|
+
EMPTY_SCHEMA = {}.freeze # :nodoc:
|
|
64
|
+
|
|
65
|
+
##
|
|
66
|
+
# Compile +schema+ and return a new validator class.
|
|
67
|
+
def self.compile schema
|
|
68
|
+
klass = Class.new(CompiledValidator)
|
|
69
|
+
klass.class_eval new(schema).body, "(yaml-schema compiled validator)", 1
|
|
70
|
+
klass
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
##
|
|
74
|
+
# Compile +schema+ and return Ruby source code defining a validator
|
|
75
|
+
# class named +class_name+.
|
|
76
|
+
#
|
|
77
|
+
# By default the generated class inherits from
|
|
78
|
+
# YAMLSchema::CompiledValidator and requires this library at runtime.
|
|
79
|
+
# Pass <tt>standalone: true</tt> to generate a class with no dependency
|
|
80
|
+
# on yaml-schema. Stand-alone validators define their own exception
|
|
81
|
+
# classes (e.g. <tt>MyValidator::UnexpectedType</tt>) rather than
|
|
82
|
+
# raising YAMLSchema::Validator exceptions.
|
|
83
|
+
def self.to_ruby schema, class_name:, standalone: false
|
|
84
|
+
body = new(schema).body
|
|
85
|
+
|
|
86
|
+
if standalone
|
|
87
|
+
body = standalone_support + "\n" + body
|
|
88
|
+
header = ""
|
|
89
|
+
superclass = ""
|
|
90
|
+
else
|
|
91
|
+
header = "\nrequire \"yaml-schema\"\n"
|
|
92
|
+
superclass = " < YAMLSchema::CompiledValidator"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
<<~RUBY
|
|
96
|
+
# frozen_string_literal: true
|
|
97
|
+
# Generated by yaml-schema #{VERSION}. Do not edit.
|
|
98
|
+
#{header}
|
|
99
|
+
class #{class_name}#{superclass}
|
|
100
|
+
#{body.gsub(/^(?=.)/, " ")}end
|
|
101
|
+
RUBY
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
STANDALONE_API = <<~'RUBY' # :nodoc:
|
|
105
|
+
class Exception < StandardError; end
|
|
106
|
+
class UnexpectedType < Exception; end
|
|
107
|
+
class UnexpectedProperty < Exception; end
|
|
108
|
+
class UnexpectedTag < Exception; end
|
|
109
|
+
class UnexpectedValue < Exception; end
|
|
110
|
+
class UnexpectedAlias < Exception; end
|
|
111
|
+
class InvalidString < Exception; end
|
|
112
|
+
class InvalidPattern < Exception; end
|
|
113
|
+
class MissingRequiredField < Exception; end
|
|
114
|
+
|
|
115
|
+
module NodeInfo
|
|
116
|
+
def self.read_tag node
|
|
117
|
+
node.tag
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
##
|
|
122
|
+
# Validate +node+, raising an exception if it is invalid.
|
|
123
|
+
def self.validate node, aliases: true
|
|
124
|
+
INSTANCE.validate node, aliases: aliases
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
##
|
|
128
|
+
# Validate +node+, returning the error if it is invalid, otherwise false.
|
|
129
|
+
def self.invalid? node, aliases: true
|
|
130
|
+
INSTANCE.invalid? node, aliases: aliases
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def initialize node_info = NodeInfo
|
|
134
|
+
@node_info = node_info
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
##
|
|
138
|
+
# Validate +node+, raising an exception if it is invalid.
|
|
139
|
+
def validate node, aliases: true
|
|
140
|
+
err = _validate_root(node, {}, [], aliases)
|
|
141
|
+
raise err if err
|
|
142
|
+
true
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
##
|
|
146
|
+
# Validate +node+, returning the error if it is invalid, otherwise false.
|
|
147
|
+
def invalid? node, aliases: true
|
|
148
|
+
_validate_root(node, {}, [], aliases) || false
|
|
149
|
+
end
|
|
150
|
+
RUBY
|
|
151
|
+
|
|
152
|
+
# Source for everything a stand-alone validator needs: exception
|
|
153
|
+
# classes, the public API, and the shared runtime support code.
|
|
154
|
+
def self.standalone_support # :nodoc:
|
|
155
|
+
src = File.read(File.join(__dir__, "runtime.rb"))
|
|
156
|
+
runtime = src[/^[ \t]*# :runtime-begin:\n(.*?)^[ \t]*# :runtime-end:\n/m, 1] or
|
|
157
|
+
raise "runtime markers not found"
|
|
158
|
+
width = runtime.scan(/^[ \t]*(?=\S)/).map(&:length).min
|
|
159
|
+
runtime = runtime.gsub(/^[ \t]{#{width}}/, "")
|
|
160
|
+
|
|
161
|
+
STANDALONE_API + "\npublic\n\n" + runtime + "\npublic\n"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def initialize schema
|
|
165
|
+
Validator::INSTANCE.validate_schema!(schema)
|
|
166
|
+
@schema = schema
|
|
167
|
+
@methods = []
|
|
168
|
+
@constants = []
|
|
169
|
+
@names = {}.compare_by_identity
|
|
170
|
+
@key_names = {}.compare_by_identity # schemas forced to "string" (property names)
|
|
171
|
+
@counter = 0
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
##
|
|
175
|
+
# Ruby source for the body of the validator class.
|
|
176
|
+
def body
|
|
177
|
+
root = schema_method(@schema, "")
|
|
178
|
+
src = +""
|
|
179
|
+
@constants.each { |c| src << c << "\n" }
|
|
180
|
+
src << "\n" unless @constants.empty?
|
|
181
|
+
src << "private\n\n"
|
|
182
|
+
src << "def _validate_root(node, aliases, path, allow_aliases)\n"
|
|
183
|
+
src << " #{root}(node, aliases, path, allow_aliases)\n"
|
|
184
|
+
src << "end\n"
|
|
185
|
+
@methods.each { |m| src << "\n" << m }
|
|
186
|
+
src << "\npublic\n\nINSTANCE = new.freeze\n"
|
|
187
|
+
src
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
private
|
|
191
|
+
|
|
192
|
+
def lit str
|
|
193
|
+
str.dump
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def next_name prefix
|
|
197
|
+
@counter += 1
|
|
198
|
+
"#{prefix}#{@counter}"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def indent code, n
|
|
202
|
+
pad = " " * n
|
|
203
|
+
code.gsub(/^(?=.)/, pad)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Returns the name of a method that fully validates a node against
|
|
207
|
+
# +schema+ (alias resolution, tag lookup, and type checks). Schemas are
|
|
208
|
+
# memoized by identity so shared sub-schemas share a method.
|
|
209
|
+
def schema_method schema, path, forced_type = nil
|
|
210
|
+
memo = forced_type ? @key_names : @names
|
|
211
|
+
return memo[schema] if memo.key?(schema)
|
|
212
|
+
|
|
213
|
+
name = memo[schema] = next_name("_v")
|
|
214
|
+
type = forced_type || schema["type"]
|
|
215
|
+
|
|
216
|
+
src = +""
|
|
217
|
+
src << "# #{path.empty? ? "/" : path}\n"
|
|
218
|
+
src << "def #{name}(node, aliases, path, allow_aliases)\n"
|
|
219
|
+
src << indent(<<~RUBY, 2)
|
|
220
|
+
if node.anchor
|
|
221
|
+
if node.alias?
|
|
222
|
+
raise UnexpectedAlias unless allow_aliases
|
|
223
|
+
node = aliases[node.anchor]
|
|
224
|
+
else
|
|
225
|
+
aliases[node.anchor] = node
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
tag = @node_info.read_tag(node)
|
|
230
|
+
RUBY
|
|
231
|
+
|
|
232
|
+
if Array === type
|
|
233
|
+
# Unions return the first success, or the last failure
|
|
234
|
+
type.each_with_index do |t, i|
|
|
235
|
+
tname = "#{name}_#{t}"
|
|
236
|
+
@methods << "def #{tname}(node, tag, aliases, path, allow_aliases)\n" +
|
|
237
|
+
indent(type_body(t, schema, path), 2) +
|
|
238
|
+
"end\n"
|
|
239
|
+
if i == type.length - 1
|
|
240
|
+
src << " #{tname}(node, tag, aliases, path, allow_aliases)\n"
|
|
241
|
+
else
|
|
242
|
+
src << " return unless #{tname}(node, tag, aliases, path, allow_aliases)\n"
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
else
|
|
246
|
+
src << indent(type_body(type, schema, path), 2)
|
|
247
|
+
end
|
|
248
|
+
src << "end\n"
|
|
249
|
+
|
|
250
|
+
@methods << src
|
|
251
|
+
name
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Code for validating +node+ (already alias-resolved, with +tag+ read)
|
|
255
|
+
# as +type+. Returns an error with +return+, and evaluates to nil on
|
|
256
|
+
# success.
|
|
257
|
+
def type_body type, schema, path
|
|
258
|
+
src = +tag_check(type, schema)
|
|
259
|
+
|
|
260
|
+
case type
|
|
261
|
+
when "object" then src << object_body(schema, path)
|
|
262
|
+
when "array" then src << array_body(schema, path)
|
|
263
|
+
else src << scalar_body(type, schema)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
src << "nil\n"
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def tag_check type, schema
|
|
270
|
+
expected = schema["tag"] if type == "object"
|
|
271
|
+
|
|
272
|
+
if expected
|
|
273
|
+
<<~RUBY
|
|
274
|
+
unless tag == #{lit expected}
|
|
275
|
+
if node.tag
|
|
276
|
+
return make_error(UnexpectedTag, #{lit "expected tag #{expected.dump}, but got "} + node.tag.dump, path)
|
|
277
|
+
else
|
|
278
|
+
return make_error(UnexpectedTag, #{lit "expected tag #{expected.dump}, but none specified"}, path)
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
RUBY
|
|
283
|
+
else
|
|
284
|
+
<<~RUBY
|
|
285
|
+
if tag
|
|
286
|
+
return make_error(UnexpectedTag, "expected no tag, but got " + node.tag.dump, path)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
RUBY
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Validate a child node, pushing +segment+ on to the path for the
|
|
294
|
+
# duration of the call.
|
|
295
|
+
def child_call meth, node_expr, segment_expr
|
|
296
|
+
<<~RUBY
|
|
297
|
+
path.push(#{segment_expr})
|
|
298
|
+
err = #{meth}(#{node_expr}, aliases, path, allow_aliases)
|
|
299
|
+
path.pop
|
|
300
|
+
return err if err
|
|
301
|
+
RUBY
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def object_body schema, path
|
|
305
|
+
src = +<<~RUBY
|
|
306
|
+
unless node.mapping?
|
|
307
|
+
return make_error(UnexpectedType, "expected Mapping, got " + node.class.name.dump, path)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
children = node.children
|
|
311
|
+
len = children.length
|
|
312
|
+
i = 0
|
|
313
|
+
RUBY
|
|
314
|
+
|
|
315
|
+
if schema["properties"]
|
|
316
|
+
properties = schema["properties"]
|
|
317
|
+
key_meth = schema_method(schema["propertyNames"] || EMPTY_SCHEMA, "#{path}/propertyNames", "string")
|
|
318
|
+
|
|
319
|
+
unknown = if schema["additionalProperties"]
|
|
320
|
+
meth = schema_method(schema["additionalProperties"], "#{path}/additionalProperties")
|
|
321
|
+
child_call(meth, "val", "key.value")
|
|
322
|
+
else
|
|
323
|
+
<<~RUBY
|
|
324
|
+
path.push(key.value)
|
|
325
|
+
err = make_error(UnexpectedProperty, "unknown property " + key.value.dump, path)
|
|
326
|
+
path.pop
|
|
327
|
+
return err
|
|
328
|
+
RUBY
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
seen = properties.keys.each_with_index.map { |_, idx| "seen_#{idx}" }
|
|
332
|
+
seen.each { |s| src << "#{s} = false\n" }
|
|
333
|
+
|
|
334
|
+
src << <<~RUBY
|
|
335
|
+
while i < len
|
|
336
|
+
key = children[i]
|
|
337
|
+
val = children[i + 1]
|
|
338
|
+
i += 2
|
|
339
|
+
|
|
340
|
+
err = #{key_meth}(key, aliases, path, allow_aliases)
|
|
341
|
+
return err if err
|
|
342
|
+
|
|
343
|
+
RUBY
|
|
344
|
+
|
|
345
|
+
if properties.empty?
|
|
346
|
+
src << indent(unknown, 2)
|
|
347
|
+
else
|
|
348
|
+
src << " case key.value\n"
|
|
349
|
+
properties.each_with_index do |(name, sub), idx|
|
|
350
|
+
meth = schema_method(sub, "#{path}/properties/#{name}")
|
|
351
|
+
src << " when #{lit name}\n"
|
|
352
|
+
src << " if #{seen[idx]}\n"
|
|
353
|
+
src << indent(unknown, 6)
|
|
354
|
+
src << " else\n"
|
|
355
|
+
src << " #{seen[idx]} = true\n"
|
|
356
|
+
src << indent(child_call(meth, "val", lit(name)), 6)
|
|
357
|
+
src << " end\n"
|
|
358
|
+
end
|
|
359
|
+
src << " else\n"
|
|
360
|
+
src << indent(unknown, 4)
|
|
361
|
+
src << " end\n"
|
|
362
|
+
end
|
|
363
|
+
src << "end\n"
|
|
364
|
+
|
|
365
|
+
required = Array(schema["required"])
|
|
366
|
+
checks = properties.keys.each_with_index.select { |name, _| required.include?(name) }
|
|
367
|
+
unless checks.empty?
|
|
368
|
+
src << "\nmissing = nil\n"
|
|
369
|
+
checks.each do |name, idx|
|
|
370
|
+
src << "(missing ||= []) << #{lit name.dump} unless #{seen[idx]}\n"
|
|
371
|
+
end
|
|
372
|
+
src << <<~RUBY
|
|
373
|
+
if missing
|
|
374
|
+
return make_error(MissingRequiredField, "missing fields " + missing.join(" "), path)
|
|
375
|
+
end
|
|
376
|
+
RUBY
|
|
377
|
+
end
|
|
378
|
+
else
|
|
379
|
+
key_meth = schema_method(EMPTY_SCHEMA, "#{path}/propertyNames", "string")
|
|
380
|
+
meth = schema_method(schema["items"], "#{path}/items")
|
|
381
|
+
src << <<~RUBY
|
|
382
|
+
while i < len
|
|
383
|
+
key = children[i]
|
|
384
|
+
val = children[i + 1]
|
|
385
|
+
i += 2
|
|
386
|
+
|
|
387
|
+
err = #{key_meth}(key, aliases, path, allow_aliases)
|
|
388
|
+
return err if err
|
|
389
|
+
|
|
390
|
+
#{indent(child_call(meth, "val", "key.value"), 2)}end
|
|
391
|
+
RUBY
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
src
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def array_body schema, path
|
|
398
|
+
src = +<<~RUBY
|
|
399
|
+
unless node.sequence?
|
|
400
|
+
return make_error(UnexpectedType, "expected Sequence, got " + node.class.name.dump, path)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
children = node.children
|
|
404
|
+
len = children.length
|
|
405
|
+
|
|
406
|
+
RUBY
|
|
407
|
+
|
|
408
|
+
if max = schema["maxItems"]
|
|
409
|
+
src << <<~RUBY
|
|
410
|
+
if len > #{max}
|
|
411
|
+
return make_error(UnexpectedValue, #{lit "expected maximum #{max} items, but found "} + len.to_s, path)
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
RUBY
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
if min = schema["minItems"]
|
|
418
|
+
src << <<~RUBY
|
|
419
|
+
if len < #{min}
|
|
420
|
+
return make_error(UnexpectedValue, #{lit "expected minimum #{min} items, but found "} + len.to_s, path)
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
RUBY
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
if schema["items"]
|
|
427
|
+
meth = schema_method(schema["items"], "#{path}/items")
|
|
428
|
+
src << <<~RUBY
|
|
429
|
+
i = 0
|
|
430
|
+
while i < len
|
|
431
|
+
#{indent(child_call(meth, "children[i]", "i"), 2)} i += 1
|
|
432
|
+
end
|
|
433
|
+
RUBY
|
|
434
|
+
else
|
|
435
|
+
schema["prefixItems"].each_with_index do |sub, idx|
|
|
436
|
+
meth = schema_method(sub, "#{path}/prefixItems/#{idx}")
|
|
437
|
+
src << "if len > #{idx}\n"
|
|
438
|
+
src << indent(child_call(meth, "children[#{idx}]", idx.to_s), 2)
|
|
439
|
+
src << "end\n"
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
src
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def pattern_constant pattern
|
|
447
|
+
name = "PATTERN_#{@constants.length}"
|
|
448
|
+
@constants << "#{name} = Regexp.new(#{lit pattern.source}, #{pattern.options})"
|
|
449
|
+
name
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def scalar_body type, schema
|
|
453
|
+
src = +<<~RUBY
|
|
454
|
+
unless node.scalar?
|
|
455
|
+
return make_error(UnexpectedType, "expected Scalar, got " + node.class.name.dump, path)
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
value = node.value
|
|
459
|
+
RUBY
|
|
460
|
+
|
|
461
|
+
pattern = schema["pattern"]
|
|
462
|
+
|
|
463
|
+
if type == "string"
|
|
464
|
+
src << <<~RUBY
|
|
465
|
+
unless node.quoted || node.tag == "!str"
|
|
466
|
+
found = extract_type(value)
|
|
467
|
+
if found != :string
|
|
468
|
+
return make_error(UnexpectedValue, "expected string, got " + found.to_s, path)
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
RUBY
|
|
472
|
+
|
|
473
|
+
if max = schema["maxLength"]
|
|
474
|
+
src << <<~RUBY
|
|
475
|
+
if value.bytesize > #{max}
|
|
476
|
+
return make_error(InvalidString, #{lit "expected string length to be <= #{max}"}, path)
|
|
477
|
+
end
|
|
478
|
+
RUBY
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
if min = schema["minLength"]
|
|
482
|
+
src << <<~RUBY
|
|
483
|
+
if value.bytesize < #{min}
|
|
484
|
+
return make_error(InvalidString, #{lit "expected string length to be >= #{min}"}, path)
|
|
485
|
+
end
|
|
486
|
+
RUBY
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
if pattern
|
|
490
|
+
src << <<~RUBY
|
|
491
|
+
unless value.match?(#{pattern_constant(pattern)})
|
|
492
|
+
return make_error(InvalidPattern, "expected string '" + value.dump + #{lit "' to match #{pattern}"}, path)
|
|
493
|
+
end
|
|
494
|
+
RUBY
|
|
495
|
+
end
|
|
496
|
+
else
|
|
497
|
+
src << <<~RUBY
|
|
498
|
+
if node.quoted
|
|
499
|
+
return make_error(UnexpectedValue, #{lit "expected #{type}, got string"}, path)
|
|
500
|
+
end
|
|
501
|
+
RUBY
|
|
502
|
+
|
|
503
|
+
if type == "null"
|
|
504
|
+
src << <<~RUBY
|
|
505
|
+
unless value == ""
|
|
506
|
+
return make_error(UnexpectedValue, "expected empty string, got " + value.dump, path)
|
|
507
|
+
end
|
|
508
|
+
RUBY
|
|
509
|
+
else
|
|
510
|
+
if pattern
|
|
511
|
+
src << <<~RUBY
|
|
512
|
+
unless value.match?(#{pattern_constant(pattern)})
|
|
513
|
+
return make_error(InvalidPattern, "expected '" + value.dump + #{lit "' to match #{pattern}"}, path)
|
|
514
|
+
end
|
|
515
|
+
RUBY
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
if type == "boolean"
|
|
519
|
+
src << <<~RUBY
|
|
520
|
+
unless value == "false" || value == "true"
|
|
521
|
+
return make_error(UnexpectedValue, "expected 'true' or 'false' for boolean", path)
|
|
522
|
+
end
|
|
523
|
+
RUBY
|
|
524
|
+
else
|
|
525
|
+
src << <<~RUBY
|
|
526
|
+
found = extract_type(value)
|
|
527
|
+
unless found == #{type.to_sym.inspect}
|
|
528
|
+
return make_error(UnexpectedValue, #{lit "expected #{type}, got "} + found.to_s, path)
|
|
529
|
+
end
|
|
530
|
+
RUBY
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
src
|
|
536
|
+
end
|
|
537
|
+
end
|
|
538
|
+
end
|