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
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YAMLSchema
|
|
4
|
+
##
|
|
5
|
+
# Support code shared by YAMLSchema::Validator and compiled validators.
|
|
6
|
+
#
|
|
7
|
+
# The code between the runtime markers is also copied verbatim into
|
|
8
|
+
# stand-alone validators generated by YAMLSchema::Compiler.to_ruby, so it
|
|
9
|
+
# must not refer to anything else in this library.
|
|
10
|
+
module Runtime
|
|
11
|
+
# :runtime-begin:
|
|
12
|
+
# Taken from http://yaml.org/type/timestamp.html
|
|
13
|
+
TIME = /^-?\d{4}-\d{1,2}-\d{1,2}(?:[Tt]|\s+)\d{1,2}:\d\d:\d\d(?:\.\d*)?(?:\s*(?:Z|[-+]\d{1,2}:?(?:\d\d)?))?$/
|
|
14
|
+
|
|
15
|
+
# Taken from http://yaml.org/type/float.html
|
|
16
|
+
# Base 60, [-+]inf and NaN are handled separately
|
|
17
|
+
FLOAT = /^(?:[-+]?([0-9][0-9_,]*)?\.[0-9]*([eE][-+][0-9]+)?(?# base 10))$/x
|
|
18
|
+
|
|
19
|
+
# Taken from http://yaml.org/type/int.html and modified to ensure at least one numerical symbol exists
|
|
20
|
+
INTEGER_STRICT = /^(?:[-+]?0b[_]*[0-1][0-1_]* (?# base 2)
|
|
21
|
+
|[-+]?0[_]*[0-7][0-7_]* (?# base 8)
|
|
22
|
+
|[-+]?(0|[1-9][0-9_]*) (?# base 10)
|
|
23
|
+
|[-+]?0x[_]*[0-9a-fA-F][0-9a-fA-F_]* (?# base 16))$/x
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# Tokenize +string+ returning the Ruby object
|
|
28
|
+
def extract_type string
|
|
29
|
+
return :null if string.empty?
|
|
30
|
+
# Check for a String type, being careful not to get caught by hash keys, hex values, and
|
|
31
|
+
# special floats (e.g., -.inf).
|
|
32
|
+
if string.match?(%r{^[^\d.:-]?[[:alpha:]_\s!@#$%\^&*(){}<>|/\\~;=]+}) || string.match?(/\n/)
|
|
33
|
+
return :string if string.length > 5
|
|
34
|
+
|
|
35
|
+
if string.match?(/^[^ytonf~]/i)
|
|
36
|
+
:string
|
|
37
|
+
elsif string == '~' || string.match?(/^null$/i)
|
|
38
|
+
:null
|
|
39
|
+
elsif string.match?(/^(yes|true|on)$/i)
|
|
40
|
+
:boolean
|
|
41
|
+
elsif string.match?(/^(no|false|off)$/i)
|
|
42
|
+
:boolean
|
|
43
|
+
else
|
|
44
|
+
:string
|
|
45
|
+
end
|
|
46
|
+
elsif string.match?(TIME)
|
|
47
|
+
:time
|
|
48
|
+
elsif string.match?(/^\d{4}-(?:1[012]|0\d|\d)-(?:[12]\d|3[01]|0\d|\d)$/)
|
|
49
|
+
:date
|
|
50
|
+
elsif string.match?(/^\+?\.inf$/i)
|
|
51
|
+
:float
|
|
52
|
+
elsif string.match?(/^-\.inf$/i)
|
|
53
|
+
:float
|
|
54
|
+
elsif string.match?(/^\.nan$/i)
|
|
55
|
+
:float
|
|
56
|
+
elsif string.match?(/^:./)
|
|
57
|
+
:symbol
|
|
58
|
+
elsif string.match?(/^[-+]?[0-9][0-9_]*(:[0-5]?[0-9]){1,2}$/)
|
|
59
|
+
:sexagesimal
|
|
60
|
+
elsif string.match?(/^[-+]?[0-9][0-9_]*(:[0-5]?[0-9]){1,2}\.[0-9_]*$/)
|
|
61
|
+
:sexagesimal
|
|
62
|
+
elsif string.match?(FLOAT)
|
|
63
|
+
if string.match?(/\A[-+]?\.\Z/)
|
|
64
|
+
:string
|
|
65
|
+
else
|
|
66
|
+
:float
|
|
67
|
+
end
|
|
68
|
+
elsif string.match?(INTEGER_STRICT)
|
|
69
|
+
:integer
|
|
70
|
+
else
|
|
71
|
+
:string
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def make_error klass, msg, path
|
|
76
|
+
ex = klass.new msg + " path: /#{path.join("/")}"
|
|
77
|
+
ex.set_backtrace caller
|
|
78
|
+
ex
|
|
79
|
+
end
|
|
80
|
+
# :runtime-end:
|
|
81
|
+
end
|
|
82
|
+
end
|
data/lib/yaml-schema.rb
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "yaml-schema/runtime"
|
|
4
|
+
|
|
3
5
|
module YAMLSchema
|
|
4
|
-
VERSION = "1.
|
|
6
|
+
VERSION = "1.3.0"
|
|
5
7
|
|
|
6
8
|
class Pointer
|
|
7
9
|
include Enumerable
|
|
@@ -63,6 +65,8 @@ module YAMLSchema
|
|
|
63
65
|
end
|
|
64
66
|
|
|
65
67
|
class Validator
|
|
68
|
+
include Runtime
|
|
69
|
+
|
|
66
70
|
class Exception < StandardError; end
|
|
67
71
|
class UnexpectedType < Exception; end
|
|
68
72
|
class UnexpectedProperty < Exception; end
|
|
@@ -79,27 +83,30 @@ module YAMLSchema
|
|
|
79
83
|
##
|
|
80
84
|
# Given a particular schema, validate that the node conforms to the
|
|
81
85
|
# schema. Raises an exception if it is invalid
|
|
82
|
-
def self.validate
|
|
86
|
+
def self.validate schema, node, aliases: true
|
|
83
87
|
INSTANCE.validate schema, node, aliases: aliases
|
|
84
88
|
end
|
|
85
89
|
|
|
86
90
|
module NodeInfo # :nodoc:
|
|
87
|
-
def self.read_tag
|
|
91
|
+
def self.read_tag node
|
|
88
92
|
node.tag
|
|
89
93
|
end
|
|
90
94
|
end
|
|
91
95
|
|
|
92
|
-
def initialize
|
|
96
|
+
def initialize node_info = NodeInfo
|
|
93
97
|
@node_info = node_info
|
|
94
98
|
end
|
|
95
99
|
|
|
96
100
|
INSTANCE = new.freeze
|
|
97
101
|
|
|
102
|
+
VALID_TYPES = %w[object array string integer float boolean null date time symbol].freeze
|
|
103
|
+
|
|
98
104
|
##
|
|
99
105
|
# Given a particular schema, validate that the node conforms to the
|
|
100
106
|
# schema. Raises an exception if it is invalid
|
|
101
|
-
def validate
|
|
102
|
-
|
|
107
|
+
def validate schema, node, aliases: true
|
|
108
|
+
validate_schema!(schema)
|
|
109
|
+
val = _validate(schema["type"], schema, node, Valid, {}, [], aliases)
|
|
103
110
|
if val.exception
|
|
104
111
|
raise val
|
|
105
112
|
else
|
|
@@ -110,8 +117,9 @@ module YAMLSchema
|
|
|
110
117
|
##
|
|
111
118
|
# Given a particular schema, validate that the node conforms to the
|
|
112
119
|
# schema. Returns an error object if the node is invalid, otherwise false.
|
|
113
|
-
def invalid?
|
|
114
|
-
|
|
120
|
+
def invalid? schema, node, aliases: true
|
|
121
|
+
validate_schema!(schema)
|
|
122
|
+
res = _validate(schema["type"], schema, node, Valid, {}, [], aliases)
|
|
115
123
|
if Valid == res
|
|
116
124
|
false
|
|
117
125
|
else
|
|
@@ -119,15 +127,115 @@ module YAMLSchema
|
|
|
119
127
|
end
|
|
120
128
|
end
|
|
121
129
|
|
|
130
|
+
##
|
|
131
|
+
# Validates a schema definition, raising InvalidSchema if it is malformed.
|
|
132
|
+
def validate_schema! schema, path = ""
|
|
133
|
+
raise InvalidSchema, "schema must be a Hash#{_sp path}" unless Hash === schema
|
|
134
|
+
|
|
135
|
+
type = schema["type"]
|
|
136
|
+
raise InvalidSchema, "schema must have a \"type\" key#{_sp path}" unless type
|
|
137
|
+
|
|
138
|
+
types = Array === type ? type : [type]
|
|
139
|
+
types.each do |t|
|
|
140
|
+
raise InvalidSchema, "type must be a String, got #{t.class}#{_sp path}" unless String === t
|
|
141
|
+
raise InvalidSchema, "unknown type #{t.dump}#{_sp path}" unless VALID_TYPES.include?(t)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
if schema["tag"]
|
|
145
|
+
raise InvalidSchema, "\"tag\" must be a String#{_sp path}" unless String === schema["tag"]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
if schema["pattern"]
|
|
149
|
+
raise InvalidSchema, "\"pattern\" must be a Regexp#{_sp path}" unless Regexp === schema["pattern"]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
types.each do |t|
|
|
153
|
+
case t
|
|
154
|
+
when "object"
|
|
155
|
+
_validate_object_schema(schema, path)
|
|
156
|
+
when "array"
|
|
157
|
+
_validate_array_schema(schema, path)
|
|
158
|
+
else
|
|
159
|
+
_validate_scalar_schema(schema, path)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
122
164
|
private
|
|
123
165
|
|
|
124
|
-
def
|
|
125
|
-
|
|
126
|
-
ex.set_backtrace caller
|
|
127
|
-
ex
|
|
166
|
+
def _sp path
|
|
167
|
+
path.empty? ? "" : " at #{path}"
|
|
128
168
|
end
|
|
129
169
|
|
|
130
|
-
def
|
|
170
|
+
def _validate_object_schema schema, path
|
|
171
|
+
if schema["properties"]
|
|
172
|
+
raise InvalidSchema, "\"properties\" must be a Hash#{_sp path}" unless Hash === schema["properties"]
|
|
173
|
+
schema["properties"].each do |key, sub|
|
|
174
|
+
raise InvalidSchema, "property name must be a String#{_sp path}" unless String === key
|
|
175
|
+
validate_schema!(sub, "#{path}/#{key}")
|
|
176
|
+
end
|
|
177
|
+
elsif schema["items"]
|
|
178
|
+
# items-based object (homogeneous key-value)
|
|
179
|
+
else
|
|
180
|
+
raise InvalidSchema, "objects must specify \"items\" or \"properties\"#{_sp path}"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
if schema["items"] && !schema["properties"]
|
|
184
|
+
raise InvalidSchema, "\"items\" must be a Hash#{_sp path}" unless Hash === schema["items"]
|
|
185
|
+
validate_schema!(schema["items"], "#{path}/items")
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
if schema["additionalProperties"]
|
|
189
|
+
raise InvalidSchema, "\"additionalProperties\" must be a Hash#{_sp path}" unless Hash === schema["additionalProperties"]
|
|
190
|
+
validate_schema!(schema["additionalProperties"], "#{path}/additionalProperties")
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
if schema["propertyNames"]
|
|
194
|
+
raise InvalidSchema, "\"propertyNames\" must be a Hash#{_sp path}" unless Hash === schema["propertyNames"]
|
|
195
|
+
validate_schema!(schema["propertyNames"], "#{path}/propertyNames") if schema["propertyNames"]["type"]
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
if schema["required"]
|
|
199
|
+
raise InvalidSchema, "\"required\" must be an Array#{_sp path}" unless Array === schema["required"]
|
|
200
|
+
schema["required"].each do |r|
|
|
201
|
+
raise InvalidSchema, "\"required\" entries must be Strings#{_sp path}" unless String === r
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def _validate_array_schema schema, path
|
|
207
|
+
if schema["items"]
|
|
208
|
+
raise InvalidSchema, "\"items\" must be a Hash#{_sp path}" unless Hash === schema["items"]
|
|
209
|
+
validate_schema!(schema["items"], "#{path}/items")
|
|
210
|
+
elsif schema["prefixItems"]
|
|
211
|
+
raise InvalidSchema, "\"prefixItems\" must be an Array#{_sp path}" unless Array === schema["prefixItems"]
|
|
212
|
+
schema["prefixItems"].each_with_index do |sub, i|
|
|
213
|
+
validate_schema!(sub, "#{path}/prefixItems/#{i}")
|
|
214
|
+
end
|
|
215
|
+
else
|
|
216
|
+
raise InvalidSchema, "arrays must specify \"items\" or \"prefixItems\"#{_sp path}"
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
if schema["minItems"]
|
|
220
|
+
raise InvalidSchema, "\"minItems\" must be an Integer#{_sp path}" unless Integer === schema["minItems"]
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
if schema["maxItems"]
|
|
224
|
+
raise InvalidSchema, "\"maxItems\" must be an Integer#{_sp path}" unless Integer === schema["maxItems"]
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def _validate_scalar_schema schema, path
|
|
229
|
+
if schema["minLength"]
|
|
230
|
+
raise InvalidSchema, "\"minLength\" must be an Integer#{_sp path}" unless Integer === schema["minLength"]
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
if schema["maxLength"]
|
|
234
|
+
raise InvalidSchema, "\"maxLength\" must be an Integer#{_sp path}" unless Integer === schema["maxLength"]
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def _validate type, schema, node, valid, aliases, path, allow_aliases
|
|
131
239
|
return valid if valid.exception
|
|
132
240
|
|
|
133
241
|
if node.anchor
|
|
@@ -188,7 +296,7 @@ module YAMLSchema
|
|
|
188
296
|
if schema["additionalProperties"]
|
|
189
297
|
schema["additionalProperties"]
|
|
190
298
|
else
|
|
191
|
-
return make_error UnexpectedProperty, "unknown property #{key.value.dump}", path
|
|
299
|
+
return make_error UnexpectedProperty, "unknown property #{key.value.dump}", path + [key.value]
|
|
192
300
|
end
|
|
193
301
|
}
|
|
194
302
|
|
|
@@ -235,7 +343,8 @@ module YAMLSchema
|
|
|
235
343
|
valid = _validate sub_schema["type"], sub_schema, item, valid, aliases, path + [i], allow_aliases
|
|
236
344
|
}
|
|
237
345
|
elsif schema["prefixItems"]
|
|
238
|
-
|
|
346
|
+
# Items beyond the end of prefixItems are not validated
|
|
347
|
+
node.children.first(schema["prefixItems"].length).each_with_index { |item, i|
|
|
239
348
|
sub_schema = schema["prefixItems"][i]
|
|
240
349
|
valid = _validate sub_schema["type"], sub_schema, item, valid, aliases, path + [i], allow_aliases
|
|
241
350
|
}
|
|
@@ -289,7 +398,7 @@ module YAMLSchema
|
|
|
289
398
|
when "integer", "float", "time", "date", "symbol"
|
|
290
399
|
found_type = extract_type(node.value)
|
|
291
400
|
unless found_type == type.to_sym
|
|
292
|
-
return make_error UnexpectedValue, "expected #{type}, got #{
|
|
401
|
+
return make_error UnexpectedValue, "expected #{type}, got #{found_type}", path
|
|
293
402
|
end
|
|
294
403
|
else
|
|
295
404
|
raise "unknown type #{schema["type"]}"
|
|
@@ -300,66 +409,7 @@ module YAMLSchema
|
|
|
300
409
|
|
|
301
410
|
valid
|
|
302
411
|
end
|
|
303
|
-
|
|
304
|
-
# Taken from http://yaml.org/type/timestamp.html
|
|
305
|
-
TIME = /^-?\d{4}-\d{1,2}-\d{1,2}(?:[Tt]|\s+)\d{1,2}:\d\d:\d\d(?:\.\d*)?(?:\s*(?:Z|[-+]\d{1,2}:?(?:\d\d)?))?$/
|
|
306
|
-
|
|
307
|
-
# Taken from http://yaml.org/type/float.html
|
|
308
|
-
# Base 60, [-+]inf and NaN are handled separately
|
|
309
|
-
FLOAT = /^(?:[-+]?([0-9][0-9_,]*)?\.[0-9]*([eE][-+][0-9]+)?(?# base 10))$/x
|
|
310
|
-
|
|
311
|
-
# Taken from http://yaml.org/type/int.html and modified to ensure at least one numerical symbol exists
|
|
312
|
-
INTEGER_STRICT = /^(?:[-+]?0b[_]*[0-1][0-1_]* (?# base 2)
|
|
313
|
-
|[-+]?0[_]*[0-7][0-7_]* (?# base 8)
|
|
314
|
-
|[-+]?(0|[1-9][0-9_]*) (?# base 10)
|
|
315
|
-
|[-+]?0x[_]*[0-9a-fA-F][0-9a-fA-F_]* (?# base 16))$/x
|
|
316
|
-
|
|
317
|
-
# Tokenize +string+ returning the Ruby object
|
|
318
|
-
def extract_type(string)
|
|
319
|
-
return :null if string.empty?
|
|
320
|
-
# Check for a String type, being careful not to get caught by hash keys, hex values, and
|
|
321
|
-
# special floats (e.g., -.inf).
|
|
322
|
-
if string.match?(%r{^[^\d.:-]?[[:alpha:]_\s!@#$%\^&*(){}<>|/\\~;=]+}) || string.match?(/\n/)
|
|
323
|
-
return :string if string.length > 5
|
|
324
|
-
|
|
325
|
-
if string.match?(/^[^ytonf~]/i)
|
|
326
|
-
:string
|
|
327
|
-
elsif string == '~' || string.match?(/^null$/i)
|
|
328
|
-
:null
|
|
329
|
-
elsif string.match?(/^(yes|true|on)$/i)
|
|
330
|
-
:boolean
|
|
331
|
-
elsif string.match?(/^(no|false|off)$/i)
|
|
332
|
-
:boolean
|
|
333
|
-
else
|
|
334
|
-
:string
|
|
335
|
-
end
|
|
336
|
-
elsif string.match?(TIME)
|
|
337
|
-
:time
|
|
338
|
-
elsif string.match?(/^\d{4}-(?:1[012]|0\d|\d)-(?:[12]\d|3[01]|0\d|\d)$/)
|
|
339
|
-
:date
|
|
340
|
-
elsif string.match?(/^\+?\.inf$/i)
|
|
341
|
-
:float
|
|
342
|
-
elsif string.match?(/^-\.inf$/i)
|
|
343
|
-
:float
|
|
344
|
-
elsif string.match?(/^\.nan$/i)
|
|
345
|
-
:float
|
|
346
|
-
elsif string.match?(/^:./)
|
|
347
|
-
:symbol
|
|
348
|
-
elsif string.match?(/^[-+]?[0-9][0-9_]*(:[0-5]?[0-9]){1,2}$/)
|
|
349
|
-
:sexagesimal
|
|
350
|
-
elsif string.match?(/^[-+]?[0-9][0-9_]*(:[0-5]?[0-9]){1,2}\.[0-9_]*$/)
|
|
351
|
-
:sexagesimal
|
|
352
|
-
elsif string.match?(FLOAT)
|
|
353
|
-
if string.match?(/\A[-+]?\.\Z/)
|
|
354
|
-
:string
|
|
355
|
-
else
|
|
356
|
-
:float
|
|
357
|
-
end
|
|
358
|
-
elsif string.match?(INTEGER_STRICT)
|
|
359
|
-
:integer
|
|
360
|
-
else
|
|
361
|
-
:string
|
|
362
|
-
end
|
|
363
|
-
end
|
|
364
412
|
end
|
|
365
413
|
end
|
|
414
|
+
|
|
415
|
+
require "yaml-schema/compiler"
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
require "minitest/autorun"
|
|
2
|
+
require "yaml-schema"
|
|
3
|
+
require "psych"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
require "open3"
|
|
6
|
+
require "rbconfig"
|
|
7
|
+
|
|
8
|
+
module YAMLSchema
|
|
9
|
+
class CompilerTest < Minitest::Test
|
|
10
|
+
SCHEMAS = [
|
|
11
|
+
{ "type" => "string" },
|
|
12
|
+
{ "type" => "string", "minLength" => 2, "maxLength" => 4, "pattern" => /\A[a-z]+\z/ },
|
|
13
|
+
{ "type" => "integer" },
|
|
14
|
+
{ "type" => "integer", "pattern" => /\A1/ },
|
|
15
|
+
{ "type" => "float" },
|
|
16
|
+
{ "type" => "boolean" },
|
|
17
|
+
{ "type" => "null" },
|
|
18
|
+
{ "type" => "date" },
|
|
19
|
+
{ "type" => "time" },
|
|
20
|
+
{ "type" => "symbol" },
|
|
21
|
+
{ "type" => ["null", "string"] },
|
|
22
|
+
{ "type" => ["integer", "boolean", "string"] },
|
|
23
|
+
{ "type" => "array", "items" => { "type" => "integer" } },
|
|
24
|
+
{ "type" => "array", "items" => { "type" => "string" }, "minItems" => 1, "maxItems" => 2 },
|
|
25
|
+
{ "type" => "array", "prefixItems" => [{ "type" => "string" }, { "type" => "integer" }] },
|
|
26
|
+
{ "type" => "object", "items" => { "type" => "integer" } },
|
|
27
|
+
{ "type" => "object", "tag" => "!thing", "items" => { "type" => "string" } },
|
|
28
|
+
{ "type" => ["null", "object"], "tag" => "!thing", "items" => { "type" => "string" } },
|
|
29
|
+
{
|
|
30
|
+
"type" => "object",
|
|
31
|
+
"properties" => {
|
|
32
|
+
"name" => { "type" => "string" },
|
|
33
|
+
"age" => { "type" => ["null", "integer"] },
|
|
34
|
+
"tags" => { "type" => "array", "items" => { "type" => "string" } },
|
|
35
|
+
},
|
|
36
|
+
"required" => ["name", "age"],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"type" => "object",
|
|
40
|
+
"properties" => { "a" => { "type" => "integer" } },
|
|
41
|
+
"additionalProperties" => { "type" => "boolean" },
|
|
42
|
+
"propertyNames" => { "pattern" => /\A[a-z]\z/, "maxLength" => 1 },
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"type" => "object",
|
|
46
|
+
"properties" => {
|
|
47
|
+
"users" => {
|
|
48
|
+
"type" => "array",
|
|
49
|
+
"items" => {
|
|
50
|
+
"type" => "object",
|
|
51
|
+
"properties" => {
|
|
52
|
+
"name" => { "type" => "string" },
|
|
53
|
+
"meta" => { "type" => "object", "items" => { "type" => ["string", "integer"] } },
|
|
54
|
+
},
|
|
55
|
+
"required" => ["name"],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
DOCUMENTS = [
|
|
63
|
+
"foo", "'foo'", "\"123\"", "123", "0x1f", "1.5", ".inf", "true", "yes",
|
|
64
|
+
"", "~", "null", "2024-01-02", "2024-01-02 10:11:12", ":sym", "abcdef",
|
|
65
|
+
"!str 123", "!thing foo", "ab", "a1",
|
|
66
|
+
"- 1\n- 2", "- foo\n- 1", "- foo\n- bar\n- baz", "[]", "- a\n- 1\n- extra",
|
|
67
|
+
"a: 1", "a: true\nb: false", "a: 1\na: true", "ab: 1", "1: 1",
|
|
68
|
+
"!thing\na: b", "!other\na: b", "{}",
|
|
69
|
+
"name: bob\nage: 1", "name: bob", "name: bob\nage:\ntags:\n- x", "name: bob\nage: 1\nextra: 1",
|
|
70
|
+
"name: 1\nage: 1", "age: x",
|
|
71
|
+
"users:\n- name: a\n meta:\n x: 1\n y: z\n- name: b", "users:\n- meta: {}",
|
|
72
|
+
"users:\n- name: a\n meta:\n x: [1]",
|
|
73
|
+
"a: &x 1\nb: *x", "x: &x\n- 1\ny: *x",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
def outcome
|
|
77
|
+
[:ok, yield]
|
|
78
|
+
rescue Validator::Exception => e
|
|
79
|
+
[e.class, e.message]
|
|
80
|
+
rescue => e
|
|
81
|
+
[:crash, e.class]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Stand-alone validators define their own exception classes, so compare
|
|
85
|
+
# exceptions by their unqualified names.
|
|
86
|
+
def loose_outcome
|
|
87
|
+
[:ok, yield]
|
|
88
|
+
rescue => e
|
|
89
|
+
# An exception raised without a message uses its class name as the message
|
|
90
|
+
message = e.message == e.class.name ? :default : e.message
|
|
91
|
+
[e.class.name.split("::").last, message]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def standalone_validator schema
|
|
95
|
+
mod = Module.new
|
|
96
|
+
mod.module_eval Compiler.to_ruby(schema, class_name: "Validator", standalone: true)
|
|
97
|
+
mod::Validator
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def test_compiled_matches_interpreter
|
|
101
|
+
SCHEMAS.each do |schema|
|
|
102
|
+
compiled = Compiler.compile(schema)
|
|
103
|
+
DOCUMENTS.each do |yaml|
|
|
104
|
+
node = Psych.parse("---\n#{yaml}").children.first
|
|
105
|
+
[true, false].each do |aliases|
|
|
106
|
+
expected = outcome { Validator.validate(schema, node, aliases: aliases) }
|
|
107
|
+
actual = outcome { compiled.validate(node, aliases: aliases) }
|
|
108
|
+
assert_equal expected, actual, "schema=#{schema.inspect} yaml=#{yaml.inspect} aliases=#{aliases}"
|
|
109
|
+
|
|
110
|
+
expected = outcome { e = Validator.new.invalid?(schema, node, aliases: aliases); e && [e.class, e.message] }
|
|
111
|
+
actual = outcome { e = compiled.invalid?(node, aliases: aliases); e && [e.class, e.message] }
|
|
112
|
+
assert_equal expected, actual, "invalid? schema=#{schema.inspect} yaml=#{yaml.inspect}"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def test_standalone_matches_interpreter
|
|
119
|
+
SCHEMAS.each do |schema|
|
|
120
|
+
standalone = standalone_validator(schema)
|
|
121
|
+
refute_operator standalone, :<, CompiledValidator
|
|
122
|
+
|
|
123
|
+
DOCUMENTS.each do |yaml|
|
|
124
|
+
node = Psych.parse("---\n#{yaml}").children.first
|
|
125
|
+
[true, false].each do |aliases|
|
|
126
|
+
expected = loose_outcome { Validator.validate(schema, node, aliases: aliases) }
|
|
127
|
+
actual = loose_outcome { standalone.validate(node, aliases: aliases) }
|
|
128
|
+
assert_equal expected, actual, "schema=#{schema.inspect} yaml=#{yaml.inspect} aliases=#{aliases}"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def test_standalone_runs_without_yaml_schema
|
|
135
|
+
schema = { "type" => "object", "properties" => { "age" => { "type" => "integer" } } }
|
|
136
|
+
script = <<~'RUBY'
|
|
137
|
+
require "psych"
|
|
138
|
+
load ARGV.fetch(0)
|
|
139
|
+
abort "yaml-schema was loaded" if defined?(YAMLSchema)
|
|
140
|
+
puts AgeValidator.validate(Psych.parse("age: 1").children.first)
|
|
141
|
+
begin
|
|
142
|
+
AgeValidator.validate(Psych.parse("age: x").children.first)
|
|
143
|
+
rescue AgeValidator::UnexpectedValue => e
|
|
144
|
+
puts e.message
|
|
145
|
+
end
|
|
146
|
+
RUBY
|
|
147
|
+
|
|
148
|
+
Dir.mktmpdir do |dir|
|
|
149
|
+
path = File.join(dir, "age_validator.rb")
|
|
150
|
+
File.write path, Compiler.to_ruby(schema, class_name: "AgeValidator", standalone: true)
|
|
151
|
+
|
|
152
|
+
# Clear RUBYOPT so bundler/setup doesn't load the gemspec (which requires yaml-schema)
|
|
153
|
+
env = { "RUBYOPT" => nil, "RUBYLIB" => nil }
|
|
154
|
+
out, err, status = Open3.capture3(env, RbConfig.ruby, "--disable-gems", "-e", script, path)
|
|
155
|
+
assert status.success?, err
|
|
156
|
+
assert_equal "true\nexpected integer, got string path: /age\n", out
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def test_validate_and_invalid?
|
|
161
|
+
v = Compiler.compile({ "type" => "array", "items" => { "type" => "integer" } })
|
|
162
|
+
assert v.validate(Psych.parse("- 1").children.first)
|
|
163
|
+
refute v.invalid?(Psych.parse("- 1").children.first)
|
|
164
|
+
|
|
165
|
+
node = Psych.parse("- 1\n- foo").children.first
|
|
166
|
+
err = v.invalid?(node)
|
|
167
|
+
assert_kind_of Validator::UnexpectedValue, err
|
|
168
|
+
assert_match %r{path: /1}, err.message
|
|
169
|
+
assert_raises(Validator::UnexpectedValue) { v.validate(node) }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def test_invalid_schema_raises_at_compile_time
|
|
173
|
+
assert_raises(Validator::InvalidSchema) { Compiler.compile({ "type" => "nope" }) }
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def test_custom_node_info
|
|
177
|
+
info = Object.new
|
|
178
|
+
def info.read_tag(node); node.tag == "!aaron" ? "test" : node.tag; end
|
|
179
|
+
v = Compiler.compile({ "type" => "object", "tag" => "test", "properties" => { "foo" => { "type" => "string" } } })
|
|
180
|
+
node = Psych.parse("--- !aaron\nfoo: bar").children.first
|
|
181
|
+
assert v.new(info).validate(node)
|
|
182
|
+
assert_raises(Validator::UnexpectedTag) { v.validate(node) }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def test_extra_prefix_items_are_allowed
|
|
186
|
+
v = Compiler.compile({ "type" => "array", "prefixItems" => [{ "type" => "string" }] })
|
|
187
|
+
assert v.validate(Psych.parse("- a\n- 1").children.first)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def test_to_ruby
|
|
191
|
+
schema = { "type" => "object", "properties" => { "name" => { "type" => "string", "pattern" => %r{\A/\w+\z}i } } }
|
|
192
|
+
src = Compiler.to_ruby(schema, class_name: "GeneratedPersonValidator")
|
|
193
|
+
|
|
194
|
+
Dir.mktmpdir do |dir|
|
|
195
|
+
path = File.join(dir, "generated_person_validator.rb")
|
|
196
|
+
File.write path, src
|
|
197
|
+
load path
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
assert_operator GeneratedPersonValidator, :<, CompiledValidator
|
|
201
|
+
assert GeneratedPersonValidator.validate(Psych.parse("name: /Bob").children.first)
|
|
202
|
+
assert_raises(Validator::InvalidPattern) do
|
|
203
|
+
GeneratedPersonValidator.validate(Psych.parse("name: bob").children.first)
|
|
204
|
+
end
|
|
205
|
+
ensure
|
|
206
|
+
YAMLSchema.send(:remove_const, :GeneratedPersonValidator) if YAMLSchema.const_defined?(:GeneratedPersonValidator, false)
|
|
207
|
+
Object.send(:remove_const, :GeneratedPersonValidator) if Object.const_defined?(:GeneratedPersonValidator, false)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|