yaml-schema 1.0.1 → 1.1.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 +112 -52
  3. data/test/validator_test.rb +157 -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: 579d1085952dfe8a826de14f05a2af2073da12bf45ef5d5959155447a02a9c0d
4
+ data.tar.gz: 165be9ca5619b56f47f54ba456e31213662a35fff3f41a21b9d03e0e400feae5
5
5
  SHA512:
6
- metadata.gz: 0fe1aba2b8398416020a28757d3b00e05927fa55c1c44d410bbf285d26ef0577fc186161c98a5daa87622604d20708d415a0b7231018842b199573906998d615
7
- data.tar.gz: 3e2f19f68706ef4404b6d7fc4cad4087f183518fade8158fbdb519cc14d530965a973efeaaf354397db3359030c4d72be0a3d93a937a79b6116ed1e29de42ded
6
+ metadata.gz: eadb3ccf99d89937351e506a95dc95d3527e19569e1e89b6dad29f66a2b5f9ade673015838526494a7a3c5c2b4f487bb5b386236505542759f0330e60207dd44
7
+ data.tar.gz: 79132f662fe6b73bed894481f10df139b89839fd7cf97ccd972c8e66bbbaaea6184ef248735c4d5b2695609d72826fa0ff247894f468030a02e1d2548b6d9ee3
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.1.0"
5
5
 
6
6
  class Pointer
7
7
  include Enumerable
@@ -70,6 +70,7 @@ module YAMLSchema
70
70
  class UnexpectedValue < Exception; end
71
71
  class InvalidSchema < Exception; end
72
72
  class InvalidString < Exception; end
73
+ class InvalidPattern < Exception; end
73
74
  class MissingRequiredField < Exception; end
74
75
 
75
76
  Valid = Struct.new(:exception).new.freeze
@@ -188,6 +189,7 @@ module YAMLSchema
188
189
  return make_error UnexpectedProperty, "unknown property #{key.value.dump}", path
189
190
  end
190
191
  }
192
+
191
193
  valid = _validate(sub_schema["type"], sub_schema, val, valid, aliases, path + [key.value])
192
194
 
193
195
  return valid if valid.exception
@@ -212,36 +214,6 @@ module YAMLSchema
212
214
  raise InvalidSchema, "objects must specify items or properties"
213
215
  end
214
216
  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
217
  when "array"
246
218
  unless node.sequence?
247
219
  return make_error UnexpectedType, "expected Sequence, got #{node.class.name.dump}", path
@@ -268,36 +240,124 @@ module YAMLSchema
268
240
  else
269
241
  raise NotImplementedError
270
242
  end
271
- when "null"
243
+ else
272
244
  unless node.scalar?
273
245
  return make_error UnexpectedType, "expected Scalar, got #{node.class.name.dump}", path
274
246
  end
275
247
 
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
248
+ if type == "string"
249
+ unless node.quoted || node.tag == "!str"
250
+ type = extract_type(node.value)
251
+
252
+ if type != :string
253
+ return make_error UnexpectedValue, "expected string, got #{type}", path
254
+ end
255
+ end
256
+
257
+ if schema["maxLength"] && node.value.bytesize > schema["maxLength"]
258
+ return make_error InvalidString, "expected string length to be <= #{schema["maxLength"]}", path
259
+ end
260
+
261
+ if schema["minLength"] && node.value.bytesize < schema["minLength"]
262
+ return make_error InvalidString, "expected string length to be >= #{schema["minLength"]}", path
263
+ end
264
+
265
+ if schema["pattern"] && !(node.value.match?(schema["pattern"]))
266
+ return make_error InvalidPattern, "expected string '#{node.value.dump}' to match #{schema["pattern"]}", path
267
+ end
268
+ else
269
+ if node.quoted
270
+ return make_error UnexpectedValue, "expected #{type}, got string", path
271
+ end
272
+
273
+ if type == "null"
274
+ unless node.value == ""
275
+ return make_error UnexpectedValue, "expected empty string, got #{node.value.dump}", path
276
+ end
277
+ else
278
+ if schema["pattern"] && !(node.value.match?(schema["pattern"]))
279
+ return make_error InvalidPattern, "expected '#{node.value.dump}' to match #{schema["pattern"]}", path
280
+ end
281
+
282
+ case type
283
+ when "boolean"
284
+ unless node.value == "false" || node.value == "true"
285
+ return make_error UnexpectedValue, "expected 'true' or 'false' for boolean", path
286
+ end
287
+ when "integer", "float", "time", "date", "symbol"
288
+ found_type = extract_type(node.value)
289
+ unless found_type == type.to_sym
290
+ return make_error UnexpectedValue, "expected #{type}, got #{type}", path
291
+ end
292
+ else
293
+ raise "unknown type #{schema["type"]}"
294
+ end
295
+ end
289
296
  end
290
- if node.quoted
291
- return make_error UnexpectedValue, "expected integer, got string", path
297
+ end
298
+
299
+ valid
300
+ end
301
+
302
+ # Taken from http://yaml.org/type/timestamp.html
303
+ 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)?))?$/
304
+
305
+ # Taken from http://yaml.org/type/float.html
306
+ # Base 60, [-+]inf and NaN are handled separately
307
+ FLOAT = /^(?:[-+]?([0-9][0-9_,]*)?\.[0-9]*([eE][-+][0-9]+)?(?# base 10))$/x
308
+
309
+ # Taken from http://yaml.org/type/int.html and modified to ensure at least one numerical symbol exists
310
+ INTEGER_STRICT = /^(?:[-+]?0b[_]*[0-1][0-1_]* (?# base 2)
311
+ |[-+]?0[_]*[0-7][0-7_]* (?# base 8)
312
+ |[-+]?(0|[1-9][0-9_]*) (?# base 10)
313
+ |[-+]?0x[_]*[0-9a-fA-F][0-9a-fA-F_]* (?# base 16))$/x
314
+
315
+ # Tokenize +string+ returning the Ruby object
316
+ def extract_type(string)
317
+ return :null if string.empty?
318
+ # Check for a String type, being careful not to get caught by hash keys, hex values, and
319
+ # special floats (e.g., -.inf).
320
+ if string.match?(%r{^[^\d.:-]?[[:alpha:]_\s!@#$%\^&*(){}<>|/\\~;=]+}) || string.match?(/\n/)
321
+ return :string if string.length > 5
322
+
323
+ if string.match?(/^[^ytonf~]/i)
324
+ :string
325
+ elsif string == '~' || string.match?(/^null$/i)
326
+ :null
327
+ elsif string.match?(/^(yes|true|on)$/i)
328
+ :boolean
329
+ elsif string.match?(/^(no|false|off)$/i)
330
+ :boolean
331
+ else
332
+ :string
292
333
  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
334
+ elsif string.match?(TIME)
335
+ :time
336
+ elsif string.match?(/^\d{4}-(?:1[012]|0\d|\d)-(?:[12]\d|3[01]|0\d|\d)$/)
337
+ :date
338
+ elsif string.match?(/^\+?\.inf$/i)
339
+ :float
340
+ elsif string.match?(/^-\.inf$/i)
341
+ :float
342
+ elsif string.match?(/^\.nan$/i)
343
+ :float
344
+ elsif string.match?(/^:./)
345
+ :symbol
346
+ elsif string.match?(/^[-+]?[0-9][0-9_]*(:[0-5]?[0-9]){1,2}$/)
347
+ :sexagesimal
348
+ elsif string.match?(/^[-+]?[0-9][0-9_]*(:[0-5]?[0-9]){1,2}\.[0-9_]*$/)
349
+ :sexagesimal
350
+ elsif string.match?(FLOAT)
351
+ if string.match?(/\A[-+]?\.\Z/)
352
+ :string
353
+ else
354
+ :float
295
355
  end
356
+ elsif string.match?(INTEGER_STRICT)
357
+ :integer
296
358
  else
297
- raise "unknown type #{schema["type"]}"
359
+ :string
298
360
  end
299
-
300
- valid
301
361
  end
302
362
  end
303
363
  end
@@ -5,6 +5,162 @@ require "psych"
5
5
  module YAMLSchema
6
6
  class Validator
7
7
  class ErrorTest < Minitest::Test
8
+ def test_pattern_symbol
9
+ ast = Psych.parse(Psych.dump({ "foo" => :foo }))
10
+
11
+ assert_raises InvalidPattern do
12
+ Validator.validate({
13
+ "type" => "object",
14
+ "properties" => {
15
+ "foo" => {
16
+ "type" => "symbol",
17
+ "pattern" => /\A:bar\z/
18
+ },
19
+ },
20
+ }, ast.children.first)
21
+ end
22
+ end
23
+
24
+ def test_pattern_time
25
+ ast = Psych.parse(Psych.dump({ "foo" => Time.now }))
26
+
27
+ assert_raises InvalidPattern do
28
+ Validator.validate({
29
+ "type" => "object",
30
+ "properties" => {
31
+ "foo" => {
32
+ "type" => "time",
33
+ "pattern" => /\Ayay\z/
34
+ },
35
+ },
36
+ }, ast.children.first)
37
+ end
38
+ end
39
+
40
+ def test_pattern_null
41
+ ast = Psych.parse(Psych.dump({ "foo" => nil }))
42
+
43
+ assert Validator.validate({
44
+ "type" => "object",
45
+ "properties" => {
46
+ "foo" => {
47
+ "type" => "null",
48
+ "pattern" => /\Anotnull\z/
49
+ },
50
+ },
51
+ }, ast.children.first)
52
+ end
53
+
54
+ def test_pattern_float
55
+ ast = Psych.parse(Psych.dump({ "foo" => 1.2 }))
56
+
57
+ assert_raises InvalidPattern do
58
+ Validator.validate({
59
+ "type" => "object",
60
+ "properties" => {
61
+ "foo" => {
62
+ "type" => "float",
63
+ "pattern" => /\A1\.3\z/
64
+ },
65
+ },
66
+ }, ast.children.first)
67
+ end
68
+ end
69
+
70
+ def test_pattern_boolean
71
+ yaml = "---\n foo: true"
72
+ ast = Psych.parse(yaml)
73
+
74
+ assert_raises InvalidPattern do
75
+ Validator.validate({
76
+ "type" => "object",
77
+ "properties" => {
78
+ "foo" => {
79
+ "type" => "boolean",
80
+ "pattern" => /\Atru\z/
81
+ },
82
+ },
83
+ }, ast.children.first)
84
+ end
85
+ end
86
+
87
+ def test_pattern_date
88
+ yaml = "---\n foo: 2025-11-19"
89
+
90
+ ast = Psych.parse(yaml)
91
+ assert Validator.validate({
92
+ "type" => "object",
93
+ "properties" => {
94
+ "foo" => {
95
+ "type" => "date",
96
+ "pattern" => /\A2025-11-19\z/
97
+ },
98
+ },
99
+ }, ast.children.first)
100
+
101
+ assert_raises InvalidPattern do
102
+ Validator.validate({
103
+ "type" => "object",
104
+ "properties" => {
105
+ "foo" => {
106
+ "type" => "date",
107
+ "pattern" => /\A2025-11-20\z/
108
+ },
109
+ },
110
+ }, ast.children.first)
111
+ end
112
+ end
113
+
114
+ def test_accept_non_strings
115
+ [Float::INFINITY, -Float::INFINITY, Float::NAN].each do |v|
116
+ ast = Psych.parse(Psych.dump({ "foo" => v }))
117
+ assert Validator.validate({
118
+ "type" => "object",
119
+ "properties" => {
120
+ "foo" => { "type" => "float" },
121
+ },
122
+ }, ast.children.first)
123
+ end
124
+
125
+ ast = Psych.parse(Psych.dump({ "foo" => Time.now }))
126
+ assert Validator.validate({
127
+ "type" => "object",
128
+ "properties" => {
129
+ "foo" => { "type" => "time" },
130
+ },
131
+ }, ast.children.first)
132
+
133
+ ast = Psych.parse(Psych.dump({ "foo" => Date.today }))
134
+ assert Validator.validate({
135
+ "type" => "object",
136
+ "properties" => {
137
+ "foo" => { "type" => "date" },
138
+ },
139
+ }, ast.children.first)
140
+
141
+ ast = Psych.parse(Psych.dump({ "foo" => :foo }))
142
+ assert Validator.validate({
143
+ "type" => "object",
144
+ "properties" => {
145
+ "foo" => { "type" => "symbol" },
146
+ },
147
+ }, ast.children.first)
148
+ end
149
+
150
+ def test_reject_non_strings
151
+ [Float::INFINITY, -Float::INFINITY, Float::NAN, Time.now, Date.today, :foo].each do |v|
152
+ ast = Psych.parse(Psych.dump({ "foo" => v }))
153
+ assert_raises UnexpectedValue do
154
+ Validator.validate({
155
+ "type" => "object",
156
+ "properties" => {
157
+ "foo" => { "type" => "string" },
158
+ },
159
+ }, ast.children.first)
160
+ end
161
+ end
162
+ end
163
+
8
164
  def test_property_max_length
9
165
  ast = Psych.parse("---\n hello: world")
10
166
  assert_raises InvalidString do
@@ -121,7 +277,7 @@ module YAMLSchema
121
277
 
122
278
  def test_regular_expression
123
279
  ast = Psych.parse("bar")
124
- assert_raises InvalidString do
280
+ assert_raises InvalidPattern do
125
281
  Validator.validate({
126
282
  "type" => "string",
127
283
  "pattern" => /foo/
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.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Patterson