json_schemer 1.0.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -6
  3. data/CHANGELOG.md +25 -0
  4. data/Gemfile.lock +1 -1
  5. data/README.md +137 -14
  6. data/json_schemer.gemspec +1 -1
  7. data/lib/json_schemer/draft201909/meta.rb +335 -0
  8. data/lib/json_schemer/draft201909/vocab/applicator.rb +104 -0
  9. data/lib/json_schemer/draft201909/vocab/core.rb +45 -0
  10. data/lib/json_schemer/draft201909/vocab.rb +31 -0
  11. data/lib/json_schemer/draft202012/meta.rb +361 -0
  12. data/lib/json_schemer/draft202012/vocab/applicator.rb +382 -0
  13. data/lib/json_schemer/draft202012/vocab/content.rb +44 -0
  14. data/lib/json_schemer/draft202012/vocab/core.rb +154 -0
  15. data/lib/json_schemer/draft202012/vocab/format_annotation.rb +31 -0
  16. data/lib/json_schemer/draft202012/vocab/format_assertion.rb +29 -0
  17. data/lib/json_schemer/draft202012/vocab/meta_data.rb +30 -0
  18. data/lib/json_schemer/draft202012/vocab/unevaluated.rb +94 -0
  19. data/lib/json_schemer/draft202012/vocab/validation.rb +286 -0
  20. data/lib/json_schemer/draft202012/vocab.rb +103 -0
  21. data/lib/json_schemer/draft4/meta.rb +155 -0
  22. data/lib/json_schemer/draft4/vocab/validation.rb +39 -0
  23. data/lib/json_schemer/draft4/vocab.rb +18 -0
  24. data/lib/json_schemer/draft6/meta.rb +161 -0
  25. data/lib/json_schemer/draft6/vocab.rb +16 -0
  26. data/lib/json_schemer/draft7/meta.rb +178 -0
  27. data/lib/json_schemer/draft7/vocab/validation.rb +69 -0
  28. data/lib/json_schemer/draft7/vocab.rb +30 -0
  29. data/lib/json_schemer/errors.rb +1 -0
  30. data/lib/json_schemer/format/duration.rb +23 -0
  31. data/lib/json_schemer/format/json_pointer.rb +18 -0
  32. data/lib/json_schemer/format.rb +52 -26
  33. data/lib/json_schemer/keyword.rb +41 -0
  34. data/lib/json_schemer/location.rb +25 -0
  35. data/lib/json_schemer/openapi.rb +40 -0
  36. data/lib/json_schemer/openapi30/document.rb +1673 -0
  37. data/lib/json_schemer/openapi30/meta.rb +26 -0
  38. data/lib/json_schemer/openapi30/vocab/base.rb +18 -0
  39. data/lib/json_schemer/openapi30/vocab.rb +12 -0
  40. data/lib/json_schemer/openapi31/document.rb +1559 -0
  41. data/lib/json_schemer/openapi31/meta.rb +128 -0
  42. data/lib/json_schemer/openapi31/vocab/base.rb +89 -0
  43. data/lib/json_schemer/openapi31/vocab.rb +18 -0
  44. data/lib/json_schemer/output.rb +55 -0
  45. data/lib/json_schemer/result.rb +168 -0
  46. data/lib/json_schemer/schema.rb +390 -0
  47. data/lib/json_schemer/version.rb +1 -1
  48. data/lib/json_schemer.rb +197 -24
  49. metadata +42 -10
  50. data/lib/json_schemer/schema/base.rb +0 -677
  51. data/lib/json_schemer/schema/draft4.json +0 -149
  52. data/lib/json_schemer/schema/draft4.rb +0 -44
  53. data/lib/json_schemer/schema/draft6.json +0 -155
  54. data/lib/json_schemer/schema/draft6.rb +0 -25
  55. data/lib/json_schemer/schema/draft7.json +0 -172
  56. data/lib/json_schemer/schema/draft7.rb +0 -32
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module OpenAPI31
4
+ BASE_URI = URI('https://spec.openapis.org/oas/3.1/dialect/base')
5
+ SCHEMA = {
6
+ '$id' => 'https://spec.openapis.org/oas/3.1/dialect/base',
7
+ '$schema' => 'https://json-schema.org/draft/2020-12/schema',
8
+
9
+ 'title' => 'OpenAPI 3.1 Schema Object Dialect',
10
+ 'description' => 'A JSON Schema dialect describing schemas found in OpenAPI documents',
11
+
12
+ '$vocabulary' => {
13
+ 'https://json-schema.org/draft/2020-12/vocab/core' => true,
14
+ 'https://json-schema.org/draft/2020-12/vocab/applicator' => true,
15
+ 'https://json-schema.org/draft/2020-12/vocab/unevaluated' => true,
16
+ 'https://json-schema.org/draft/2020-12/vocab/validation' => true,
17
+ 'https://json-schema.org/draft/2020-12/vocab/meta-data' => true,
18
+ 'https://json-schema.org/draft/2020-12/vocab/format-annotation' => true,
19
+ 'https://json-schema.org/draft/2020-12/vocab/content' => true,
20
+ 'https://spec.openapis.org/oas/3.1/vocab/base' => false
21
+ },
22
+
23
+ '$dynamicAnchor' => 'meta',
24
+
25
+ 'allOf' => [
26
+ { '$ref' => 'https://json-schema.org/draft/2020-12/schema' },
27
+ { '$ref' => 'https://spec.openapis.org/oas/3.1/meta/base' }
28
+ ]
29
+ }
30
+
31
+
32
+ module Meta
33
+ BASE = {
34
+ '$id' => 'https://spec.openapis.org/oas/3.1/meta/base',
35
+ '$schema' => 'https://json-schema.org/draft/2020-12/schema',
36
+
37
+ 'title' => 'OAS Base vocabulary',
38
+ 'description' => 'A JSON Schema Vocabulary used in the OpenAPI Schema Dialect',
39
+
40
+ '$vocabulary' => {
41
+ 'https://spec.openapis.org/oas/3.1/vocab/base' => true
42
+ },
43
+
44
+ '$dynamicAnchor' => 'meta',
45
+
46
+ 'type' => ['object', 'boolean'],
47
+ 'properties' => {
48
+ 'example' => true,
49
+ 'discriminator' => { '$ref' => '#/$defs/discriminator' },
50
+ 'externalDocs' => { '$ref' => '#/$defs/external-docs' },
51
+ 'xml' => { '$ref' => '#/$defs/xml' }
52
+ },
53
+
54
+ '$defs' => {
55
+ 'extensible' => {
56
+ 'patternProperties' => {
57
+ '^x-' => true
58
+ }
59
+ },
60
+
61
+ 'discriminator' => {
62
+ '$ref' => '#/$defs/extensible',
63
+ 'type' => 'object',
64
+ 'properties' => {
65
+ 'propertyName' => {
66
+ 'type' => 'string'
67
+ },
68
+ 'mapping' => {
69
+ 'type' => 'object',
70
+ 'additionalProperties' => {
71
+ 'type' => 'string'
72
+ }
73
+ }
74
+ },
75
+ 'required' => ['propertyName'],
76
+ 'unevaluatedProperties' => false
77
+ },
78
+
79
+ 'external-docs' => {
80
+ '$ref' => '#/$defs/extensible',
81
+ 'type' => 'object',
82
+ 'properties' => {
83
+ 'url' => {
84
+ 'type' => 'string',
85
+ 'format' => 'uri-reference'
86
+ },
87
+ 'description' => {
88
+ 'type' => 'string'
89
+ }
90
+ },
91
+ 'required' => ['url'],
92
+ 'unevaluatedProperties' => false
93
+ },
94
+
95
+ 'xml' => {
96
+ '$ref' => '#/$defs/extensible',
97
+ 'type' => 'object',
98
+ 'properties' => {
99
+ 'name' => {
100
+ 'type' => 'string'
101
+ },
102
+ 'namespace' => {
103
+ 'type' => 'string',
104
+ 'format' => 'uri'
105
+ },
106
+ 'prefix' => {
107
+ 'type' => 'string'
108
+ },
109
+ 'attribute' => {
110
+ 'type' => 'boolean'
111
+ },
112
+ 'wrapped' => {
113
+ 'type' => 'boolean'
114
+ }
115
+ },
116
+ 'unevaluatedProperties' => false
117
+ }
118
+ }
119
+ }
120
+
121
+
122
+ SCHEMAS = Draft202012::Meta::SCHEMAS.merge(
123
+ Draft202012::BASE_URI => Draft202012::SCHEMA,
124
+ URI('https://spec.openapis.org/oas/3.1/meta/base') => BASE
125
+ )
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module OpenAPI31
4
+ module Vocab
5
+ module Base
6
+ class AllOf < Draft202012::Vocab::Applicator::AllOf
7
+ attr_accessor :skip_ref_once
8
+
9
+ def validate(instance, instance_location, keyword_location, context)
10
+ nested = []
11
+ parsed.each_with_index do |subschema, index|
12
+ if ref_schema = subschema.parsed['$ref']&.ref_schema
13
+ next if skip_ref_once == ref_schema.absolute_keyword_location
14
+ ref_schema.parsed['discriminator']&.skip_ref_once = schema.absolute_keyword_location
15
+ end
16
+ nested << subschema.validate_instance(instance, instance_location, join_location(keyword_location, index.to_s), context)
17
+ end
18
+ result(instance, instance_location, keyword_location, nested.all?(&:valid), nested)
19
+ ensure
20
+ self.skip_ref_once = nil
21
+ end
22
+ end
23
+
24
+ class AnyOf < Draft202012::Vocab::Applicator::AnyOf
25
+ def validate(*)
26
+ schema.parsed.key?('discriminator') ? nil : super
27
+ end
28
+ end
29
+
30
+ class OneOf < Draft202012::Vocab::Applicator::OneOf
31
+ def validate(*)
32
+ schema.parsed.key?('discriminator') ? nil : super
33
+ end
34
+ end
35
+
36
+ class Discriminator < Keyword
37
+ include Format::JSONPointer
38
+
39
+ attr_accessor :skip_ref_once
40
+
41
+ def error(formatted_instance_location:, **)
42
+ "value at #{formatted_instance_location} does not match `discriminator` schema"
43
+ end
44
+
45
+ def validate(instance, instance_location, keyword_location, context)
46
+ property_name = value.fetch('propertyName')
47
+ mapping = value['mapping'] || {}
48
+
49
+ return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Hash) && instance.key?(property_name)
50
+
51
+ property = instance.fetch(property_name)
52
+ ref = mapping.fetch(property, property)
53
+
54
+ ref_schema = nil
55
+ unless ref.start_with?('#') && valid_json_pointer?(ref.delete_prefix('#'))
56
+ ref_schema = begin
57
+ root.resolve_ref(URI.join(schema.base_uri, "#/components/schemas/#{ref}"))
58
+ rescue InvalidRefPointer
59
+ nil
60
+ end
61
+ end
62
+ ref_schema ||= root.resolve_ref(URI.join(schema.base_uri, ref))
63
+
64
+ return if skip_ref_once == ref_schema.absolute_keyword_location
65
+
66
+ nested = []
67
+
68
+ if schema.parsed.key?('anyOf') || schema.parsed.key?('oneOf')
69
+ subschemas = schema.parsed['anyOf']&.parsed || []
70
+ subschemas += schema.parsed['oneOf']&.parsed || []
71
+ subschemas.each do |subschema|
72
+ if subschema.parsed.fetch('$ref').ref_schema.absolute_keyword_location == ref_schema.absolute_keyword_location
73
+ nested << subschema.validate_instance(instance, instance_location, keyword_location, context)
74
+ end
75
+ end
76
+ else
77
+ ref_schema.parsed['allOf']&.skip_ref_once = schema.absolute_keyword_location
78
+ nested << ref_schema.validate_instance(instance, instance_location, keyword_location, context)
79
+ end
80
+
81
+ result(instance, instance_location, keyword_location, (nested.any? && nested.all?(&:valid)), nested)
82
+ ensure
83
+ self.skip_ref_once = nil
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module OpenAPI31
4
+ module Vocab
5
+ # https://spec.openapis.org/oas/latest.html#schema-object
6
+ BASE = {
7
+ # https://spec.openapis.org/oas/latest.html#discriminator-object
8
+ 'discriminator' => Base::Discriminator,
9
+ 'allOf' => Base::AllOf,
10
+ 'anyOf' => Base::AnyOf,
11
+ 'oneOf' => Base::OneOf
12
+ # 'xml' => Base::Xml,
13
+ # 'externalDocs' => Base::ExternalDocs,
14
+ # 'example' => Base::Example
15
+ }
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module Output
4
+ FRAGMENT_ENCODE_REGEX = /[^\w?\/:@\-.~!$&'()*+,;=]/
5
+
6
+ attr_reader :keyword, :schema
7
+
8
+ private
9
+
10
+ def result(instance, instance_location, keyword_location, valid, nested = nil, type: nil, annotation: nil, details: nil, ignore_nested: false)
11
+ if valid
12
+ Result.new(self, instance, instance_location, keyword_location, valid, nested, type, annotation, details, ignore_nested, 'annotations')
13
+ else
14
+ Result.new(self, instance, instance_location, keyword_location, valid, nested, type, annotation, details, ignore_nested, 'errors')
15
+ end
16
+ end
17
+
18
+ def escaped_keyword
19
+ @escaped_keyword ||= Location.escape_json_pointer_token(keyword)
20
+ end
21
+
22
+ def join_location(location, keyword)
23
+ Location.join(location, keyword)
24
+ end
25
+
26
+ def fragment_encode(location)
27
+ Format.percent_encode(location, FRAGMENT_ENCODE_REGEX)
28
+ end
29
+
30
+ # :nocov:
31
+ if Symbol.method_defined?(:name)
32
+ def stringify(key)
33
+ key.is_a?(Symbol) ? key.name : key.to_s
34
+ end
35
+ else
36
+ def stringify(key)
37
+ key.to_s
38
+ end
39
+ end
40
+ # :nocov:
41
+
42
+ def deep_stringify_keys(obj)
43
+ case obj
44
+ when Hash
45
+ obj.each_with_object({}) do |(key, value), out|
46
+ out[stringify(key)] = deep_stringify_keys(value)
47
+ end
48
+ when Array
49
+ obj.map { |item| deep_stringify_keys(item) }
50
+ else
51
+ obj
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ Result = Struct.new(:source, :instance, :instance_location, :keyword_location, :valid, :nested, :type, :annotation, :details, :ignore_nested, :nested_key) do
4
+ CLASSIC_ERROR_TYPES = Hash.new do |hash, klass|
5
+ hash[klass] = klass.name.rpartition('::').last.sub(/\A[[:alpha:]]/, &:downcase)
6
+ end
7
+
8
+ def output(output_format)
9
+ case output_format
10
+ when 'classic'
11
+ classic
12
+ when 'flag'
13
+ flag
14
+ when 'basic'
15
+ basic
16
+ when 'detailed'
17
+ detailed
18
+ when 'verbose'
19
+ verbose
20
+ else
21
+ raise UnknownOutputFormat, output_format
22
+ end
23
+ end
24
+
25
+ def error
26
+ return @error if defined?(@error)
27
+ resolved_instance_location = Location.resolve(instance_location)
28
+ @error = source.error(
29
+ :formatted_instance_location => resolved_instance_location.empty? ? 'root' : "`#{resolved_instance_location}`",
30
+ :details => details
31
+ )
32
+ end
33
+
34
+ def to_output_unit
35
+ out = {
36
+ 'valid' => valid,
37
+ 'keywordLocation' => Location.resolve(keyword_location),
38
+ 'absoluteKeywordLocation' => source.absolute_keyword_location,
39
+ 'instanceLocation' => Location.resolve(instance_location)
40
+ }
41
+ out['error'] = error unless valid
42
+ out['annotation'] = annotation if valid && annotation
43
+ out
44
+ end
45
+
46
+ def to_classic
47
+ schema = source.schema
48
+ out = {
49
+ 'data' => instance,
50
+ 'data_pointer' => Location.resolve(instance_location),
51
+ 'schema' => schema.value,
52
+ 'schema_pointer' => schema.schema_pointer,
53
+ 'root_schema' => schema.root.value,
54
+ 'type' => type || CLASSIC_ERROR_TYPES[source.class]
55
+ }
56
+ out['error'] = error
57
+ out['details'] = details if details
58
+ out
59
+ end
60
+
61
+ def flag
62
+ { 'valid' => valid }
63
+ end
64
+
65
+ def basic
66
+ out = to_output_unit
67
+ if nested&.any?
68
+ out[nested_key] = Enumerator.new do |yielder|
69
+ results = [self]
70
+ while result = results.pop
71
+ if result.ignore_nested || !result.nested&.any?
72
+ yielder << result.to_output_unit
73
+ else
74
+ previous_results_size = results.size
75
+ result.nested.reverse_each do |nested_result|
76
+ results << nested_result if nested_result.valid == valid
77
+ end
78
+ yielder << result.to_output_unit unless (results.size - previous_results_size) == 1
79
+ end
80
+ end
81
+ end
82
+ end
83
+ out
84
+ end
85
+
86
+ def detailed
87
+ return to_output_unit if ignore_nested || !nested&.any?
88
+ matching_results = nested.select { |nested_result| nested_result.valid == valid }
89
+ if matching_results.size == 1
90
+ matching_results.first.detailed
91
+ else
92
+ out = to_output_unit
93
+ if matching_results.any?
94
+ out[nested_key] = Enumerator.new do |yielder|
95
+ matching_results.each { |nested_result| yielder << nested_result.detailed }
96
+ end
97
+ end
98
+ out
99
+ end
100
+ end
101
+
102
+ def verbose
103
+ out = to_output_unit
104
+ if nested&.any?
105
+ out[nested_key] = Enumerator.new do |yielder|
106
+ nested.each { |nested_result| yielder << nested_result.verbose }
107
+ end
108
+ end
109
+ out
110
+ end
111
+
112
+ def classic
113
+ Enumerator.new do |yielder|
114
+ unless valid
115
+ results = [self]
116
+ while result = results.pop
117
+ if result.ignore_nested || !result.nested&.any?
118
+ yielder << result.to_classic
119
+ else
120
+ previous_results_size = results.size
121
+ result.nested.reverse_each do |nested_result|
122
+ results << nested_result if nested_result.valid == valid
123
+ end
124
+ yielder << result.to_classic if (results.size - previous_results_size) == 0
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+ def insert_property_defaults(context)
132
+ instance_locations = {}
133
+
134
+ results = [[self, true]]
135
+ while (result, valid = results.pop)
136
+ next if result.source.is_a?(Schema::NOT_KEYWORD_CLASS)
137
+
138
+ valid &&= result.valid
139
+ result.nested&.each { |nested_result| results << [nested_result, valid] }
140
+
141
+ if result.source.is_a?(Schema::PROPERTIES_KEYWORD_CLASS) && result.instance.is_a?(Hash)
142
+ result.source.parsed.each do |property, schema|
143
+ next if result.instance.key?(property) || !schema.parsed.key?('default')
144
+ default = schema.parsed.fetch('default')
145
+ instance_location = Location.join(result.instance_location, property)
146
+ keyword_location = Location.join(Location.join(result.keyword_location, property), default.keyword)
147
+ default_result = default.validate(nil, instance_location, keyword_location, nil)
148
+ instance_locations[result.instance_location] ||= {}
149
+ instance_locations[result.instance_location][property] ||= []
150
+ instance_locations[result.instance_location][property] << [default_result, valid]
151
+ end
152
+ end
153
+ end
154
+
155
+ inserted = false
156
+
157
+ instance_locations.each do |instance_location, properties|
158
+ original_instance = context.original_instance(instance_location)
159
+ properties.each do |property, results_with_tree_validity|
160
+ property_inserted = yield(original_instance, property, results_with_tree_validity)
161
+ inserted ||= (property_inserted != false)
162
+ end
163
+ end
164
+
165
+ inserted
166
+ end
167
+ end
168
+ end