json_schemer 1.0.3 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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