yaml-schema 1.0.0 → 1.0.1

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: 1c8b8b2332c8bfbf1061b7082c1fc4d5df88ce365d6f9ff4796561abfb25a3af
4
+ data.tar.gz: e91a4093b75138771c8a78f64a2de9beb4a55dd6727a3604f1863c3a130907b6
5
5
  SHA512:
6
- metadata.gz: c943fc76323ec4ad6159939b00773537c76a360ff93bac4dad0e2f1bda94dd814a55437f60e80df6fa37e8d99b4a890a6c150c8d393f10c309d6a4899ee73976
7
- data.tar.gz: 4bc0b0c28f916e2fb86a43390303714ca19d290d6d5dbf4561a189cfeb6da99b6d01e76d579e3d8b4b6a0aee835173aac31ada6cc0401e6fd41005a80a3168a4
6
+ metadata.gz: 0fe1aba2b8398416020a28757d3b00e05927fa55c1c44d410bbf285d26ef0577fc186161c98a5daa87622604d20708d415a0b7231018842b199573906998d615
7
+ data.tar.gz: 3e2f19f68706ef4404b6d7fc4cad4087f183518fade8158fbdb519cc14d530965a973efeaaf354397db3359030c4d72be0a3d93a937a79b6116ed1e29de42ded
@@ -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.0.1"
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,8 @@ 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 MissingRequiredField < Exception; end
11
74
 
12
75
  Valid = Struct.new(:exception).new.freeze
13
76
 
@@ -112,18 +175,30 @@ module YAMLSchema
112
175
  end
113
176
  if schema["properties"]
114
177
  properties = schema["properties"].dup
178
+ key_restriction = schema["propertyNames"] || {}
115
179
  node.children.each_slice(2) do |key, val|
116
- valid = _validate("string", {}, key, valid, aliases, path)
180
+ valid = _validate("string", key_restriction, key, valid, aliases, path)
117
181
 
118
182
  return valid if valid.exception
119
183
 
120
184
  sub_schema = properties.delete(key.value) {
121
- return make_error UnexpectedProperty, "unknown property #{key.value.dump}", path
185
+ if schema["additionalProperties"]
186
+ schema["additionalProperties"]
187
+ else
188
+ return make_error UnexpectedProperty, "unknown property #{key.value.dump}", path
189
+ end
122
190
  }
123
191
  valid = _validate(sub_schema["type"], sub_schema, val, valid, aliases, path + [key.value])
124
192
 
125
193
  return valid if valid.exception
126
194
  end
195
+
196
+ if schema["required"]
197
+ missing_fields = properties.keys & schema["required"]
198
+ unless missing_fields.empty?
199
+ return make_error MissingRequiredField, "missing fields #{missing_fields.map(&:dump).join(" ")}", path
200
+ end
201
+ end
127
202
  else
128
203
  if schema["items"]
129
204
  sub_schema = schema["items"]
@@ -155,6 +230,18 @@ module YAMLSchema
155
230
  return make_error UnexpectedValue, "expected string, got integer", path
156
231
  end
157
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
158
245
  when "array"
159
246
  unless node.sequence?
160
247
  return make_error UnexpectedType, "expected Sequence, got #{node.class.name.dump}", path
@@ -164,6 +251,10 @@ module YAMLSchema
164
251
  return make_error UnexpectedValue, "expected maximum #{schema["maxItems"]} items, but found #{node.children.length}", path
165
252
  end
166
253
 
254
+ if schema["minItems"] && node.children.length < schema["minItems"]
255
+ return make_error UnexpectedValue, "expected minimum #{schema["minItems"]} items, but found #{node.children.length}", path
256
+ end
257
+
167
258
  if schema["items"]
168
259
  node.children.each_with_index { |item, i|
169
260
  sub_schema = schema["items"]
@@ -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,161 @@ require "psych"
5
5
  module YAMLSchema
6
6
  class Validator
7
7
  class ErrorTest < Minitest::Test
8
+ def test_property_max_length
9
+ ast = Psych.parse("---\n hello: world")
10
+ assert_raises InvalidString do
11
+ Validator.validate({
12
+ "type" => "object",
13
+ "properties" => {
14
+ "hello" => { "type" => "string" },
15
+ },
16
+ "propertyNames" => {
17
+ "maxLength" => 4
18
+ },
19
+ "items" => { "type" => "string" },
20
+ }, ast.children.first)
21
+ end
22
+
23
+ assert Validator.validate({
24
+ "type" => "object",
25
+ "properties" => {
26
+ "hello" => { "type" => "string" },
27
+ },
28
+ "maxPropertyLength" => 5,
29
+ "items" => { "type" => "string" },
30
+ }, ast.children.first)
31
+ end
32
+
33
+ def test_additional_properties
34
+ ast = Psych.parse("---\n hello: world\n foo: bar")
35
+ assert_raises UnexpectedProperty do
36
+ Validator.validate({
37
+ "type" => "object",
38
+ "properties" => {
39
+ "hello" => { "type" => "string" },
40
+ },
41
+ "items" => { "type" => "string" }
42
+ }, ast.children.first)
43
+ end
44
+
45
+ ast = Psych.parse("---\n hello: world\n foo: bar")
46
+ assert Validator.validate({
47
+ "type" => "object",
48
+ "properties" => {
49
+ "hello" => { "type" => "string" },
50
+ },
51
+ "items" => { "type" => "string" },
52
+ "additionalProperties" => {
53
+ "type" => "string"
54
+ },
55
+ }, ast.children.first)
56
+
57
+ ast = Psych.parse("---\n hello: world\n foo: bar")
58
+ assert_raises UnexpectedValue do
59
+ Validator.validate({
60
+ "type" => "object",
61
+ "properties" => {
62
+ "hello" => { "type" => "string" },
63
+ },
64
+ "items" => { "type" => "string" },
65
+ "additionalProperties" => {
66
+ "type" => "null"
67
+ },
68
+ }, ast.children.first)
69
+ end
70
+ end
71
+
72
+ def test_string_min_length
73
+ ast = Psych.parse("--- hello")
74
+ assert_raises InvalidString do
75
+ Validator.validate({
76
+ "type" => "string",
77
+ "minLength" => 6,
78
+ }, ast.children.first)
79
+ end
80
+
81
+ ast = Psych.parse("--- hello")
82
+ assert Validator.validate({
83
+ "type" => "string",
84
+ "minLength" => 5,
85
+ }, ast.children.first)
86
+ end
87
+
88
+ def test_string_max_length
89
+ ast = Psych.parse("--- hello")
90
+ assert_raises InvalidString do
91
+ Validator.validate({
92
+ "type" => "string",
93
+ "maxLength" => 4,
94
+ }, ast.children.first)
95
+ end
96
+
97
+ ast = Psych.parse("--- hello")
98
+ assert Validator.validate({
99
+ "type" => "string",
100
+ "maxLength" => 5,
101
+ }, ast.children.first)
102
+ end
103
+
104
+ def test_minItems
105
+ ast = Psych.parse("---\n- bar")
106
+ assert_raises UnexpectedValue do
107
+ Validator.validate({
108
+ "type" => "array",
109
+ "items" => { "type" => "string" },
110
+ "minItems" => 2
111
+ }, ast.children.first)
112
+ end
113
+
114
+ ast = Psych.parse("---\n- bar")
115
+ assert Validator.validate({
116
+ "type" => "array",
117
+ "items" => { "type" => "string" },
118
+ "minItems" => 1
119
+ }, ast.children.first)
120
+ end
121
+
122
+ def test_regular_expression
123
+ ast = Psych.parse("bar")
124
+ assert_raises InvalidString do
125
+ Validator.validate({
126
+ "type" => "string",
127
+ "pattern" => /foo/
128
+ }, ast.children.first)
129
+ end
130
+
131
+ assert Validator.validate({
132
+ "type" => "string",
133
+ "pattern" => /bar/
134
+ }, ast.children.first)
135
+ end
136
+
137
+ def test_missing_required
138
+ ast = Psych.parse("foo: bar")
139
+ assert_raises MissingRequiredField do
140
+ Validator.validate({
141
+ "type" => "object",
142
+ "properties" => {
143
+ "foo" => { "type" => "string" },
144
+ "bar" => { "type" => "string" },
145
+ "baz" => { "type" => "string" },
146
+ },
147
+ "required" => [ "foo", "baz"],
148
+ }, ast.children.first)
149
+ end
150
+
151
+ ast = Psych.parse("---\n foo: bar\n baz: hello")
152
+ assert Validator.validate({
153
+ "type" => "object",
154
+ "properties" => {
155
+ "foo" => { "type" => "string" },
156
+ "bar" => { "type" => "string" },
157
+ "baz" => { "type" => "string" },
158
+ },
159
+ "required" => [ "foo", "baz"],
160
+ }, ast.children.first)
161
+ end
162
+
8
163
  def test_missing_tag
9
164
  ast = Psych.parse("foo: bar")
10
165
  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.0.1
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