yaml-schema 1.0.1 → 1.2.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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/lib/yaml-schema.rb +128 -66
  3. data/test/validator_test.rb +176 -1
  4. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1c8b8b2332c8bfbf1061b7082c1fc4d5df88ce365d6f9ff4796561abfb25a3af
4
- data.tar.gz: e91a4093b75138771c8a78f64a2de9beb4a55dd6727a3604f1863c3a130907b6
3
+ metadata.gz: e7079554145bf81aef3f0fb89dfa5ad5e0867336866989da48e9217cbae1f114
4
+ data.tar.gz: e6bc2b714f98cce44632e17fc6b9d4d8bbaebea4583427897019ce8d28f81293
5
5
  SHA512:
6
- metadata.gz: 0fe1aba2b8398416020a28757d3b00e05927fa55c1c44d410bbf285d26ef0577fc186161c98a5daa87622604d20708d415a0b7231018842b199573906998d615
7
- data.tar.gz: 3e2f19f68706ef4404b6d7fc4cad4087f183518fade8158fbdb519cc14d530965a973efeaaf354397db3359030c4d72be0a3d93a937a79b6116ed1e29de42ded
6
+ metadata.gz: 347fe6c4f3a01eefc61b4f6a18570893c472fa815f942b9ec57ac9ccd8abd6d7f06d73b0f757dd38dee5c9da5f6e01d8c4dd76e5e726d4b92aa0e4a966126f89
7
+ data.tar.gz: 62a7ec295631d838f06c8932f4c1ae614650cfb635b1bd9bb2539425a53575e1bf87a9af748d1632e082c0f25dc93c7cc311173300c5bf47bfbf3cd11dbc6d41
data/lib/yaml-schema.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module YAMLSchema
4
- VERSION = "1.0.1"
4
+ VERSION = "1.2.0"
5
5
 
6
6
  class Pointer
7
7
  include Enumerable
@@ -68,8 +68,10 @@ module YAMLSchema
68
68
  class UnexpectedProperty < Exception; end
69
69
  class UnexpectedTag < Exception; end
70
70
  class UnexpectedValue < Exception; end
71
+ class UnexpectedAlias < Exception; end
71
72
  class InvalidSchema < Exception; end
72
73
  class InvalidString < Exception; end
74
+ class InvalidPattern < Exception; end
73
75
  class MissingRequiredField < Exception; end
74
76
 
75
77
  Valid = Struct.new(:exception).new.freeze
@@ -77,8 +79,8 @@ module YAMLSchema
77
79
  ##
78
80
  # Given a particular schema, validate that the node conforms to the
79
81
  # schema. Raises an exception if it is invalid
80
- def self.validate(schema, node)
81
- INSTANCE.validate schema, node
82
+ def self.validate(schema, node, aliases: true)
83
+ INSTANCE.validate schema, node, aliases: aliases
82
84
  end
83
85
 
84
86
  module NodeInfo # :nodoc:
@@ -96,8 +98,8 @@ module YAMLSchema
96
98
  ##
97
99
  # Given a particular schema, validate that the node conforms to the
98
100
  # schema. Raises an exception if it is invalid
99
- def validate(schema, node)
100
- val = _validate(schema["type"], schema, node, Valid, {}, ["root"])
101
+ def validate(schema, node, aliases: true)
102
+ val = _validate(schema["type"], schema, node, Valid, {}, ["root"], aliases)
101
103
  if val.exception
102
104
  raise val
103
105
  else
@@ -108,8 +110,8 @@ module YAMLSchema
108
110
  ##
109
111
  # Given a particular schema, validate that the node conforms to the
110
112
  # schema. Returns an error object if the node is invalid, otherwise false.
111
- def invalid?(schema, node)
112
- res = _validate(schema["type"], schema, node, Valid, {}, ["root"])
113
+ def invalid?(schema, node, aliases: true)
114
+ res = _validate(schema["type"], schema, node, Valid, {}, ["root"], aliases)
113
115
  if Valid == res
114
116
  false
115
117
  else
@@ -125,11 +127,12 @@ module YAMLSchema
125
127
  ex
126
128
  end
127
129
 
128
- def _validate(type, schema, node, valid, aliases, path)
130
+ def _validate(type, schema, node, valid, aliases, path, allow_aliases)
129
131
  return valid if valid.exception
130
132
 
131
133
  if node.anchor
132
134
  if node.alias?
135
+ raise UnexpectedAlias unless allow_aliases
133
136
  node = aliases[node.anchor]
134
137
  else
135
138
  aliases[node.anchor] = node
@@ -141,7 +144,7 @@ module YAMLSchema
141
144
  if Array === type
142
145
  v = valid
143
146
  type.each do |t|
144
- v = _validate t, schema, node, valid, aliases, path
147
+ v = _validate t, schema, node, valid, aliases, path, allow_aliases
145
148
  unless v.exception
146
149
  break
147
150
  end
@@ -177,7 +180,7 @@ module YAMLSchema
177
180
  properties = schema["properties"].dup
178
181
  key_restriction = schema["propertyNames"] || {}
179
182
  node.children.each_slice(2) do |key, val|
180
- valid = _validate("string", key_restriction, key, valid, aliases, path)
183
+ valid = _validate("string", key_restriction, key, valid, aliases, path, allow_aliases)
181
184
 
182
185
  return valid if valid.exception
183
186
 
@@ -188,7 +191,8 @@ module YAMLSchema
188
191
  return make_error UnexpectedProperty, "unknown property #{key.value.dump}", path
189
192
  end
190
193
  }
191
- valid = _validate(sub_schema["type"], sub_schema, val, valid, aliases, path + [key.value])
194
+
195
+ valid = _validate(sub_schema["type"], sub_schema, val, valid, aliases, path + [key.value], allow_aliases)
192
196
 
193
197
  return valid if valid.exception
194
198
  end
@@ -203,45 +207,15 @@ module YAMLSchema
203
207
  if schema["items"]
204
208
  sub_schema = schema["items"]
205
209
  node.children.each_slice(2) do |key, val|
206
- valid = _validate("string", {}, key, valid, aliases, path)
210
+ valid = _validate("string", {}, key, valid, aliases, path, allow_aliases)
207
211
  return valid if valid.exception
208
- valid = _validate(sub_schema["type"], sub_schema, val, valid, aliases, path + [key.value])
212
+ valid = _validate(sub_schema["type"], sub_schema, val, valid, aliases, path + [key.value], allow_aliases)
209
213
  return valid if valid.exception
210
214
  end
211
215
  else
212
216
  raise InvalidSchema, "objects must specify items or properties"
213
217
  end
214
218
  end
215
- when "string"
216
- unless node.scalar?
217
- return make_error UnexpectedType, "expected Scalar, got #{node.class.name.dump}", path
218
- end
219
-
220
- unless node.quoted || node.tag == "!str"
221
- if node.value == "false" || node.value == "true"
222
- return make_error UnexpectedValue, "expected string, got boolean", path
223
- end
224
-
225
- if node.value == ""
226
- return make_error UnexpectedValue, "expected string, got null", path
227
- end
228
-
229
- if node.value.match?(/^[-+]?(?:0|[1-9](?:[0-9]|,[0-9]|_[0-9])*)$/)
230
- return make_error UnexpectedValue, "expected string, got integer", path
231
- end
232
- end
233
-
234
- if schema["maxLength"] && node.value.bytesize > schema["maxLength"]
235
- return make_error InvalidString, "expected string length to be <= #{schema["maxLength"]}", path
236
- end
237
-
238
- if schema["minLength"] && node.value.bytesize < schema["minLength"]
239
- return make_error InvalidString, "expected string length to be >= #{schema["minLength"]}", path
240
- end
241
-
242
- if schema["pattern"] && !(node.value.match?(schema["pattern"]))
243
- return make_error InvalidString, "expected string to match #{schema["pattern"]}", path
244
- end
245
219
  when "array"
246
220
  unless node.sequence?
247
221
  return make_error UnexpectedType, "expected Sequence, got #{node.class.name.dump}", path
@@ -258,46 +232,134 @@ module YAMLSchema
258
232
  if schema["items"]
259
233
  node.children.each_with_index { |item, i|
260
234
  sub_schema = schema["items"]
261
- valid = _validate sub_schema["type"], sub_schema, item, valid, aliases, path + [i]
235
+ valid = _validate sub_schema["type"], sub_schema, item, valid, aliases, path + [i], allow_aliases
262
236
  }
263
237
  elsif schema["prefixItems"]
264
238
  node.children.each_with_index { |item, i|
265
239
  sub_schema = schema["prefixItems"][i]
266
- valid = _validate sub_schema["type"], sub_schema, item, valid, aliases, path + [i]
240
+ valid = _validate sub_schema["type"], sub_schema, item, valid, aliases, path + [i], allow_aliases
267
241
  }
268
242
  else
269
243
  raise NotImplementedError
270
244
  end
271
- when "null"
245
+ else
272
246
  unless node.scalar?
273
247
  return make_error UnexpectedType, "expected Scalar, got #{node.class.name.dump}", path
274
248
  end
275
249
 
276
- unless node.value == ""
277
- return make_error UnexpectedValue, "expected empty string, got #{node.value.dump}", path
278
- end
279
- when "boolean"
280
- unless node.scalar?
281
- return make_error UnexpectedType, "expected Scalar, got #{node.class.name.dump}", path
282
- end
283
- unless node.value == "false" || node.value == "true"
284
- return make_error UnexpectedValue, "expected 'true' or 'false' for boolean", path
285
- end
286
- when "integer"
287
- unless node.scalar?
288
- return make_error UnexpectedType, "expected Scalar, got #{node.class.name.dump}", path
250
+ if type == "string"
251
+ unless node.quoted || node.tag == "!str"
252
+ type = extract_type(node.value)
253
+
254
+ if type != :string
255
+ return make_error UnexpectedValue, "expected string, got #{type}", path
256
+ end
257
+ end
258
+
259
+ if schema["maxLength"] && node.value.bytesize > schema["maxLength"]
260
+ return make_error InvalidString, "expected string length to be <= #{schema["maxLength"]}", path
261
+ end
262
+
263
+ if schema["minLength"] && node.value.bytesize < schema["minLength"]
264
+ return make_error InvalidString, "expected string length to be >= #{schema["minLength"]}", path
265
+ end
266
+
267
+ if schema["pattern"] && !(node.value.match?(schema["pattern"]))
268
+ return make_error InvalidPattern, "expected string '#{node.value.dump}' to match #{schema["pattern"]}", path
269
+ end
270
+ else
271
+ if node.quoted
272
+ return make_error UnexpectedValue, "expected #{type}, got string", path
273
+ end
274
+
275
+ if type == "null"
276
+ unless node.value == ""
277
+ return make_error UnexpectedValue, "expected empty string, got #{node.value.dump}", path
278
+ end
279
+ else
280
+ if schema["pattern"] && !(node.value.match?(schema["pattern"]))
281
+ return make_error InvalidPattern, "expected '#{node.value.dump}' to match #{schema["pattern"]}", path
282
+ end
283
+
284
+ case type
285
+ when "boolean"
286
+ unless node.value == "false" || node.value == "true"
287
+ return make_error UnexpectedValue, "expected 'true' or 'false' for boolean", path
288
+ end
289
+ when "integer", "float", "time", "date", "symbol"
290
+ found_type = extract_type(node.value)
291
+ unless found_type == type.to_sym
292
+ return make_error UnexpectedValue, "expected #{type}, got #{type}", path
293
+ end
294
+ else
295
+ raise "unknown type #{schema["type"]}"
296
+ end
297
+ end
289
298
  end
290
- if node.quoted
291
- return make_error UnexpectedValue, "expected integer, got string", path
299
+ end
300
+
301
+ valid
302
+ 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
292
335
  end
293
- unless node.value.match?(/^[-+]?(?:0|[1-9](?:[0-9]|,[0-9]|_[0-9])*)$/)
294
- return make_error UnexpectedValue, "expected integer, got string", path
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
295
357
  end
358
+ elsif string.match?(INTEGER_STRICT)
359
+ :integer
296
360
  else
297
- raise "unknown type #{schema["type"]}"
361
+ :string
298
362
  end
299
-
300
- valid
301
363
  end
302
364
  end
303
365
  end
@@ -1,10 +1,167 @@
1
1
  require "minitest/autorun"
2
2
  require "yaml-schema"
3
3
  require "psych"
4
+ require "date"
4
5
 
5
6
  module YAMLSchema
6
7
  class Validator
7
8
  class ErrorTest < Minitest::Test
9
+ def test_pattern_symbol
10
+ ast = Psych.parse(Psych.dump({ "foo" => :foo }))
11
+
12
+ assert_raises InvalidPattern do
13
+ Validator.validate({
14
+ "type" => "object",
15
+ "properties" => {
16
+ "foo" => {
17
+ "type" => "symbol",
18
+ "pattern" => /\A:bar\z/
19
+ },
20
+ },
21
+ }, ast.children.first)
22
+ end
23
+ end
24
+
25
+ def test_pattern_time
26
+ ast = Psych.parse(Psych.dump({ "foo" => Time.now }))
27
+
28
+ assert_raises InvalidPattern do
29
+ Validator.validate({
30
+ "type" => "object",
31
+ "properties" => {
32
+ "foo" => {
33
+ "type" => "time",
34
+ "pattern" => /\Ayay\z/
35
+ },
36
+ },
37
+ }, ast.children.first)
38
+ end
39
+ end
40
+
41
+ def test_pattern_null
42
+ ast = Psych.parse(Psych.dump({ "foo" => nil }))
43
+
44
+ assert Validator.validate({
45
+ "type" => "object",
46
+ "properties" => {
47
+ "foo" => {
48
+ "type" => "null",
49
+ "pattern" => /\Anotnull\z/
50
+ },
51
+ },
52
+ }, ast.children.first)
53
+ end
54
+
55
+ def test_pattern_float
56
+ ast = Psych.parse(Psych.dump({ "foo" => 1.2 }))
57
+
58
+ assert_raises InvalidPattern do
59
+ Validator.validate({
60
+ "type" => "object",
61
+ "properties" => {
62
+ "foo" => {
63
+ "type" => "float",
64
+ "pattern" => /\A1\.3\z/
65
+ },
66
+ },
67
+ }, ast.children.first)
68
+ end
69
+ end
70
+
71
+ def test_pattern_boolean
72
+ yaml = "---\n foo: true"
73
+ ast = Psych.parse(yaml)
74
+
75
+ assert_raises InvalidPattern do
76
+ Validator.validate({
77
+ "type" => "object",
78
+ "properties" => {
79
+ "foo" => {
80
+ "type" => "boolean",
81
+ "pattern" => /\Atru\z/
82
+ },
83
+ },
84
+ }, ast.children.first)
85
+ end
86
+ end
87
+
88
+ def test_pattern_date
89
+ yaml = "---\n foo: 2025-11-19"
90
+
91
+ ast = Psych.parse(yaml)
92
+ assert Validator.validate({
93
+ "type" => "object",
94
+ "properties" => {
95
+ "foo" => {
96
+ "type" => "date",
97
+ "pattern" => /\A2025-11-19\z/
98
+ },
99
+ },
100
+ }, ast.children.first)
101
+
102
+ assert_raises InvalidPattern do
103
+ Validator.validate({
104
+ "type" => "object",
105
+ "properties" => {
106
+ "foo" => {
107
+ "type" => "date",
108
+ "pattern" => /\A2025-11-20\z/
109
+ },
110
+ },
111
+ }, ast.children.first)
112
+ end
113
+ end
114
+
115
+ def test_accept_non_strings
116
+ [Float::INFINITY, -Float::INFINITY, Float::NAN].each do |v|
117
+ ast = Psych.parse(Psych.dump({ "foo" => v }))
118
+ assert Validator.validate({
119
+ "type" => "object",
120
+ "properties" => {
121
+ "foo" => { "type" => "float" },
122
+ },
123
+ }, ast.children.first)
124
+ end
125
+
126
+ ast = Psych.parse(Psych.dump({ "foo" => Time.now }))
127
+ assert Validator.validate({
128
+ "type" => "object",
129
+ "properties" => {
130
+ "foo" => { "type" => "time" },
131
+ },
132
+ }, ast.children.first)
133
+
134
+ ast = Psych.parse(Psych.dump({ "foo" => Date.today }))
135
+ assert Validator.validate({
136
+ "type" => "object",
137
+ "properties" => {
138
+ "foo" => { "type" => "date" },
139
+ },
140
+ }, ast.children.first)
141
+
142
+ ast = Psych.parse(Psych.dump({ "foo" => :foo }))
143
+ assert Validator.validate({
144
+ "type" => "object",
145
+ "properties" => {
146
+ "foo" => { "type" => "symbol" },
147
+ },
148
+ }, ast.children.first)
149
+ end
150
+
151
+ def test_reject_non_strings
152
+ [Float::INFINITY, -Float::INFINITY, Float::NAN, Time.now, Date.today, :foo].each do |v|
153
+ ast = Psych.parse(Psych.dump({ "foo" => v }))
154
+ assert_raises UnexpectedValue do
155
+ Validator.validate({
156
+ "type" => "object",
157
+ "properties" => {
158
+ "foo" => { "type" => "string" },
159
+ },
160
+ }, ast.children.first)
161
+ end
162
+ end
163
+ end
164
+
8
165
  def test_property_max_length
9
166
  ast = Psych.parse("---\n hello: world")
10
167
  assert_raises InvalidString do
@@ -121,7 +278,7 @@ module YAMLSchema
121
278
 
122
279
  def test_regular_expression
123
280
  ast = Psych.parse("bar")
124
- assert_raises InvalidString do
281
+ assert_raises InvalidPattern do
125
282
  Validator.validate({
126
283
  "type" => "string",
127
284
  "pattern" => /foo/
@@ -534,6 +691,24 @@ bar: *1
534
691
  }, ast.children.first)
535
692
  end
536
693
 
694
+ def test_optionally_disallow_aliases
695
+ ast = Psych.parse(<<-eoyml)
696
+ ---
697
+ foo: &1
698
+ - foo
699
+ bar: *1
700
+ eoyml
701
+ assert_raises UnexpectedAlias do
702
+ Validator.validate({
703
+ "type" => "object",
704
+ "properties" => {
705
+ "foo" => { "type" => "array", "items" => { "type" => "string" } },
706
+ "bar" => { "type" => "array", "items" => { "type" => "string" } },
707
+ },
708
+ }, ast.children.first, aliases: false)
709
+ end
710
+ end
711
+
537
712
  class CustomInfo
538
713
  def read_tag(node)
539
714
  if node.tag == "!aaron"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yaml-schema
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Patterson