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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +3 -7
- data/CHANGELOG.md +69 -0
- data/Gemfile.lock +28 -10
- data/README.md +379 -4
- data/bin/hostname_character_classes +42 -0
- data/bin/rake +29 -0
- data/exe/json_schemer +62 -0
- data/json_schemer.gemspec +6 -12
- data/lib/json_schemer/cached_resolver.rb +16 -0
- data/lib/json_schemer/content.rb +18 -0
- data/lib/json_schemer/draft201909/meta.rb +320 -0
- data/lib/json_schemer/draft201909/vocab/applicator.rb +104 -0
- data/lib/json_schemer/draft201909/vocab/core.rb +45 -0
- data/lib/json_schemer/draft201909/vocab.rb +31 -0
- data/lib/json_schemer/draft202012/meta.rb +364 -0
- data/lib/json_schemer/draft202012/vocab/applicator.rb +382 -0
- data/lib/json_schemer/draft202012/vocab/content.rb +52 -0
- data/lib/json_schemer/draft202012/vocab/core.rb +160 -0
- data/lib/json_schemer/draft202012/vocab/format_annotation.rb +23 -0
- data/lib/json_schemer/draft202012/vocab/format_assertion.rb +23 -0
- data/lib/json_schemer/draft202012/vocab/meta_data.rb +30 -0
- data/lib/json_schemer/draft202012/vocab/unevaluated.rb +94 -0
- data/lib/json_schemer/draft202012/vocab/validation.rb +286 -0
- data/lib/json_schemer/draft202012/vocab.rb +105 -0
- data/lib/json_schemer/draft4/meta.rb +161 -0
- data/lib/json_schemer/draft4/vocab/validation.rb +39 -0
- data/lib/json_schemer/draft4/vocab.rb +18 -0
- data/lib/json_schemer/draft6/meta.rb +172 -0
- data/lib/json_schemer/draft6/vocab.rb +16 -0
- data/lib/json_schemer/draft7/meta.rb +183 -0
- data/lib/json_schemer/draft7/vocab/validation.rb +69 -0
- data/lib/json_schemer/draft7/vocab.rb +30 -0
- data/lib/json_schemer/ecma_regexp.rb +51 -0
- data/lib/json_schemer/errors.rb +1 -0
- data/lib/json_schemer/format/duration.rb +23 -0
- data/lib/json_schemer/format/email.rb +56 -0
- data/lib/json_schemer/format/hostname.rb +58 -0
- data/lib/json_schemer/format/json_pointer.rb +18 -0
- data/lib/json_schemer/format/uri_template.rb +34 -0
- data/lib/json_schemer/format.rb +129 -109
- data/lib/json_schemer/keyword.rb +53 -0
- data/lib/json_schemer/location.rb +25 -0
- data/lib/json_schemer/openapi.rb +40 -0
- data/lib/json_schemer/openapi30/document.rb +1672 -0
- data/lib/json_schemer/openapi30/meta.rb +32 -0
- data/lib/json_schemer/openapi30/vocab/base.rb +18 -0
- data/lib/json_schemer/openapi30/vocab.rb +12 -0
- data/lib/json_schemer/openapi31/document.rb +1557 -0
- data/lib/json_schemer/openapi31/meta.rb +136 -0
- data/lib/json_schemer/openapi31/vocab/base.rb +127 -0
- data/lib/json_schemer/openapi31/vocab.rb +18 -0
- data/lib/json_schemer/output.rb +56 -0
- data/lib/json_schemer/result.rb +229 -0
- data/lib/json_schemer/schema.rb +423 -0
- data/lib/json_schemer/version.rb +1 -1
- data/lib/json_schemer.rb +218 -28
- metadata +98 -25
- data/lib/json_schemer/cached_ref_resolver.rb +0 -14
- data/lib/json_schemer/schema/base.rb +0 -658
- data/lib/json_schemer/schema/draft4.rb +0 -44
- data/lib/json_schemer/schema/draft6.rb +0 -25
- data/lib/json_schemer/schema/draft7.rb +0 -32
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1981782f2342c7123beb0374680037bc11f3be72a8e4be0a213cfb7691643415
|
|
4
|
+
data.tar.gz: e8b6c605ba1ce2a2751ac90167f53c27fad975140ab9a0e8328c2b04ce8fe732
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4d4c6e6c3660560a602b5610559efe37b5839f1142af6486e350bd207cad49e37a00f397d555878957b14951da95745092240bd14417e2ffd02826f053099952
|
|
7
|
+
data.tar.gz: 8dc561414463ceeacf7bbd0be95be9c441a50919cdb839ff8366fa0268d96d91e4947985bd7bab406f3c163e913bf7ca923a38e22d777d2549af3329f91c14f2
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -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.
|
|
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@
|
|
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:
|
|
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 (
|
|
5
|
-
ecma-re-validator (~> 0.3)
|
|
4
|
+
json_schemer (2.1.1)
|
|
6
5
|
hana (~> 1.3)
|
|
7
6
|
regexp_parser (~> 2.0)
|
|
8
|
-
|
|
7
|
+
simpleidn (~> 0.2)
|
|
9
8
|
|
|
10
9
|
GEM
|
|
11
10
|
remote: https://rubygems.org/
|
|
12
11
|
specs:
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
concurrent-ruby (1.2.2)
|
|
13
|
+
docile (1.4.0)
|
|
15
14
|
hana (1.3.7)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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.
|
|
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
|
|
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,
|
|
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
|
-
#
|
|
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")
|