json_schemer 0.2.18 → 2.1.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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +3 -7
  3. data/CHANGELOG.md +69 -0
  4. data/Gemfile.lock +28 -10
  5. data/README.md +379 -4
  6. data/bin/hostname_character_classes +42 -0
  7. data/bin/rake +29 -0
  8. data/exe/json_schemer +62 -0
  9. data/json_schemer.gemspec +6 -12
  10. data/lib/json_schemer/cached_resolver.rb +16 -0
  11. data/lib/json_schemer/content.rb +18 -0
  12. data/lib/json_schemer/draft201909/meta.rb +320 -0
  13. data/lib/json_schemer/draft201909/vocab/applicator.rb +104 -0
  14. data/lib/json_schemer/draft201909/vocab/core.rb +45 -0
  15. data/lib/json_schemer/draft201909/vocab.rb +31 -0
  16. data/lib/json_schemer/draft202012/meta.rb +364 -0
  17. data/lib/json_schemer/draft202012/vocab/applicator.rb +382 -0
  18. data/lib/json_schemer/draft202012/vocab/content.rb +52 -0
  19. data/lib/json_schemer/draft202012/vocab/core.rb +160 -0
  20. data/lib/json_schemer/draft202012/vocab/format_annotation.rb +23 -0
  21. data/lib/json_schemer/draft202012/vocab/format_assertion.rb +23 -0
  22. data/lib/json_schemer/draft202012/vocab/meta_data.rb +30 -0
  23. data/lib/json_schemer/draft202012/vocab/unevaluated.rb +94 -0
  24. data/lib/json_schemer/draft202012/vocab/validation.rb +286 -0
  25. data/lib/json_schemer/draft202012/vocab.rb +105 -0
  26. data/lib/json_schemer/draft4/meta.rb +161 -0
  27. data/lib/json_schemer/draft4/vocab/validation.rb +39 -0
  28. data/lib/json_schemer/draft4/vocab.rb +18 -0
  29. data/lib/json_schemer/draft6/meta.rb +172 -0
  30. data/lib/json_schemer/draft6/vocab.rb +16 -0
  31. data/lib/json_schemer/draft7/meta.rb +183 -0
  32. data/lib/json_schemer/draft7/vocab/validation.rb +69 -0
  33. data/lib/json_schemer/draft7/vocab.rb +30 -0
  34. data/lib/json_schemer/ecma_regexp.rb +51 -0
  35. data/lib/json_schemer/errors.rb +1 -0
  36. data/lib/json_schemer/format/duration.rb +23 -0
  37. data/lib/json_schemer/format/email.rb +56 -0
  38. data/lib/json_schemer/format/hostname.rb +58 -0
  39. data/lib/json_schemer/format/json_pointer.rb +18 -0
  40. data/lib/json_schemer/format/uri_template.rb +34 -0
  41. data/lib/json_schemer/format.rb +129 -109
  42. data/lib/json_schemer/keyword.rb +53 -0
  43. data/lib/json_schemer/location.rb +25 -0
  44. data/lib/json_schemer/openapi.rb +40 -0
  45. data/lib/json_schemer/openapi30/document.rb +1672 -0
  46. data/lib/json_schemer/openapi30/meta.rb +32 -0
  47. data/lib/json_schemer/openapi30/vocab/base.rb +18 -0
  48. data/lib/json_schemer/openapi30/vocab.rb +12 -0
  49. data/lib/json_schemer/openapi31/document.rb +1557 -0
  50. data/lib/json_schemer/openapi31/meta.rb +136 -0
  51. data/lib/json_schemer/openapi31/vocab/base.rb +127 -0
  52. data/lib/json_schemer/openapi31/vocab.rb +18 -0
  53. data/lib/json_schemer/output.rb +56 -0
  54. data/lib/json_schemer/result.rb +229 -0
  55. data/lib/json_schemer/schema.rb +423 -0
  56. data/lib/json_schemer/version.rb +1 -1
  57. data/lib/json_schemer.rb +218 -28
  58. metadata +98 -25
  59. data/lib/json_schemer/cached_ref_resolver.rb +0 -14
  60. data/lib/json_schemer/schema/base.rb +0 -658
  61. data/lib/json_schemer/schema/draft4.rb +0 -44
  62. data/lib/json_schemer/schema/draft6.rb +0 -25
  63. data/lib/json_schemer/schema/draft7.rb +0 -32
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 426a95173deee91594b5ad1df05dac15ea3c90bac6e17b3136a7170744555c50
4
- data.tar.gz: 25268e7f5cb108245aad3d112624eee96774fe8820eda5e24a4f91d97d91e676
3
+ metadata.gz: 1981782f2342c7123beb0374680037bc11f3be72a8e4be0a213cfb7691643415
4
+ data.tar.gz: e8b6c605ba1ce2a2751ac90167f53c27fad975140ab9a0e8328c2b04ce8fe732
5
5
  SHA512:
6
- metadata.gz: 5534a623dfece170bd27bbb2cbc34978e9e81c075ddad60213d8ce4538d50a6ee119b7efcb2b750068cee0cf565ee582a35eac990c14c2ca647d9d8cc314d4f5
7
- data.tar.gz: b813c1b1acd0b1cf0680bf0ecae7eef33b6eb576ff5735d76f072e82fc5cfb36e80ec5b0a3fde6829ec3de856e903b42164a917b833019c33a11d79d6db6080b
6
+ metadata.gz: 4d4c6e6c3660560a602b5610559efe37b5839f1142af6486e350bd207cad49e37a00f397d555878957b14951da95745092240bd14417e2ffd02826f053099952
7
+ data.tar.gz: 8dc561414463ceeacf7bbd0be95be9c441a50919cdb839ff8366fa0268d96d91e4947985bd7bab406f3c163e913bf7ca923a38e22d777d2549af3329f91c14f2
@@ -6,21 +6,17 @@ jobs:
6
6
  fail-fast: false
7
7
  matrix:
8
8
  os: [ubuntu-latest, windows-latest, macos-latest]
9
- ruby: [2.4, 2.5, 2.6, 2.7, 3.0, head, jruby, jruby-head, truffleruby, truffleruby-head]
9
+ ruby: [2.5, 2.6, 2.7, 3.0, 3.1, 3.2, head, jruby, jruby-head, truffleruby, truffleruby-head]
10
10
  exclude:
11
- - os: windows-latest
12
- ruby: jruby
13
- - os: windows-latest
14
- ruby: jruby-head
15
11
  - os: windows-latest
16
12
  ruby: truffleruby
17
13
  - os: windows-latest
18
14
  ruby: truffleruby-head
19
15
  runs-on: ${{ matrix.os }}
20
16
  steps:
21
- - uses: actions/checkout@v2
17
+ - uses: actions/checkout@v4
22
18
  - uses: ruby/setup-ruby@v1
23
19
  with:
24
20
  ruby-version: ${{ matrix.ruby }}
25
21
  bundler-cache: true
26
- - run: bundle exec rake test
22
+ - run: bin/rake test
data/CHANGELOG.md ADDED
@@ -0,0 +1,69 @@
1
+ # Changelog
2
+
3
+ ## [2.1.1] - 2023-11-28
4
+
5
+ ### Bug Fixes
6
+
7
+ - Fix refs to/through keyword objects: https://github.com/davishmcclurg/json_schemer/pull/160
8
+ - Temporary fix for incorrect `uri-reference` format in OpenAPI 3.x: https://github.com/davishmcclurg/json_schemer/pull/161
9
+
10
+ [2.1.1]: https://github.com/davishmcclurg/json_schemer/releases/tag/v2.1.1
11
+
12
+ ## [2.1.0] - 2023-11-17
13
+
14
+ ### Bug Fixes
15
+
16
+ - Limit anyOf/oneOf discriminator to listed refs: https://github.com/davishmcclurg/json_schemer/pull/145
17
+ - Require discriminator `propertyName` property: https://github.com/davishmcclurg/json_schemer/pull/145
18
+ - Support `Schema#ref` in subschemas: https://github.com/davishmcclurg/json_schemer/pull/145
19
+ - Resolve JSON pointer refs using correct base URI: https://github.com/davishmcclurg/json_schemer/pull/147
20
+ - `date` format in OpenAPI 3.0: https://github.com/davishmcclurg/json_schemer/commit/69fe7a815ecf0cfb1c40ac402bf46a789c05e972
21
+
22
+ ### Features
23
+
24
+ - Custom error messages with `x-error` keyword and I18n: https://github.com/davishmcclurg/json_schemer/pull/149
25
+ - Custom content encodings and media types: https://github.com/davishmcclurg/json_schemer/pull/148
26
+
27
+ [2.1.0]: https://github.com/davishmcclurg/json_schemer/releases/tag/v2.1.0
28
+
29
+ ## [2.0.0] - 2023-08-20
30
+
31
+ For 2.0.0, much of the codebase was rewritten to simplify support for the two new JSON Schema draft versions (2019-09 and 2020-12). The major change is moving each keyword into its own class and organizing them into vocabularies. [Output formats](https://json-schema.org/draft/2020-12/json-schema-core.html#section-12) and [annotations](https://json-schema.org/draft/2020-12/json-schema-core.html#section-7.7) from the new drafts are also supported. The known breaking changes are listed below, but there may be others that haven't been identified.
32
+
33
+ ### Breaking Changes
34
+
35
+ - The default meta schema is now Draft 2020-12. Other meta schemas can be specified using `meta_schema`.
36
+ - Schemas use `json-schemer://schema` as the default base URI. Relative `$id` and `$ref` values are joined to the default base URI and are always absolute. For example, the schema `{ '$id' => 'foo', '$ref' => 'bar' }` uses `json-schemer://schema/foo` as the base URI and passes `json-schemer://schema/bar` to the ref resolver. For relative refs, `URI#path` can be used in the ref resolver to access the relative portion, ie: `URI('json-schemer://schema/bar').path => "/bar"`.
37
+ - Property validation hooks (`before_property_validation` and `after_property_validation`) run immediately before and after `properties` validation. Previously, `before_property_validation` ran before all "object" validations (`dependencies`, `patternProperties`, `additionalProperties`, etc) and `after_property_validation` was called after them.
38
+ - `insert_property_defaults` now inserts defaults in conditional subschemas when possible (if there's only one default or if there's only one unique default from a valid subtree).
39
+ - Error output
40
+ - Special characters in `schema_pointer` are no longer percent encoded (eg, `definitions/foo\"bar` instead of `/definitions/foo%22bar`)
41
+ - Keyword validation order changed so errors may be returned in a different order (eg, `items` errors before `contains`).
42
+ - Array `dependencies` return `"type": "dependencies"` errors instead of `"required"` and point to the schema that contains the `dependencies` keyword.
43
+ - `not` errors point to the schema that contains the `not` keyword (instead of the schema defined by the `not` keyword).
44
+ - Custom keyword errors are now always wrapped in regular error hashes. Returned strings are used to set `type`:
45
+ ```
46
+ >> JSONSchemer.schema({ 'x' => 'y' }, :keywords => { 'x' => proc { false } }).validate({}).to_a
47
+ => [{"data"=>{}, "data_pointer"=>"", "schema"=>{"x"=>"y"}, "schema_pointer"=>"", "root_schema"=>{"x"=>"y"}, "type"=>"x"}]
48
+ >> JSONSchemer.schema({ 'x' => 'y' }, :keywords => { 'x' => proc { 'wrong!' } }).validate({}).to_a
49
+ => [{"data"=>{}, "data_pointer"=>"", "schema"=>{"x"=>"y"}, "schema_pointer"=>"", "root_schema"=>{"x"=>"y"}, "type"=>"wrong!"}]
50
+ ```
51
+
52
+ [2.0.0]: https://github.com/davishmcclurg/json_schemer/releases/tag/v2.0.0
53
+
54
+ ## [1.0.0] - 2023-05-26
55
+
56
+ ### Breaking Changes
57
+
58
+ - Ruby 2.4 is no longer supported.
59
+ - The default `regexp_resolver` is now `ruby`, which passes patterns directly to `Regexp`. The previous default, `ecma`, rewrites patterns to behave more like Javascript (ECMA-262) regular expressions:
60
+ - Beginning of string: `^` -> `\A`
61
+ - End of string: `$` -> `\z`
62
+ - Space: `\s` -> `[\t\r\n\f\v\uFEFF\u2029\p{Zs}]`
63
+ - Non-space: `\S` -> `[^\t\r\n\f\v\uFEFF\u2029\p{Zs}]`
64
+ - Invalid ECMA-262 regular expressions raise `JSONSchemer::InvalidEcmaRegexp` when `regexp_resolver` is set to `ecma`.
65
+ - Embedded subschemas (ie, subschemas referenced by `$id`) can only be found under "known" keywords (eg, `definitions`). Previously, the entire schema object was scanned for `$id`.
66
+ - Empty fragments are now removed from `$ref` URIs before calling `ref_resolver`.
67
+ - Refs that are fragment-only JSON pointers with special characters must use the proper encoding (eg, `"$ref": "#/definitions/some-%7Bid%7D"`).
68
+
69
+ [1.0.0]: https://github.com/davishmcclurg/json_schemer/releases/tag/v1.0.0
data/Gemfile.lock CHANGED
@@ -1,31 +1,49 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- json_schemer (0.2.18)
5
- ecma-re-validator (~> 0.3)
4
+ json_schemer (2.1.1)
6
5
  hana (~> 1.3)
7
6
  regexp_parser (~> 2.0)
8
- uri_template (~> 0.7)
7
+ simpleidn (~> 0.2)
9
8
 
10
9
  GEM
11
10
  remote: https://rubygems.org/
12
11
  specs:
13
- ecma-re-validator (0.3.0)
14
- regexp_parser (~> 2.0)
12
+ concurrent-ruby (1.2.2)
13
+ docile (1.4.0)
15
14
  hana (1.3.7)
16
- minitest (5.14.3)
17
- rake (13.0.1)
18
- regexp_parser (2.1.1)
19
- uri_template (0.7.0)
15
+ i18n (1.14.1)
16
+ concurrent-ruby (~> 1.0)
17
+ i18n-debug (1.2.0)
18
+ i18n (< 2)
19
+ minitest (5.15.0)
20
+ rake (13.0.6)
21
+ regexp_parser (2.8.2)
22
+ simplecov (0.22.0)
23
+ docile (~> 1.1)
24
+ simplecov-html (~> 0.11)
25
+ simplecov_json_formatter (~> 0.1)
26
+ simplecov-html (0.12.3)
27
+ simplecov_json_formatter (0.1.4)
28
+ simpleidn (0.2.1)
29
+ unf (~> 0.1.4)
30
+ unf (0.1.4)
31
+ unf_ext
32
+ unf (0.1.4-java)
33
+ unf_ext (0.0.9.1)
20
34
 
21
35
  PLATFORMS
36
+ java
22
37
  ruby
23
38
 
24
39
  DEPENDENCIES
25
40
  bundler (~> 2.0)
41
+ i18n
42
+ i18n-debug
26
43
  json_schemer!
27
44
  minitest (~> 5.0)
28
45
  rake (~> 13.0)
46
+ simplecov (~> 0.22)
29
47
 
30
48
  BUNDLED WITH
31
- 2.2.11
49
+ 2.3.25
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # JSONSchemer
2
2
 
3
- JSON Schema validator. Supports drafts 4, 6, and 7.
3
+ JSON Schema validator. Supports drafts 4, 6, 7, 2019-09, 2020-12, OpenAPI 3.0, and OpenAPI 3.1.
4
4
 
5
5
  ## Installation
6
6
 
@@ -45,7 +45,13 @@ schemer.valid?({ 'abc' => 10 })
45
45
  # error validation (`validate` returns an enumerator)
46
46
 
47
47
  schemer.validate({ 'abc' => 10 }).to_a
48
- # => [{"data"=>10, "schema"=>{"type"=>"integer", "minimum"=>11}, "pointer"=>"#/abc", "type"=>"minimum"}]
48
+ # => [{"data"=>10,
49
+ # "data_pointer"=>"/abc",
50
+ # "schema"=>{"type"=>"integer", "minimum"=>11},
51
+ # "schema_pointer"=>"/properties/abc",
52
+ # "root_schema"=>{"type"=>"object", "properties"=>{"abc"=>{"type"=>"integer", "minimum"=>11}}},
53
+ # "type"=>"minimum",
54
+ # "error"=>"number at `/abc` is less than: 11"}]
49
55
 
50
56
  # default property values
51
57
 
@@ -74,6 +80,80 @@ schemer = JSONSchemer.schema(schema)
74
80
 
75
81
  schema = '{ "type": "integer" }'
76
82
  schemer = JSONSchemer.schema(schema)
83
+
84
+ # schema validation
85
+
86
+ JSONSchemer.valid_schema?({ '$id' => 'valid' })
87
+ # => true
88
+
89
+ JSONSchemer.validate_schema({ '$id' => '#invalid' }).to_a
90
+ # => [{"data"=>"#invalid",
91
+ # "data_pointer"=>"/$id",
92
+ # "schema"=>{"$ref"=>"#/$defs/uriReferenceString", "$comment"=>"Non-empty fragments not allowed.", "pattern"=>"^[^#]*#?$"},
93
+ # "schema_pointer"=>"/properties/$id",
94
+ # "root_schema"=>{...meta schema},
95
+ # "type"=>"pattern",
96
+ # "error"=>"string at `/$id` does not match pattern: ^[^#]*#?$"}]
97
+
98
+ JSONSchemer.schema({ '$id' => 'valid' }).valid_schema?
99
+ # => true
100
+
101
+ JSONSchemer.schema({ '$id' => '#invalid' }).validate_schema.to_a
102
+ # => [{"data"=>"#invalid",
103
+ # "data_pointer"=>"/$id",
104
+ # "schema"=>{"$ref"=>"#/$defs/uriReferenceString", "$comment"=>"Non-empty fragments not allowed.", "pattern"=>"^[^#]*#?$"},
105
+ # "schema_pointer"=>"/properties/$id",
106
+ # "root_schema"=>{...meta schema},
107
+ # "type"=>"pattern",
108
+ # "error"=>"string at `/$id` does not match pattern: ^[^#]*#?$"}]
109
+
110
+ # subschemas
111
+
112
+ schema = {
113
+ 'type' => 'integer',
114
+ '$defs' => {
115
+ 'foo' => {
116
+ 'type' => 'string'
117
+ }
118
+ }
119
+ }
120
+ schemer = JSONSchemer.schema(schema)
121
+
122
+ schemer.ref('#/$defs/foo').validate(1).to_a
123
+ # => [{"data"=>1,
124
+ # "data_pointer"=>"",
125
+ # "schema"=>{"type"=>"string"},
126
+ # "schema_pointer"=>"/$defs/foo",
127
+ # "root_schema"=>{"type"=>"integer", "$defs"=>{"foo"=>{"type"=>"string"}}},
128
+ # "type"=>"string",
129
+ # "error"=>"value at root is not a string"}]
130
+
131
+ # schema bundling (https://json-schema.org/draft/2020-12/json-schema-core.html#section-9.3)
132
+
133
+ schema = {
134
+ '$id' => 'http://example.com/schema',
135
+ 'allOf' => [
136
+ { '$ref' => 'schema/one' },
137
+ { '$ref' => 'schema/two' }
138
+ ]
139
+ }
140
+ refs = {
141
+ URI('http://example.com/schema/one') => {
142
+ 'type' => 'integer'
143
+ },
144
+ URI('http://example.com/schema/two') => {
145
+ 'minimum' => 11
146
+ }
147
+ }
148
+ schemer = JSONSchemer.schema(schema, :ref_resolver => refs.to_proc)
149
+
150
+ schemer.bundle
151
+ # => {"$id"=>"http://example.com/schema",
152
+ # "allOf"=>[{"$ref"=>"schema/one"}, {"$ref"=>"schema/two"}],
153
+ # "$schema"=>"https://json-schema.org/draft/2020-12/schema",
154
+ # "$defs"=>
155
+ # {"http://example.com/schema/one"=>{"type"=>"integer", "$id"=>"http://example.com/schema/one", "$schema"=>"https://json-schema.org/draft/2020-12/schema"},
156
+ # "http://example.com/schema/two"=>{"minimum"=>11, "$id"=>"http://example.com/schema/two", "$schema"=>"https://json-schema.org/draft/2020-12/schema"}}}
77
157
  ```
78
158
 
79
159
  ## Options
@@ -82,11 +162,55 @@ schemer = JSONSchemer.schema(schema)
82
162
  JSONSchemer.schema(
83
163
  schema,
84
164
 
85
- # validate `format` (https://tools.ietf.org/html/draft-handrews-json-schema-validation-00#section-7)
165
+ # meta schema to use for vocabularies (keyword behavior) and schema validation
166
+ # String/JSONSchemer::Schema
167
+ # 'https://json-schema.org/draft/2020-12/schema': JSONSchemer.draft202012
168
+ # 'https://json-schema.org/draft/2019-09/schema': JSONSchemer.draft201909
169
+ # 'http://json-schema.org/draft-07/schema#': JSONSchemer.draft7
170
+ # 'http://json-schema.org/draft-06/schema#': JSONSchemer.draft6
171
+ # 'http://json-schema.org/draft-04/schema#': JSONSchemer.draft4
172
+ # 'http://json-schema.org/schema#': JSONSchemer.draft4
173
+ # 'https://spec.openapis.org/oas/3.1/dialect/base': JSONSchemer.openapi31
174
+ # 'json-schemer://openapi30/schema': JSONSchemer.openapi30
175
+ # default: JSONSchemer.draft202012
176
+ meta_schema: 'https://json-schema.org/draft/2020-12/schema',
177
+
178
+ # validate `format` (https://json-schema.org/draft/2020-12/json-schema-validation.html#section-7)
86
179
  # true/false
87
180
  # default: true
88
181
  format: true,
89
182
 
183
+ # custom formats
184
+ formats: {
185
+ 'int32' => proc do |instance, _format|
186
+ instance.is_a?(Integer) && instance.bit_length <= 32
187
+ end,
188
+ # disable specific format
189
+ 'email' => false
190
+ },
191
+
192
+ # custom content encodings
193
+ # only `base64` is available by default
194
+ content_encodings: {
195
+ # return [success, annotation] tuple
196
+ 'urlsafe_base64' => proc do |instance|
197
+ [true, Base64.urlsafe_decode64(instance)]
198
+ rescue
199
+ [false, nil]
200
+ end
201
+ },
202
+
203
+ # custom content media types
204
+ # only `application/json` is available by default
205
+ content_media_types: {
206
+ # return [success, annotation] tuple
207
+ 'text/csv' => proc do |instance|
208
+ [true, CSV.parse(instance)]
209
+ rescue
210
+ [false, nil]
211
+ end
212
+ },
213
+
90
214
  # insert default property values during validation
91
215
  # true/false
92
216
  # default: false
@@ -110,10 +234,261 @@ JSONSchemer.schema(
110
234
  # 'net/http'/proc/lambda/respond_to?(:call)
111
235
  # 'net/http': proc { |uri| JSON.parse(Net::HTTP.get(uri)) }
112
236
  # default: proc { |uri| raise UnknownRef, uri.to_s }
113
- ref_resolver: 'net/http'
237
+ ref_resolver: 'net/http',
238
+
239
+ # use different method to match regexes
240
+ # 'ruby'/'ecma'/proc/lambda/respond_to?(:call)
241
+ # 'ruby': proc { |pattern| Regexp.new(pattern) }
242
+ # default: 'ruby'
243
+ regexp_resolver: proc do |pattern|
244
+ RE2::Regexp.new(pattern)
245
+ end,
246
+
247
+ # output formatting (https://json-schema.org/draft/2020-12/json-schema-core.html#section-12)
248
+ # 'classic'/'flag'/'basic'/'detailed'/'verbose'
249
+ # default: 'classic'
250
+ output_format: 'basic',
251
+
252
+ # validate `readOnly`/`writeOnly` keywords (https://spec.openapis.org/oas/v3.0.3#fixed-fields-19)
253
+ # 'read'/'write'/nil
254
+ # default: nil
255
+ access_mode: 'read'
114
256
  )
115
257
  ```
116
258
 
259
+ ## Custom Error Messages
260
+
261
+ Error messages can be customized using the `x-error` keyword and/or [I18n](https://github.com/ruby-i18n/i18n) translations. `x-error` takes precedence if both are defined.
262
+
263
+ ### `x-error` Keyword
264
+
265
+ ```ruby
266
+ # override all errors for a schema
267
+ schemer = JSONSchemer.schema({
268
+ 'type' => 'string',
269
+ 'x-error' => 'custom error for schema and all keywords'
270
+ })
271
+
272
+ schemer.validate(1).first
273
+ # => {"data"=>1,
274
+ # "data_pointer"=>"",
275
+ # "schema"=>{"type"=>"string", "x-error"=>"custom error for schema and all keywords"},
276
+ # "schema_pointer"=>"",
277
+ # "root_schema"=>{"type"=>"string", "x-error"=>"custom error for schema and all keywords"},
278
+ # "type"=>"string",
279
+ # "error"=>"custom error for schema and all keywords",
280
+ # "x-error"=>true}
281
+
282
+ schemer.validate(1, :output_format => 'basic')
283
+ # => {"valid"=>false,
284
+ # "keywordLocation"=>"",
285
+ # "absoluteKeywordLocation"=>"json-schemer://schema#",
286
+ # "instanceLocation"=>"",
287
+ # "error"=>"custom error for schema and all keywords",
288
+ # "x-error"=>true,
289
+ # "errors"=>#<Enumerator: ...>}
290
+
291
+ # keyword-specific errors
292
+ schemer = JSONSchemer.schema({
293
+ 'type' => 'string',
294
+ 'minLength' => 10,
295
+ 'x-error' => {
296
+ 'type' => 'custom error for `type` keyword',
297
+ # special `^` keyword for schema-level error
298
+ '^' => 'custom error for schema',
299
+ # same behavior as when `x-error` is a string
300
+ '*' => 'fallback error for schema and all keywords'
301
+ }
302
+ })
303
+
304
+ schemer.validate(1).map { _1.fetch('error') }
305
+ # => ["custom error for `type` keyword"]
306
+
307
+ schemer.validate('1').map { _1.fetch('error') }
308
+ # => ["custom error for schema and all keywords"]
309
+
310
+ schemer.validate(1, :output_format => 'basic').fetch('error')
311
+ # => "custom error for schema"
312
+
313
+ # variable interpolation (instance/instanceLocation/keywordLocation/absoluteKeywordLocation)
314
+ schemer = JSONSchemer.schema({
315
+ '$id' => 'https://example.com/schema',
316
+ 'properties' => {
317
+ 'abc' => {
318
+ 'type' => 'string',
319
+ 'x-error' => <<~ERROR
320
+ instance: %{instance}
321
+ instance location: %{instanceLocation}
322
+ keyword location: %{keywordLocation}
323
+ absolute keyword location: %{absoluteKeywordLocation}
324
+ ERROR
325
+ }
326
+ }
327
+ })
328
+
329
+ puts schemer.validate({ 'abc' => 1 }).first.fetch('error')
330
+ # instance: 1
331
+ # instance location: /abc
332
+ # keyword location: /properties/abc/type
333
+ # absolute keyword location: https://example.com/schema#/properties/abc/type
334
+ ```
335
+
336
+ ### I18n
337
+
338
+ When the [I18n gem](https://github.com/ruby-i18n/i18n) is loaded, custom error messages are looked up under the `json_schemer` key. It may be necessary to restart your application after adding the root key because the existence check is cached for performance reasons.
339
+
340
+ Translation keys are looked up in this order:
341
+
342
+ 1. `$LOCALE.json_schemer.errors.$ABSOLUTE_KEYWORD_LOCATION`
343
+ 2. `$LOCALE.json_schemer.errors.$SCHEMA_ID.$KEYWORD_LOCATION`
344
+ 3. `$LOCALE.json_schemer.errors.$KEYWORD_LOCATION`
345
+ 4. `$LOCALE.json_schemer.errors.$SCHEMA_ID.$KEYWORD`
346
+ 5. `$LOCALE.json_schemer.errors.$SCHEMA_ID.*`
347
+ 6. `$LOCALE.json_schemer.errors.$META_SCHEMA_ID.$KEYWORD`
348
+ 7. `$LOCALE.json_schemer.errors.$META_SCHEMA_ID.*`
349
+ 8. `$LOCALE.json_schemer.errors.$KEYWORD`
350
+ 9. `$LOCALE.json_schemer.errors.*`
351
+
352
+ Example translations file:
353
+
354
+ ```yaml
355
+ en:
356
+ json_schemer:
357
+ errors:
358
+ 'https://example.com/schema#/properties/abc/type': custom error for absolute keyword location
359
+ 'https://example.com/schema':
360
+ '#/properties/abc/type': custom error for keyword location, nested under schema $id
361
+ 'type': custom error for `type` keyword, nested under schema $id
362
+ '^': custom error for schema, nested under schema $id
363
+ '*': fallback error for schema and all keywords, nested under schema $id
364
+ '#/properties/abc/type': custom error for keyword location
365
+ 'http://json-schema.org/draft-07/schema#':
366
+ 'type': custom error for `type` keyword, nested under meta-schema $id ($schema)
367
+ '^': custom error for schema, nested under meta-schema $id
368
+ '*': fallback error for schema and all keywords, nested under meta-schema $id ($schema)
369
+ 'type': custom error for `type` keyword
370
+ '^': custom error for schema
371
+ # variable interpolation (instance/instanceLocation/keywordLocation/absoluteKeywordLocation)
372
+ '*': |
373
+ fallback error for schema and all keywords
374
+ instance: %{instance}
375
+ instance location: %{instanceLocation}
376
+ keyword location: %{keywordLocation}
377
+ absolute keyword location: %{absoluteKeywordLocation}
378
+ ```
379
+
380
+ And output:
381
+
382
+ ```ruby
383
+ require 'i18n'
384
+ I18n.locale = :en # $LOCALE=en
385
+
386
+ schemer = JSONSchemer.schema({
387
+ '$id' => 'https://example.com/schema', # $SCHEMA_ID=https://example.com/schema
388
+ '$schema' => 'http://json-schema.org/draft-07/schema#', # $META_SCHEMA_ID=http://json-schema.org/draft-07/schema#
389
+ 'properties' => {
390
+ 'abc' => {
391
+ 'type' => 'integer' # $KEYWORD=type
392
+ } # $KEYWORD_LOCATION=#/properties/abc/type
393
+ } # $ABSOLUTE_KEYWORD_LOCATION=https://example.com/schema#/properties/abc/type
394
+ })
395
+
396
+ schemer.validate({ 'abc' => 'not-an-integer' }).first
397
+ # => {"data"=>"not-an-integer",
398
+ # "data_pointer"=>"/abc",
399
+ # "schema"=>{"type"=>"integer"},
400
+ # "schema_pointer"=>"/properties/abc",
401
+ # "root_schema"=>{"$id"=>"https://example.com/schema", "$schema"=>"http://json-schema.org/draft-07/schema#", "properties"=>{"abc"=>{"type"=>"integer"}}},
402
+ # "type"=>"integer",
403
+ # "error"=>"custom error for absolute keyword location",
404
+ # "i18n"=>true
405
+ ```
406
+
407
+ In the example above, custom error messsages are looked up using the following keys (in order until one is found):
408
+
409
+ 1. `en.json_schemer.errors.'https://example.com/schema#/properties/abc/type'`
410
+ 2. `en.json_schemer.errors.'https://example.com/schema'.'#/properties/abc/type'`
411
+ 3. `en.json_schemer.errors.'#/properties/abc/type'`
412
+ 4. `en.json_schemer.errors.'https://example.com/schema'.type`
413
+ 5. `en.json_schemer.errors.'https://example.com/schema'.*`
414
+ 6. `en.json_schemer.errors.'http://json-schema.org/draft-07/schema#'.type`
415
+ 7. `en.json_schemer.errors.'http://json-schema.org/draft-07/schema#'.*`
416
+ 8. `en.json_schemer.errors.type`
417
+ 9. `en.json_schemer.errors.*`
418
+
419
+ ## OpenAPI
420
+
421
+ ```ruby
422
+ document = JSONSchemer.openapi({
423
+ 'openapi' => '3.1.0',
424
+ 'info' => {
425
+ 'title' => 'example'
426
+ },
427
+ 'components' => {
428
+ 'schemas' => {
429
+ 'example' => {
430
+ 'type' => 'integer'
431
+ }
432
+ }
433
+ }
434
+ })
435
+
436
+ # document validation using meta schema
437
+
438
+ document.valid?
439
+ # => false
440
+
441
+ document.validate.to_a
442
+ # => [{"data"=>{"title"=>"example"},
443
+ # "data_pointer"=>"/info",
444
+ # "schema"=>{...info schema},
445
+ # "schema_pointer"=>"/$defs/info",
446
+ # "root_schema"=>{...meta schema},
447
+ # "type"=>"required",
448
+ # "details"=>{"missing_keys"=>["version"]}},
449
+ # ...]
450
+
451
+ # data validation using schema by name (in `components/schemas`)
452
+
453
+ document.schema('example').valid?(1)
454
+ # => true
455
+
456
+ document.schema('example').valid?('one')
457
+ # => false
458
+
459
+ # data validation using schema by ref
460
+
461
+ document.ref('#/components/schemas/example').valid?(1)
462
+ # => true
463
+
464
+ document.ref('#/components/schemas/example').valid?('one')
465
+ # => false
466
+ ```
467
+
468
+ ## CLI
469
+
470
+ The `json_schemer` executable takes a JSON schema file as the first argument followed by one or more JSON data files to validate. If there are any validation errors, it outputs them and returns an error code.
471
+
472
+ Validation errors are output as single-line JSON objects. The `--errors` option can be used to limit the number of errors returned or prevent output entirely (and fail fast).
473
+
474
+ The schema or data can also be read from stdin using `-`.
475
+
476
+ ```
477
+ % json_schemer --help
478
+ Usage:
479
+ json_schemer [options] <schema> <data>...
480
+ json_schemer [options] <schema> -
481
+ json_schemer [options] - <data>...
482
+ json_schemer -h | --help
483
+ json_schemer --version
484
+
485
+ Options:
486
+ -e, --errors MAX Maximum number of errors to output
487
+ Use "0" to validate with no output
488
+ -h, --help Show help
489
+ -v, --version Show version
490
+ ```
491
+
117
492
  ## Development
118
493
 
119
494
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'open-uri'
4
+ require 'csv'
5
+
6
+ # https://datatracker.ietf.org/doc/html/rfc5892#appendix-A.1
7
+ # https://datatracker.ietf.org/doc/html/rfc5892#appendix-A.2
8
+
9
+ csv_options = { :col_sep => ';', :skip_blanks => true, :skip_lines => /\A#/ }
10
+
11
+ unicode_data = URI('https://www.unicode.org/Public/UCD/latest/ucd/UnicodeData.txt')
12
+ derived_joining_type = URI('https://www.unicode.org/Public/UCD/latest/ucd/extracted/DerivedJoiningType.txt')
13
+
14
+ # https://www.unicode.org/reports/tr44/#Canonical_Combining_Class_Values
15
+ virama_canonical_combining_class = '9'
16
+
17
+ virama_codes = CSV.new(unicode_data.read, **csv_options).select do |code, _name, _category, canonical_combining_class|
18
+ canonical_combining_class == virama_canonical_combining_class
19
+ end.map(&:first)
20
+
21
+ # https://www.unicode.org/reports/tr44/#Default_Values
22
+ # https://www.unicode.org/reports/tr44/#Derived_Extracted
23
+ codes_by_joining_type = CSV.new(derived_joining_type.read, **csv_options).group_by do |_code, joining_type|
24
+ joining_type.gsub(/#.+/, '').strip
25
+ end.transform_values do |rows|
26
+ rows.map do |code, _joining_type|
27
+ code.strip
28
+ end
29
+ end
30
+
31
+ def codes_to_character_class(codes)
32
+ characters = codes.map do |code|
33
+ code.gsub(/(\h+)/, '\u{\1}').gsub('..', '-')
34
+ end
35
+ "[#{characters.join}]"
36
+ end
37
+
38
+ puts "VIRAMA_CHARACTER_CLASS = '#{codes_to_character_class(virama_codes)}'"
39
+
40
+ codes_by_joining_type.slice('L', 'D', 'T', 'R').each do |joining_type, codes|
41
+ puts "JOINING_TYPE_#{joining_type}_CHARACTER_CLASS = '#{codes_to_character_class(codes)}'"
42
+ end
data/bin/rake ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rake' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rake", "rake")