json_schemer 0.2.18 → 2.2.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +7 -7
  3. data/CHANGELOG.md +89 -0
  4. data/Gemfile.lock +35 -10
  5. data/README.md +402 -6
  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 +9 -12
  10. data/lib/json_schemer/cached_resolver.rb +16 -0
  11. data/lib/json_schemer/configuration.rb +31 -0
  12. data/lib/json_schemer/content.rb +18 -0
  13. data/lib/json_schemer/draft201909/meta.rb +320 -0
  14. data/lib/json_schemer/draft201909/vocab/applicator.rb +104 -0
  15. data/lib/json_schemer/draft201909/vocab/core.rb +45 -0
  16. data/lib/json_schemer/draft201909/vocab.rb +31 -0
  17. data/lib/json_schemer/draft202012/meta.rb +364 -0
  18. data/lib/json_schemer/draft202012/vocab/applicator.rb +382 -0
  19. data/lib/json_schemer/draft202012/vocab/content.rb +52 -0
  20. data/lib/json_schemer/draft202012/vocab/core.rb +160 -0
  21. data/lib/json_schemer/draft202012/vocab/format_annotation.rb +23 -0
  22. data/lib/json_schemer/draft202012/vocab/format_assertion.rb +23 -0
  23. data/lib/json_schemer/draft202012/vocab/meta_data.rb +30 -0
  24. data/lib/json_schemer/draft202012/vocab/unevaluated.rb +104 -0
  25. data/lib/json_schemer/draft202012/vocab/validation.rb +286 -0
  26. data/lib/json_schemer/draft202012/vocab.rb +105 -0
  27. data/lib/json_schemer/draft4/meta.rb +161 -0
  28. data/lib/json_schemer/draft4/vocab/validation.rb +39 -0
  29. data/lib/json_schemer/draft4/vocab.rb +18 -0
  30. data/lib/json_schemer/draft6/meta.rb +172 -0
  31. data/lib/json_schemer/draft6/vocab.rb +16 -0
  32. data/lib/json_schemer/draft7/meta.rb +183 -0
  33. data/lib/json_schemer/draft7/vocab/validation.rb +69 -0
  34. data/lib/json_schemer/draft7/vocab.rb +30 -0
  35. data/lib/json_schemer/ecma_regexp.rb +51 -0
  36. data/lib/json_schemer/errors.rb +1 -0
  37. data/lib/json_schemer/format/duration.rb +23 -0
  38. data/lib/json_schemer/format/email.rb +56 -0
  39. data/lib/json_schemer/format/hostname.rb +58 -0
  40. data/lib/json_schemer/format/json_pointer.rb +18 -0
  41. data/lib/json_schemer/format/uri_template.rb +34 -0
  42. data/lib/json_schemer/format.rb +128 -109
  43. data/lib/json_schemer/keyword.rb +56 -0
  44. data/lib/json_schemer/location.rb +25 -0
  45. data/lib/json_schemer/openapi.rb +38 -0
  46. data/lib/json_schemer/openapi30/document.rb +1672 -0
  47. data/lib/json_schemer/openapi30/meta.rb +32 -0
  48. data/lib/json_schemer/openapi30/vocab/base.rb +18 -0
  49. data/lib/json_schemer/openapi30/vocab.rb +12 -0
  50. data/lib/json_schemer/openapi31/document.rb +1557 -0
  51. data/lib/json_schemer/openapi31/meta.rb +136 -0
  52. data/lib/json_schemer/openapi31/vocab/base.rb +127 -0
  53. data/lib/json_schemer/openapi31/vocab.rb +18 -0
  54. data/lib/json_schemer/output.rb +56 -0
  55. data/lib/json_schemer/result.rb +242 -0
  56. data/lib/json_schemer/schema.rb +424 -0
  57. data/lib/json_schemer/version.rb +1 -1
  58. data/lib/json_schemer.rb +243 -29
  59. metadata +141 -25
  60. data/lib/json_schemer/cached_ref_resolver.rb +0 -14
  61. data/lib/json_schemer/schema/base.rb +0 -658
  62. data/lib/json_schemer/schema/draft4.rb +0 -44
  63. data/lib/json_schemer/schema/draft6.rb +0 -25
  64. data/lib/json_schemer/schema/draft7.rb +0 -32
@@ -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")
data/exe/json_schemer ADDED
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'json'
4
+ require 'optparse'
5
+ require 'pathname'
6
+ require 'json_schemer'
7
+
8
+ parser = OptionParser.new('Usage:', 32, ' ')
9
+ parser.separator(" #{parser.program_name} [options] <schema> <data>...")
10
+ parser.separator(" #{parser.program_name} [options] <schema> -")
11
+ parser.separator(" #{parser.program_name} [options] - <data>...")
12
+ parser.separator(" #{parser.program_name} -h | --help")
13
+ parser.separator(" #{parser.program_name} --version")
14
+ parser.separator('')
15
+ parser.separator('Options:')
16
+ parser.on('-e', '--errors MAX', Integer, 'Maximum number of errors to output', 'Use "0" to validate with no output')
17
+ parser.on_tail('-h', '--help', 'Show help')
18
+ parser.on_tail('-v', '--version', 'Show version')
19
+
20
+ options = {}
21
+ parser.parse!(:into => options)
22
+
23
+ if options[:help]
24
+ $stdout.puts(parser)
25
+ exit
26
+ end
27
+
28
+ if options[:version]
29
+ $stdout.puts("#{parser.program_name} #{JSONSchemer::VERSION}")
30
+ exit
31
+ end
32
+
33
+ if ARGV.size == 0
34
+ $stderr.puts("#{parser.program_name}: no schema or data")
35
+ exit(false)
36
+ end
37
+
38
+ if ARGV.size == 1
39
+ $stderr.puts("#{parser.program_name}: no data")
40
+ exit(false)
41
+ end
42
+
43
+ if ARGV.count('-') > 1
44
+ $stderr.puts("#{parser.program_name}: multiple stdin")
45
+ exit(false)
46
+ end
47
+
48
+ errors = 0
49
+ schema = ARGF.file.is_a?(File) ? Pathname.new(ARGF.file.path) : ARGF.file.read
50
+ schemer = JSONSchemer.schema(schema)
51
+
52
+ while ARGV.any?
53
+ data = JSON.parse(ARGF.skip.file.read)
54
+ schemer.validate(data).each do |error|
55
+ exit(false) if options[:errors] == 0
56
+ errors += 1
57
+ $stdout.puts(JSON.generate(error))
58
+ exit(false) if options[:errors] == errors
59
+ end
60
+ end
61
+
62
+ exit(errors.zero?)
data/json_schemer.gemspec CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["David Harsha"]
10
10
  spec.email = ["davishmcclurg@gmail.com"]
11
11
 
12
- spec.summary = "JSON Schema validator. Supports drafts 4, 6, and 7."
12
+ spec.summary = "JSON Schema validator. Supports drafts 4, 6, 7, 2019-09, 2020-12, OpenAPI 3.0, and OpenAPI 3.1."
13
13
  spec.homepage = "https://github.com/davishmcclurg/json_schemer"
14
14
  spec.license = "MIT"
15
15
 
@@ -20,22 +20,19 @@ Gem::Specification.new do |spec|
20
20
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
21
  spec.require_paths = ["lib"]
22
22
 
23
- spec.required_ruby_version = '>= 2.4'
23
+ spec.required_ruby_version = '>= 2.5'
24
24
 
25
25
  spec.add_development_dependency "bundler", "~> 2.0"
26
26
  spec.add_development_dependency "rake", "~> 13.0"
27
27
  spec.add_development_dependency "minitest", "~> 5.0"
28
+ spec.add_development_dependency "simplecov", "~> 0.22"
29
+ spec.add_development_dependency "csv"
30
+ spec.add_development_dependency "i18n"
31
+ spec.add_development_dependency "i18n-debug"
28
32
 
29
- # spec.add_development_dependency "benchmark-ips", "~> 2.7.2"
30
- # spec.add_development_dependency "jschema", "~> 0.2.1"
31
- # spec.add_development_dependency "json-schema", "~> 2.8.0"
32
- # spec.add_development_dependency "json_schema", "~> 0.17.0"
33
- # spec.add_development_dependency "json_validation", "~> 0.1.0"
34
- # spec.add_development_dependency "jsonschema", "~> 2.0.2"
35
- # spec.add_development_dependency "rj_schema", "~> 0.2.0"
36
-
37
- spec.add_runtime_dependency "ecma-re-validator", "~> 0.3"
33
+ spec.add_runtime_dependency "base64"
34
+ spec.add_runtime_dependency "bigdecimal"
38
35
  spec.add_runtime_dependency "hana", "~> 1.3"
39
- spec.add_runtime_dependency "uri_template", "~> 0.7"
40
36
  spec.add_runtime_dependency "regexp_parser", "~> 2.0"
37
+ spec.add_runtime_dependency "simpleidn", "~> 0.2"
41
38
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ class CachedResolver
4
+ def initialize(&resolver)
5
+ @resolver = resolver
6
+ @cache = {}
7
+ end
8
+
9
+ def call(*args)
10
+ @cache[args] = @resolver.call(*args) unless @cache.key?(args)
11
+ @cache[args]
12
+ end
13
+ end
14
+
15
+ class CachedRefResolver < CachedResolver; end
16
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ Configuration = Struct.new(
4
+ :base_uri, :meta_schema, :vocabulary, :format, :formats, :content_encodings, :content_media_types, :keywords,
5
+ :before_property_validation, :after_property_validation, :insert_property_defaults, :property_default_resolver,
6
+ :ref_resolver, :regexp_resolver, :output_format, :resolve_enumerators, :access_mode,
7
+ keyword_init: true
8
+ ) do
9
+ def initialize(
10
+ base_uri: URI('json-schemer://schema'),
11
+ meta_schema: Draft202012::BASE_URI.to_s,
12
+ vocabulary: nil,
13
+ format: true,
14
+ formats: {},
15
+ content_encodings: {},
16
+ content_media_types: {},
17
+ keywords: {},
18
+ before_property_validation: [],
19
+ after_property_validation: [],
20
+ insert_property_defaults: false,
21
+ property_default_resolver: nil,
22
+ ref_resolver: proc { |uri| raise UnknownRef, uri.to_s },
23
+ regexp_resolver: 'ruby',
24
+ output_format: 'classic',
25
+ resolve_enumerators: false,
26
+ access_mode: nil
27
+ )
28
+ super
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module ContentEncoding
4
+ BASE64 = proc do |instance|
5
+ [true, Base64.strict_decode64(instance)]
6
+ rescue
7
+ [false, nil]
8
+ end
9
+ end
10
+
11
+ module ContentMediaType
12
+ JSON = proc do |instance|
13
+ [true, ::JSON.parse(instance)]
14
+ rescue
15
+ [false, nil]
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,320 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module Draft201909
4
+ BASE_URI = URI('https://json-schema.org/draft/2019-09/schema')
5
+ FORMATS = Draft202012::FORMATS
6
+ CONTENT_ENCODINGS = Draft202012::CONTENT_ENCODINGS
7
+ CONTENT_MEDIA_TYPES = Draft202012::CONTENT_MEDIA_TYPES
8
+ SCHEMA = {
9
+ '$schema' => 'https://json-schema.org/draft/2019-09/schema',
10
+ '$id' => 'https://json-schema.org/draft/2019-09/schema',
11
+ '$vocabulary' => {
12
+ 'https://json-schema.org/draft/2019-09/vocab/core' => true,
13
+ 'https://json-schema.org/draft/2019-09/vocab/applicator' => true,
14
+ 'https://json-schema.org/draft/2019-09/vocab/validation' => true,
15
+ 'https://json-schema.org/draft/2019-09/vocab/meta-data' => true,
16
+ 'https://json-schema.org/draft/2019-09/vocab/format' => false,
17
+ 'https://json-schema.org/draft/2019-09/vocab/content' => true
18
+ },
19
+ '$recursiveAnchor' => true,
20
+ 'title' => 'Core and Validation specifications meta-schema',
21
+ 'allOf' => [
22
+ {'$ref' => 'meta/core'},
23
+ {'$ref' => 'meta/applicator'},
24
+ {'$ref' => 'meta/validation'},
25
+ {'$ref' => 'meta/meta-data'},
26
+ {'$ref' => 'meta/format'},
27
+ {'$ref' => 'meta/content'}
28
+ ],
29
+ 'type' => ['object', 'boolean'],
30
+ 'properties' => {
31
+ 'definitions' => {
32
+ '$comment' => 'While no longer an official keyword as it is replaced by $defs, this keyword is retained in the meta-schema to prevent incompatible extensions as it remains in common use.',
33
+ 'type' => 'object',
34
+ 'additionalProperties' => { '$recursiveRef' => '#' },
35
+ 'default' => {}
36
+ },
37
+ 'dependencies' => {
38
+ '$comment' => '"dependencies" is no longer a keyword, but schema authors should avoid redefining it to facilitate a smooth transition to "dependentSchemas" and "dependentRequired"',
39
+ 'type' => 'object',
40
+ 'additionalProperties' => {
41
+ 'anyOf' => [
42
+ { '$recursiveRef' => '#' },
43
+ { '$ref' => 'meta/validation#/$defs/stringArray' }
44
+ ]
45
+ }
46
+ }
47
+ }
48
+ }
49
+
50
+ module Meta
51
+ CORE = {
52
+ '$schema' => 'https://json-schema.org/draft/2019-09/schema',
53
+ '$id' => 'https://json-schema.org/draft/2019-09/meta/core',
54
+ '$recursiveAnchor' => true,
55
+ 'title' => 'Core vocabulary meta-schema',
56
+ 'type' => ['object', 'boolean'],
57
+ 'properties' => {
58
+ '$id' => {
59
+ 'type' => 'string',
60
+ 'format' => 'uri-reference',
61
+ '$comment' => 'Non-empty fragments not allowed.',
62
+ 'pattern' => '^[^#]*#?$'
63
+ },
64
+ '$schema' => {
65
+ 'type' => 'string',
66
+ 'format' => 'uri'
67
+ },
68
+ '$anchor' => {
69
+ 'type' => 'string',
70
+ 'pattern' => '^[A-Za-z][-A-Za-z0-9.:_]*$'
71
+ },
72
+ '$ref' => {
73
+ 'type' => 'string',
74
+ 'format' => 'uri-reference'
75
+ },
76
+ '$recursiveRef' => {
77
+ 'type' => 'string',
78
+ 'format' => 'uri-reference'
79
+ },
80
+ '$recursiveAnchor' => {
81
+ 'type' => 'boolean',
82
+ 'default' => false
83
+ },
84
+ '$vocabulary' => {
85
+ 'type' => 'object',
86
+ 'propertyNames' => {
87
+ 'type' => 'string',
88
+ 'format' => 'uri'
89
+ },
90
+ 'additionalProperties' => {
91
+ 'type' => 'boolean'
92
+ }
93
+ },
94
+ '$comment' => {
95
+ 'type' => 'string'
96
+ },
97
+ '$defs' => {
98
+ 'type' => 'object',
99
+ 'additionalProperties' => { '$recursiveRef' => '#' },
100
+ 'default' => {}
101
+ }
102
+ }
103
+ }
104
+
105
+ APPLICATOR = {
106
+ '$schema' => 'https://json-schema.org/draft/2019-09/schema',
107
+ '$id' => 'https://json-schema.org/draft/2019-09/meta/applicator',
108
+ '$recursiveAnchor' => true,
109
+ 'title' => 'Applicator vocabulary meta-schema',
110
+ 'type' => ['object', 'boolean'],
111
+ 'properties' => {
112
+ 'additionalItems' => { '$recursiveRef' => '#' },
113
+ 'unevaluatedItems' => { '$recursiveRef' => '#' },
114
+ 'items' => {
115
+ 'anyOf' => [
116
+ { '$recursiveRef' => '#' },
117
+ { '$ref' => '#/$defs/schemaArray' }
118
+ ]
119
+ },
120
+ 'contains' => { '$recursiveRef' => '#' },
121
+ 'additionalProperties' => { '$recursiveRef' => '#' },
122
+ 'unevaluatedProperties' => { '$recursiveRef' => '#' },
123
+ 'properties' => {
124
+ 'type' => 'object',
125
+ 'additionalProperties' => { '$recursiveRef' => '#' },
126
+ 'default' => {}
127
+ },
128
+ 'patternProperties' => {
129
+ 'type' => 'object',
130
+ 'additionalProperties' => { '$recursiveRef' => '#' },
131
+ 'propertyNames' => { 'format' => 'regex' },
132
+ 'default' => {}
133
+ },
134
+ 'dependentSchemas' => {
135
+ 'type' => 'object',
136
+ 'additionalProperties' => {
137
+ '$recursiveRef' => '#'
138
+ }
139
+ },
140
+ 'propertyNames' => { '$recursiveRef' => '#' },
141
+ 'if' => { '$recursiveRef' => '#' },
142
+ 'then' => { '$recursiveRef' => '#' },
143
+ 'else' => { '$recursiveRef' => '#' },
144
+ 'allOf' => { '$ref' => '#/$defs/schemaArray' },
145
+ 'anyOf' => { '$ref' => '#/$defs/schemaArray' },
146
+ 'oneOf' => { '$ref' => '#/$defs/schemaArray' },
147
+ 'not' => { '$recursiveRef' => '#' }
148
+ },
149
+ '$defs' => {
150
+ 'schemaArray' => {
151
+ 'type' => 'array',
152
+ 'minItems' => 1,
153
+ 'items' => { '$recursiveRef' => '#' }
154
+ }
155
+ }
156
+ }
157
+
158
+ VALIDATION = {
159
+ '$schema' => 'https://json-schema.org/draft/2019-09/schema',
160
+ '$id' => 'https://json-schema.org/draft/2019-09/meta/validation',
161
+ '$recursiveAnchor' => true,
162
+ 'title' => 'Validation vocabulary meta-schema',
163
+ 'type' => ['object', 'boolean'],
164
+ 'properties' => {
165
+ 'multipleOf' => {
166
+ 'type' => 'number',
167
+ 'exclusiveMinimum' => 0
168
+ },
169
+ 'maximum' => {
170
+ 'type' => 'number'
171
+ },
172
+ 'exclusiveMaximum' => {
173
+ 'type' => 'number'
174
+ },
175
+ 'minimum' => {
176
+ 'type' => 'number'
177
+ },
178
+ 'exclusiveMinimum' => {
179
+ 'type' => 'number'
180
+ },
181
+ 'maxLength' => { '$ref' => '#/$defs/nonNegativeInteger' },
182
+ 'minLength' => { '$ref' => '#/$defs/nonNegativeIntegerDefault0' },
183
+ 'pattern' => {
184
+ 'type' => 'string',
185
+ 'format' => 'regex'
186
+ },
187
+ 'maxItems' => { '$ref' => '#/$defs/nonNegativeInteger' },
188
+ 'minItems' => { '$ref' => '#/$defs/nonNegativeIntegerDefault0' },
189
+ 'uniqueItems' => {
190
+ 'type' => 'boolean',
191
+ 'default' => false
192
+ },
193
+ 'maxContains' => { '$ref' => '#/$defs/nonNegativeInteger' },
194
+ 'minContains' => {
195
+ '$ref' => '#/$defs/nonNegativeInteger',
196
+ 'default' => 1
197
+ },
198
+ 'maxProperties' => { '$ref' => '#/$defs/nonNegativeInteger' },
199
+ 'minProperties' => { '$ref' => '#/$defs/nonNegativeIntegerDefault0' },
200
+ 'required' => { '$ref' => '#/$defs/stringArray' },
201
+ 'dependentRequired' => {
202
+ 'type' => 'object',
203
+ 'additionalProperties' => {
204
+ '$ref' => '#/$defs/stringArray'
205
+ }
206
+ },
207
+ 'const' => true,
208
+ 'enum' => {
209
+ 'type' => 'array',
210
+ 'items' => true
211
+ },
212
+ 'type' => {
213
+ 'anyOf' => [
214
+ { '$ref' => '#/$defs/simpleTypes' },
215
+ {
216
+ 'type' => 'array',
217
+ 'items' => { '$ref' => '#/$defs/simpleTypes' },
218
+ 'minItems' => 1,
219
+ 'uniqueItems' => true
220
+ }
221
+ ]
222
+ }
223
+ },
224
+ '$defs' => {
225
+ 'nonNegativeInteger' => {
226
+ 'type' => 'integer',
227
+ 'minimum' => 0
228
+ },
229
+ 'nonNegativeIntegerDefault0' => {
230
+ '$ref' => '#/$defs/nonNegativeInteger',
231
+ 'default' => 0
232
+ },
233
+ 'simpleTypes' => {
234
+ 'enum' => [
235
+ 'array',
236
+ 'boolean',
237
+ 'integer',
238
+ 'null',
239
+ 'number',
240
+ 'object',
241
+ 'string'
242
+ ]
243
+ },
244
+ 'stringArray' => {
245
+ 'type' => 'array',
246
+ 'items' => { 'type' => 'string' },
247
+ 'uniqueItems' => true,
248
+ 'default' => []
249
+ }
250
+ }
251
+ }
252
+
253
+ META_DATA = {
254
+ '$schema' => 'https://json-schema.org/draft/2019-09/schema',
255
+ '$id' => 'https://json-schema.org/draft/2019-09/meta/meta-data',
256
+ '$recursiveAnchor' => true,
257
+ 'title' => 'Meta-data vocabulary meta-schema',
258
+ 'type' => ['object', 'boolean'],
259
+ 'properties' => {
260
+ 'title' => {
261
+ 'type' => 'string'
262
+ },
263
+ 'description' => {
264
+ 'type' => 'string'
265
+ },
266
+ 'default' => true,
267
+ 'deprecated' => {
268
+ 'type' => 'boolean',
269
+ 'default' => false
270
+ },
271
+ 'readOnly' => {
272
+ 'type' => 'boolean',
273
+ 'default' => false
274
+ },
275
+ 'writeOnly' => {
276
+ 'type' => 'boolean',
277
+ 'default' => false
278
+ },
279
+ 'examples' => {
280
+ 'type' => 'array',
281
+ 'items' => true
282
+ }
283
+ }
284
+ }
285
+
286
+ FORMAT = {
287
+ '$schema' => 'https://json-schema.org/draft/2019-09/schema',
288
+ '$id' => 'https://json-schema.org/draft/2019-09/meta/format',
289
+ '$recursiveAnchor' => true,
290
+ 'title' => 'Format vocabulary meta-schema',
291
+ 'type' => ['object', 'boolean'],
292
+ 'properties' => {
293
+ 'format' => { 'type' => 'string' }
294
+ }
295
+ }
296
+
297
+ CONTENT = {
298
+ '$schema' => 'https://json-schema.org/draft/2019-09/schema',
299
+ '$id' => 'https://json-schema.org/draft/2019-09/meta/content',
300
+ '$recursiveAnchor' => true,
301
+ 'title' => 'Content vocabulary meta-schema',
302
+ 'type' => ['object', 'boolean'],
303
+ 'properties' => {
304
+ 'contentMediaType' => { 'type' => 'string' },
305
+ 'contentEncoding' => { 'type' => 'string' },
306
+ 'contentSchema' => { '$recursiveRef' => '#' }
307
+ }
308
+ }
309
+
310
+ SCHEMAS = {
311
+ URI('https://json-schema.org/draft/2019-09/meta/core') => CORE,
312
+ URI('https://json-schema.org/draft/2019-09/meta/applicator') => APPLICATOR,
313
+ URI('https://json-schema.org/draft/2019-09/meta/validation') => VALIDATION,
314
+ URI('https://json-schema.org/draft/2019-09/meta/meta-data') => META_DATA,
315
+ URI('https://json-schema.org/draft/2019-09/meta/format') => FORMAT,
316
+ URI('https://json-schema.org/draft/2019-09/meta/content') => CONTENT
317
+ }
318
+ end
319
+ end
320
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module Draft201909
4
+ module Vocab
5
+ module Applicator
6
+ class Items < Keyword
7
+ def error(formatted_instance_location:, **)
8
+ "array items at #{formatted_instance_location} do not match `items` schema(s)"
9
+ end
10
+
11
+ def parse
12
+ if value.is_a?(Array)
13
+ value.map.with_index do |subschema, index|
14
+ subschema(subschema, index.to_s)
15
+ end
16
+ else
17
+ subschema(value)
18
+ end
19
+ end
20
+
21
+ def validate(instance, instance_location, keyword_location, context)
22
+ return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Array)
23
+
24
+ nested = if parsed.is_a?(Array)
25
+ instance.take(parsed.size).map.with_index do |item, index|
26
+ parsed.fetch(index).validate_instance(item, join_location(instance_location, index.to_s), join_location(keyword_location, index.to_s), context)
27
+ end
28
+ else
29
+ instance.map.with_index do |item, index|
30
+ parsed.validate_instance(item, join_location(instance_location, index.to_s), keyword_location, context)
31
+ end
32
+ end
33
+
34
+ result(instance, instance_location, keyword_location, nested.all?(&:valid), nested, :annotation => (nested.size - 1))
35
+ end
36
+ end
37
+
38
+ class AdditionalItems < Keyword
39
+ def error(formatted_instance_location:, **)
40
+ "array items at #{formatted_instance_location} do not match `additionalItems` schema"
41
+ end
42
+
43
+ def parse
44
+ subschema(value)
45
+ end
46
+
47
+ def validate(instance, instance_location, keyword_location, context)
48
+ return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Array)
49
+
50
+ evaluated_index = context.adjacent_results[Items]&.annotation
51
+ offset = evaluated_index ? (evaluated_index + 1) : instance.size
52
+
53
+ nested = instance.slice(offset..-1).map.with_index do |item, index|
54
+ parsed.validate_instance(item, join_location(instance_location, (offset + index).to_s), keyword_location, context)
55
+ end
56
+
57
+ result(instance, instance_location, keyword_location, nested.all?(&:valid), nested, :annotation => nested.any?)
58
+ end
59
+ end
60
+
61
+ class UnevaluatedItems < Keyword
62
+ def error(formatted_instance_location:, **)
63
+ "array items at #{formatted_instance_location} do not match `unevaluatedItems` schema"
64
+ end
65
+
66
+ def parse
67
+ subschema(value)
68
+ end
69
+
70
+ def validate(instance, instance_location, keyword_location, context)
71
+ return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Array)
72
+
73
+ unevaluated_items = instance.size.times.to_set
74
+
75
+ context.adjacent_results.each_value do |adjacent_result|
76
+ collect_unevaluated_items(adjacent_result, instance_location, unevaluated_items)
77
+ end
78
+
79
+ nested = unevaluated_items.map do |index|
80
+ parsed.validate_instance(instance.fetch(index), join_location(instance_location, index.to_s), keyword_location, context)
81
+ end
82
+
83
+ result(instance, instance_location, keyword_location, nested.all?(&:valid), nested, :annotation => nested.any?)
84
+ end
85
+
86
+ private
87
+
88
+ def collect_unevaluated_items(result, instance_location, unevaluated_items)
89
+ return unless result.valid && result.instance_location == instance_location
90
+ case result.source
91
+ when Items
92
+ unevaluated_items.subtract(0..result.annotation)
93
+ when AdditionalItems, UnevaluatedItems
94
+ unevaluated_items.clear if result.annotation
95
+ end
96
+ result.nested&.each do |nested_result|
97
+ collect_unevaluated_items(nested_result, instance_location, unevaluated_items)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end