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.
@@ -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.2.0"
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(schema, node, aliases: true)
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(node)
91
+ def self.read_tag node
88
92
  node.tag
89
93
  end
90
94
  end
91
95
 
92
- def initialize(node_info = NodeInfo)
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(schema, node, aliases: true)
102
- val = _validate(schema["type"], schema, node, Valid, {}, ["root"], aliases)
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?(schema, node, aliases: true)
114
- res = _validate(schema["type"], schema, node, Valid, {}, ["root"], aliases)
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 make_error(klass, msg, path)
125
- ex = klass.new msg + " path: #{path.join(" -> ")}"
126
- ex.set_backtrace caller
127
- ex
166
+ def _sp path
167
+ path.empty? ? "" : " at #{path}"
128
168
  end
129
169
 
130
- def _validate(type, schema, node, valid, aliases, path, allow_aliases)
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
- node.children.each_with_index { |item, i|
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 #{type}", path
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