yaml-schema 1.0.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 701e8ba71eb1d60990092ad38d7dca6f3954f4bd70ec1a5f292ed9eb244a7aef
4
- data.tar.gz: a930db9675bdfe08fe078431e3d8ded4104ee427694641e7d88961aed3cd9e16
3
+ metadata.gz: 579d1085952dfe8a826de14f05a2af2073da12bf45ef5d5959155447a02a9c0d
4
+ data.tar.gz: 165be9ca5619b56f47f54ba456e31213662a35fff3f41a21b9d03e0e400feae5
5
5
  SHA512:
6
- metadata.gz: c943fc76323ec4ad6159939b00773537c76a360ff93bac4dad0e2f1bda94dd814a55437f60e80df6fa37e8d99b4a890a6c150c8d393f10c309d6a4899ee73976
7
- data.tar.gz: 4bc0b0c28f916e2fb86a43390303714ca19d290d6d5dbf4561a189cfeb6da99b6d01e76d579e3d8b4b6a0aee835173aac31ada6cc0401e6fd41005a80a3168a4
6
+ metadata.gz: eadb3ccf99d89937351e506a95dc95d3527e19569e1e89b6dad29f66a2b5f9ade673015838526494a7a3c5c2b4f487bb5b386236505542759f0330e60207dd44
7
+ data.tar.gz: 79132f662fe6b73bed894481f10df139b89839fd7cf97ccd972c8e66bbbaaea6184ef248735c4d5b2695609d72826fa0ff247894f468030a02e1d2548b6d9ee3
@@ -0,0 +1,30 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ ruby-versions:
9
+ uses: ruby/actions/.github/workflows/ruby_versions.yml@master
10
+
11
+ test:
12
+ needs: ruby-versions
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ matrix:
16
+ ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }}
17
+
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+
21
+ - name: Set up Ruby
22
+ uses: ruby/setup-ruby-pkgs@v1
23
+ with:
24
+ ruby-version: ${{ matrix.ruby }}
25
+ apt-get: "haveged libyaml-dev"
26
+ brew: libyaml
27
+ vcpkg: libyaml
28
+
29
+ - name: Run tests
30
+ run: bundle exec rake test
@@ -0,0 +1,52 @@
1
+ name: Publish gem to rubygems.org
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ push:
13
+ if: github.repository == 'tenderlove/yaml-schema'
14
+ runs-on: ubuntu-latest
15
+
16
+ environment:
17
+ name: rubygems.org
18
+ url: https://rubygems.org/gems/yaml-schema
19
+
20
+ permissions:
21
+ contents: write
22
+ id-token: write
23
+
24
+ strategy:
25
+ matrix:
26
+ ruby: ["ruby"]
27
+
28
+ steps:
29
+ - name: Harden Runner
30
+ uses: step-security/harden-runner@v2
31
+ with:
32
+ egress-policy: audit
33
+
34
+ - uses: actions/checkout@v4
35
+
36
+ - name: Set up Ruby
37
+ uses: ruby/setup-ruby@v1
38
+ with:
39
+ ruby-version: ${{ matrix.ruby }}
40
+
41
+ - name: Install dependencies
42
+ run: bundle install --jobs 4 --retry 3
43
+
44
+ - name: Publish to RubyGems
45
+ uses: rubygems/release-gem@v1
46
+
47
+ - name: Create GitHub release
48
+ run: |
49
+ tag_name="$(git describe --tags --abbrev=0)"
50
+ gh release create "${tag_name}" --verify-tag --generate-notes
51
+ env:
52
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,77 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to make participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, sex characteristics, gender identity and expression,
9
+ level of experience, education, socio-economic status, nationality, personal
10
+ appearance, race, religion, or sexual identity and orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies within all project spaces, and it also applies when
49
+ an individual is representing the project or its community in public spaces.
50
+ Examples of representing a project or community include using an official
51
+ project e-mail address, posting via an official social media account, or acting
52
+ as an appointed representative at an online or offline event. Representation of
53
+ a project may be further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at aaron.patterson at gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72
+
73
+ [homepage]: https://www.contributor-covenant.org
74
+
75
+ For answers to common questions about this code of conduct, see
76
+ https://www.contributor-covenant.org/faq
77
+
data/lib/yaml-schema.rb CHANGED
@@ -1,6 +1,67 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module YAMLSchema
4
+ VERSION = "1.1.0"
5
+
6
+ class Pointer
7
+ include Enumerable
8
+
9
+ class Exception < StandardError; end
10
+ class FormatError < Exception; end
11
+
12
+ def initialize path
13
+ @path = Pointer.parse path
14
+ end
15
+
16
+ def each(&block); @path.each(&block); end
17
+
18
+ def eval object
19
+ Pointer.eval @path, object
20
+ end
21
+ alias :[] :eval
22
+
23
+ ESC = {'^/' => '/', '^^' => '^', '~0' => '~', '~1' => '/'} # :nodoc:
24
+
25
+ def self.[] path, object
26
+ eval parse(path), object
27
+ end
28
+
29
+ def self.eval list, object # :nodoc:
30
+ object = object.children.first if object.document?
31
+
32
+ list.inject(object) { |o, part|
33
+ return nil unless o
34
+
35
+ if o.sequence?
36
+ raise IndexError unless part =~ /\A(?:\d|[1-9]\d+)\Z/
37
+ o.children.fetch(part.to_i)
38
+ else
39
+ o.children.each_slice(2) do |key, value|
40
+ if key.value == part
41
+ break value
42
+ end
43
+ end
44
+ end
45
+ }
46
+ end
47
+
48
+ def self.parse path
49
+ return [''] if path == '/'
50
+ return [] if path == ''
51
+
52
+ unless path.start_with? '/'
53
+ raise FormatError, "Pointer should start with a slash"
54
+ end
55
+
56
+ parts = path.sub(/^\//, '').split(/(?<!\^)\//).each { |part|
57
+ part.gsub!(/\^[\/^]|~[01]/) { |m| ESC[m] }
58
+ }
59
+
60
+ parts.push("") if path[-1] == '/'
61
+ parts
62
+ end
63
+ end
64
+
4
65
  class Validator
5
66
  class Exception < StandardError; end
6
67
  class UnexpectedType < Exception; end
@@ -8,6 +69,9 @@ module YAMLSchema
8
69
  class UnexpectedTag < Exception; end
9
70
  class UnexpectedValue < Exception; end
10
71
  class InvalidSchema < Exception; end
72
+ class InvalidString < Exception; end
73
+ class InvalidPattern < Exception; end
74
+ class MissingRequiredField < Exception; end
11
75
 
12
76
  Valid = Struct.new(:exception).new.freeze
13
77
 
@@ -112,18 +176,31 @@ module YAMLSchema
112
176
  end
113
177
  if schema["properties"]
114
178
  properties = schema["properties"].dup
179
+ key_restriction = schema["propertyNames"] || {}
115
180
  node.children.each_slice(2) do |key, val|
116
- valid = _validate("string", {}, key, valid, aliases, path)
181
+ valid = _validate("string", key_restriction, key, valid, aliases, path)
117
182
 
118
183
  return valid if valid.exception
119
184
 
120
185
  sub_schema = properties.delete(key.value) {
121
- return make_error UnexpectedProperty, "unknown property #{key.value.dump}", path
186
+ if schema["additionalProperties"]
187
+ schema["additionalProperties"]
188
+ else
189
+ return make_error UnexpectedProperty, "unknown property #{key.value.dump}", path
190
+ end
122
191
  }
192
+
123
193
  valid = _validate(sub_schema["type"], sub_schema, val, valid, aliases, path + [key.value])
124
194
 
125
195
  return valid if valid.exception
126
196
  end
197
+
198
+ if schema["required"]
199
+ missing_fields = properties.keys & schema["required"]
200
+ unless missing_fields.empty?
201
+ return make_error MissingRequiredField, "missing fields #{missing_fields.map(&:dump).join(" ")}", path
202
+ end
203
+ end
127
204
  else
128
205
  if schema["items"]
129
206
  sub_schema = schema["items"]
@@ -137,24 +214,6 @@ module YAMLSchema
137
214
  raise InvalidSchema, "objects must specify items or properties"
138
215
  end
139
216
  end
140
- when "string"
141
- unless node.scalar?
142
- return make_error UnexpectedType, "expected Scalar, got #{node.class.name.dump}", path
143
- end
144
-
145
- unless node.quoted || node.tag == "!str"
146
- if node.value == "false" || node.value == "true"
147
- return make_error UnexpectedValue, "expected string, got boolean", path
148
- end
149
-
150
- if node.value == ""
151
- return make_error UnexpectedValue, "expected string, got null", path
152
- end
153
-
154
- if node.value.match?(/^[-+]?(?:0|[1-9](?:[0-9]|,[0-9]|_[0-9])*)$/)
155
- return make_error UnexpectedValue, "expected string, got integer", path
156
- end
157
- end
158
217
  when "array"
159
218
  unless node.sequence?
160
219
  return make_error UnexpectedType, "expected Sequence, got #{node.class.name.dump}", path
@@ -164,6 +223,10 @@ module YAMLSchema
164
223
  return make_error UnexpectedValue, "expected maximum #{schema["maxItems"]} items, but found #{node.children.length}", path
165
224
  end
166
225
 
226
+ if schema["minItems"] && node.children.length < schema["minItems"]
227
+ return make_error UnexpectedValue, "expected minimum #{schema["minItems"]} items, but found #{node.children.length}", path
228
+ end
229
+
167
230
  if schema["items"]
168
231
  node.children.each_with_index { |item, i|
169
232
  sub_schema = schema["items"]
@@ -177,36 +240,124 @@ module YAMLSchema
177
240
  else
178
241
  raise NotImplementedError
179
242
  end
180
- when "null"
243
+ else
181
244
  unless node.scalar?
182
245
  return make_error UnexpectedType, "expected Scalar, got #{node.class.name.dump}", path
183
246
  end
184
247
 
185
- unless node.value == ""
186
- return make_error UnexpectedValue, "expected empty string, got #{node.value.dump}", path
187
- end
188
- when "boolean"
189
- unless node.scalar?
190
- return make_error UnexpectedType, "expected Scalar, got #{node.class.name.dump}", path
191
- end
192
- unless node.value == "false" || node.value == "true"
193
- return make_error UnexpectedValue, "expected 'true' or 'false' for boolean", path
194
- end
195
- when "integer"
196
- unless node.scalar?
197
- 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
198
296
  end
199
- if node.quoted
200
- 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
201
333
  end
202
- unless node.value.match?(/^[-+]?(?:0|[1-9](?:[0-9]|,[0-9]|_[0-9])*)$/)
203
- 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
204
355
  end
356
+ elsif string.match?(INTEGER_STRICT)
357
+ :integer
205
358
  else
206
- raise "unknown type #{schema["type"]}"
359
+ :string
207
360
  end
208
-
209
- valid
210
361
  end
211
362
  end
212
363
  end
@@ -0,0 +1,36 @@
1
+ require "minitest/autorun"
2
+ require "yaml-schema"
3
+ require "psych"
4
+
5
+ module YAMLSchema
6
+ class PointerTest < Minitest::Test
7
+ def test_value
8
+ ast = Psych.parse("---\n hello: world")
9
+ pointer = YAMLSchema::Pointer.new("/hello")
10
+ assert_equal "world", pointer.eval(ast).value
11
+ assert_equal "world", pointer.eval(ast.children.first).value
12
+ end
13
+
14
+ def test_nested_values
15
+ ast = Psych.parse("---\n hello: \n nested: world")
16
+ pointer = YAMLSchema::Pointer.new("/hello/nested")
17
+ assert_equal "world", pointer.eval(ast).value
18
+ assert_equal "world", pointer.eval(ast.children.first).value
19
+ end
20
+
21
+ def test_array_part
22
+ ast = Psych.parse("---\n- a\n- b\n- c")
23
+ pointer = YAMLSchema::Pointer.new("/0")
24
+ assert_equal "a", pointer.eval(ast).value
25
+ assert_equal "a", pointer[ast].value
26
+ assert_equal "b", Pointer["/1", ast].value
27
+ assert_equal "c", Pointer["/2", ast].value
28
+ assert_raises(IndexError) { Pointer["/3", ast] }
29
+ end
30
+
31
+ def test_map_key
32
+ ast = Psych.parse("---\n- a\n- b\n- c")
33
+ assert_raises(IndexError) { Pointer["/foo", ast] }
34
+ end
35
+ end
36
+ end
@@ -5,6 +5,317 @@ 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
+
164
+ def test_property_max_length
165
+ ast = Psych.parse("---\n hello: world")
166
+ assert_raises InvalidString do
167
+ Validator.validate({
168
+ "type" => "object",
169
+ "properties" => {
170
+ "hello" => { "type" => "string" },
171
+ },
172
+ "propertyNames" => {
173
+ "maxLength" => 4
174
+ },
175
+ "items" => { "type" => "string" },
176
+ }, ast.children.first)
177
+ end
178
+
179
+ assert Validator.validate({
180
+ "type" => "object",
181
+ "properties" => {
182
+ "hello" => { "type" => "string" },
183
+ },
184
+ "maxPropertyLength" => 5,
185
+ "items" => { "type" => "string" },
186
+ }, ast.children.first)
187
+ end
188
+
189
+ def test_additional_properties
190
+ ast = Psych.parse("---\n hello: world\n foo: bar")
191
+ assert_raises UnexpectedProperty do
192
+ Validator.validate({
193
+ "type" => "object",
194
+ "properties" => {
195
+ "hello" => { "type" => "string" },
196
+ },
197
+ "items" => { "type" => "string" }
198
+ }, ast.children.first)
199
+ end
200
+
201
+ ast = Psych.parse("---\n hello: world\n foo: bar")
202
+ assert Validator.validate({
203
+ "type" => "object",
204
+ "properties" => {
205
+ "hello" => { "type" => "string" },
206
+ },
207
+ "items" => { "type" => "string" },
208
+ "additionalProperties" => {
209
+ "type" => "string"
210
+ },
211
+ }, ast.children.first)
212
+
213
+ ast = Psych.parse("---\n hello: world\n foo: bar")
214
+ assert_raises UnexpectedValue do
215
+ Validator.validate({
216
+ "type" => "object",
217
+ "properties" => {
218
+ "hello" => { "type" => "string" },
219
+ },
220
+ "items" => { "type" => "string" },
221
+ "additionalProperties" => {
222
+ "type" => "null"
223
+ },
224
+ }, ast.children.first)
225
+ end
226
+ end
227
+
228
+ def test_string_min_length
229
+ ast = Psych.parse("--- hello")
230
+ assert_raises InvalidString do
231
+ Validator.validate({
232
+ "type" => "string",
233
+ "minLength" => 6,
234
+ }, ast.children.first)
235
+ end
236
+
237
+ ast = Psych.parse("--- hello")
238
+ assert Validator.validate({
239
+ "type" => "string",
240
+ "minLength" => 5,
241
+ }, ast.children.first)
242
+ end
243
+
244
+ def test_string_max_length
245
+ ast = Psych.parse("--- hello")
246
+ assert_raises InvalidString do
247
+ Validator.validate({
248
+ "type" => "string",
249
+ "maxLength" => 4,
250
+ }, ast.children.first)
251
+ end
252
+
253
+ ast = Psych.parse("--- hello")
254
+ assert Validator.validate({
255
+ "type" => "string",
256
+ "maxLength" => 5,
257
+ }, ast.children.first)
258
+ end
259
+
260
+ def test_minItems
261
+ ast = Psych.parse("---\n- bar")
262
+ assert_raises UnexpectedValue do
263
+ Validator.validate({
264
+ "type" => "array",
265
+ "items" => { "type" => "string" },
266
+ "minItems" => 2
267
+ }, ast.children.first)
268
+ end
269
+
270
+ ast = Psych.parse("---\n- bar")
271
+ assert Validator.validate({
272
+ "type" => "array",
273
+ "items" => { "type" => "string" },
274
+ "minItems" => 1
275
+ }, ast.children.first)
276
+ end
277
+
278
+ def test_regular_expression
279
+ ast = Psych.parse("bar")
280
+ assert_raises InvalidPattern do
281
+ Validator.validate({
282
+ "type" => "string",
283
+ "pattern" => /foo/
284
+ }, ast.children.first)
285
+ end
286
+
287
+ assert Validator.validate({
288
+ "type" => "string",
289
+ "pattern" => /bar/
290
+ }, ast.children.first)
291
+ end
292
+
293
+ def test_missing_required
294
+ ast = Psych.parse("foo: bar")
295
+ assert_raises MissingRequiredField do
296
+ Validator.validate({
297
+ "type" => "object",
298
+ "properties" => {
299
+ "foo" => { "type" => "string" },
300
+ "bar" => { "type" => "string" },
301
+ "baz" => { "type" => "string" },
302
+ },
303
+ "required" => [ "foo", "baz"],
304
+ }, ast.children.first)
305
+ end
306
+
307
+ ast = Psych.parse("---\n foo: bar\n baz: hello")
308
+ assert Validator.validate({
309
+ "type" => "object",
310
+ "properties" => {
311
+ "foo" => { "type" => "string" },
312
+ "bar" => { "type" => "string" },
313
+ "baz" => { "type" => "string" },
314
+ },
315
+ "required" => [ "foo", "baz"],
316
+ }, ast.children.first)
317
+ end
318
+
8
319
  def test_missing_tag
9
320
  ast = Psych.parse("foo: bar")
10
321
  assert_raises UnexpectedTag do
data/yaml-schema.gemspec CHANGED
@@ -1,8 +1,9 @@
1
1
  $: << File.expand_path("lib")
2
+ require "yaml-schema"
2
3
 
3
4
  Gem::Specification.new do |s|
4
5
  s.name = "yaml-schema"
5
- s.version = "1.0.0"
6
+ s.version = YAMLSchema::VERSION
6
7
  s.summary = "Validate YAML against a schema"
7
8
  s.description = "If you need to validate YAML against a schema, use this"
8
9
  s.authors = ["Aaron Patterson"]
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.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Patterson
@@ -57,11 +57,15 @@ executables: []
57
57
  extensions: []
58
58
  extra_rdoc_files: []
59
59
  files:
60
+ - ".github/workflows/ci.yml"
61
+ - ".github/workflows/release.yml"
62
+ - CODE_OF_CONDUCT.md
60
63
  - Gemfile
61
64
  - LICENSE
62
65
  - README.md
63
66
  - Rakefile
64
67
  - lib/yaml-schema.rb
68
+ - test/pointer_test.rb
65
69
  - test/validator_test.rb
66
70
  - yaml-schema.gemspec
67
71
  homepage: https://github.com/tenderlove/yaml-schema
@@ -82,8 +86,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
82
86
  - !ruby/object:Gem::Version
83
87
  version: '0'
84
88
  requirements: []
85
- rubygems_version: 4.0.0.dev
89
+ rubygems_version: 3.6.9
86
90
  specification_version: 4
87
91
  summary: Validate YAML against a schema
88
92
  test_files:
93
+ - test/pointer_test.rb
89
94
  - test/validator_test.rb