openapi_parser 2.3.1 → 2.4.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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yaml +1 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +31 -0
- data/lib/openapi_parser/config.rb +12 -0
- data/lib/openapi_parser/schema_validator/array_validator.rb +7 -3
- data/lib/openapi_parser/schema_validator/minimum_maximum.rb +17 -4
- data/lib/openapi_parser/schema_validator/nil_validator.rb +4 -0
- data/lib/openapi_parser/schema_validator/type_mismatch_validator.rb +11 -0
- data/lib/openapi_parser/schema_validator.rb +82 -1
- data/lib/openapi_parser/schemas/components.rb +4 -0
- data/lib/openapi_parser/schemas/openapi.rb +20 -0
- data/lib/openapi_parser/schemas/schema.rb +12 -1
- data/lib/openapi_parser/spec_validator/rule.rb +68 -0
- data/lib/openapi_parser/spec_validator/rules/const_in_30.rb +25 -0
- data/lib/openapi_parser/spec_validator/rules/content_encoding_in_30.rb +24 -0
- data/lib/openapi_parser/spec_validator/rules/content_media_type_in_30.rb +25 -0
- data/lib/openapi_parser/spec_validator/rules/content_schema_in_30.rb +25 -0
- data/lib/openapi_parser/spec_validator/rules/dynamic_anchor_in_30.rb +25 -0
- data/lib/openapi_parser/spec_validator/rules/dynamic_ref_in_30.rb +25 -0
- data/lib/openapi_parser/spec_validator/rules/example_singular_deprecation.rb +25 -0
- data/lib/openapi_parser/spec_validator/rules/exclusive_maximum.rb +32 -0
- data/lib/openapi_parser/spec_validator/rules/exclusive_minimum.rb +32 -0
- data/lib/openapi_parser/spec_validator/rules/json_schema_dialect_in_30.rb +19 -0
- data/lib/openapi_parser/spec_validator/rules/nullable_deprecation.rb +25 -0
- data/lib/openapi_parser/spec_validator/rules/path_items_in_30.rb +25 -0
- data/lib/openapi_parser/spec_validator/rules/prefix_items_in_30.rb +25 -0
- data/lib/openapi_parser/spec_validator/rules/type_array_in_30.rb +25 -0
- data/lib/openapi_parser/spec_validator/rules/type_null_in_30.rb +26 -0
- data/lib/openapi_parser/spec_validator/rules/webhooks_in_30.rb +18 -0
- data/lib/openapi_parser/spec_validator/spec_violation.rb +11 -0
- data/lib/openapi_parser/spec_validator.rb +85 -0
- data/lib/openapi_parser/version.rb +1 -1
- data/lib/openapi_parser.rb +3 -0
- data/sig/openapi_parser/config.rbs +2 -0
- data/sig/openapi_parser/schema_validator.rbs +3 -0
- data/sig/openapi_parser/spec_validator.rbs +109 -0
- data/sig/openapi_parser.rbs +2 -0
- metadata +23 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d03c17295ee85e0cf17e06e83f6e28c1bed8028e7de1ae68574e2d69ad6160b2
|
|
4
|
+
data.tar.gz: 6cae65df5bab290cff5d7c014d181f5eb631e153863f5eac82c7ce460d224dd9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3074ba856acef7b064c0b1d2cf7b79b274e00766d90797ddfe95626f19ea09571fe72e2346e397bc7cac18f986959c659b62de2933b42d0a403074d5e41a73a9
|
|
7
|
+
data.tar.gz: 7c373bb6e33c6bf221b7d3dbc41f89951109b8adc7bc9aef362eb5c820733e5c1203279c272e92e96b1cd4a6278b09f70aab5ffee809cf635e9253d6e616bb06
|
data/.github/workflows/ci.yaml
CHANGED
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.3.12
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
## Unreleased
|
|
2
2
|
|
|
3
|
+
## 2.4.0 (2026-10-14)
|
|
4
|
+
* support `components.pathItems` so `$ref`s into it resolve, unblocking OpenAPI 3.1 documents that use reusable path items
|
|
5
|
+
* support array-form `type` (3.1) in value validation
|
|
6
|
+
* support root-level `jsonSchemaDialect` (OpenAPI 3.1) in the parse layer
|
|
7
|
+
* support `prefixItems` (OpenAPI 3.1) with positional tuple validation
|
|
8
|
+
* support `contentMediaType` (OpenAPI 3.1) in the parse layer
|
|
9
|
+
* support `contentEncoding` (OpenAPI 3.1) in the parse layer
|
|
10
|
+
* add `SpecValidator` with `strict_specification_version` config (`:silent` / `:warn` / `:raise`) to detect version mismatches between declared OpenAPI version and actual field usage
|
|
11
|
+
* `ContentEncodingIn30`: detect `contentEncoding` usage in 3.0 documents (3.1 addition)
|
|
12
|
+
* `ContentMediaTypeIn30`: detect `contentMediaType` usage in 3.0 documents (3.1 addition)
|
|
13
|
+
* `PrefixItemsIn30`: detect `prefixItems` usage in 3.0 documents (3.1 addition)
|
|
14
|
+
* `JsonSchemaDialectIn30`: detect root-level `jsonSchemaDialect` usage in 3.0 documents (3.1 addition)
|
|
15
|
+
* `TypeArrayIn30`: detect array-form `type` usage in 3.0 documents (3.1 form)
|
|
16
|
+
* `NullableDeprecation`: detect `nullable` usage in 3.1 documents (removed in 3.1)
|
|
17
|
+
* `ExampleSingularDeprecation`: detect singular `example` on schemas in 3.1 documents (deprecated in 3.1)
|
|
18
|
+
* `PathItemsIn30`: detect `components.pathItems` usage in 3.0 documents (3.1 addition)
|
|
19
|
+
* `ExclusiveMinimum` / `ExclusiveMaximum`: detect 3.0 Boolean vs 3.1 numeric form mismatch
|
|
20
|
+
* `TypeNullIn30`: detect `type: "null"` usage in 3.0 documents (3.1 primitive)
|
|
21
|
+
* `WebhooksIn30`: detect root-level `webhooks` usage in 3.0 documents (3.1 addition)
|
|
22
|
+
* `ConstIn30`: detect `const` usage in 3.0 documents (3.1 addition)
|
|
23
|
+
* `DynamicRefIn30`: detect `$dynamicRef` usage in 3.0 documents (3.1 addition)
|
|
24
|
+
* `DynamicAnchorIn30`: detect `$dynamicAnchor` usage in 3.0 documents (3.1 addition)
|
|
25
|
+
* `ContentSchemaIn30`: detect `contentSchema` usage in 3.0 documents (3.1 addition)
|
|
26
|
+
* expose the declared version as `OpenAPI#openapi_version` (a `Gem::Version`, or nil when the field is missing or malformed) so `SpecValidator` rules compare version ranges; a 3.2 document is checked by the 3.1-or-later rules
|
|
27
|
+
* support 3.1-style numeric `exclusiveMinimum` / `exclusiveMaximum` in value validation (standalone bound, not a Boolean modifier on `minimum` / `maximum`)
|
|
28
|
+
* support `type: "null"` (3.1 primitive) in value validation
|
|
29
|
+
* support root-level `webhooks` (OpenAPI 3.1) in the parse layer
|
|
30
|
+
* support `const` (OpenAPI 3.1) with exact-equality value validation
|
|
31
|
+
* support `contentSchema` (OpenAPI 3.1) in the parse layer
|
|
32
|
+
* coerce String values into array-form `type` (3.1) when `coerce_value` is on, so params like `type: [integer, "null"]` accept `"5"`
|
|
33
|
+
|
|
3
34
|
## 2.3.1 (2025-11-14)
|
|
4
35
|
* add optional date coercion with behavior matching existing datetime coercion
|
|
5
36
|
|
|
@@ -41,6 +41,18 @@ class OpenAPIParser::Config
|
|
|
41
41
|
@config.fetch(:strict_reference_validation, false)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
KNOWN_STRICT_SPECIFICATION_VERSION_VALUES = %i[silent warn raise].freeze
|
|
45
|
+
|
|
46
|
+
# @return [Symbol] :silent / :warn / :raise
|
|
47
|
+
def strict_specification_version
|
|
48
|
+
value = @config.fetch(:strict_specification_version, :silent)
|
|
49
|
+
unless KNOWN_STRICT_SPECIFICATION_VERSION_VALUES.include?(value)
|
|
50
|
+
warn("[OpenAPIParser] unknown strict_specification_version: #{value.inspect}, falling back to :silent")
|
|
51
|
+
return :silent
|
|
52
|
+
end
|
|
53
|
+
value
|
|
54
|
+
end
|
|
55
|
+
|
|
44
56
|
def validate_header
|
|
45
57
|
@config.fetch(:validate_header, true)
|
|
46
58
|
end
|
|
@@ -11,10 +11,14 @@ class OpenAPIParser::SchemaValidator
|
|
|
11
11
|
value, err = validate_unique_items(value, schema)
|
|
12
12
|
return [nil, err] if err
|
|
13
13
|
|
|
14
|
-
#
|
|
14
|
+
# 3.1: prefixItems positionally validates the leading N elements.
|
|
15
|
+
# Elements past the prefix fall back to schema.items, matching how
|
|
16
|
+
# JSON Schema 2020-12 layers the two keywords.
|
|
17
|
+
prefix_schemas = schema.prefix_items || []
|
|
15
18
|
items_schema = schema.items
|
|
16
|
-
coerced_values = value.map do |v|
|
|
17
|
-
|
|
19
|
+
coerced_values = value.each_with_index.map do |v, idx|
|
|
20
|
+
sub_schema = prefix_schemas[idx] || items_schema
|
|
21
|
+
coerced, err = validatable.validate_schema(v, sub_schema)
|
|
18
22
|
return [nil, err] if err
|
|
19
23
|
|
|
20
24
|
coerced
|
|
@@ -4,8 +4,10 @@ class OpenAPIParser::SchemaValidator
|
|
|
4
4
|
# @param [Object] value
|
|
5
5
|
# @param [OpenAPIParser::Schemas::Schema] schema
|
|
6
6
|
def check_minimum_maximum(value, schema)
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
has_min_or_max = schema.minimum || schema.maximum
|
|
8
|
+
has_numeric_exclusive_min = schema.exclusiveMinimum.is_a?(Numeric)
|
|
9
|
+
has_numeric_exclusive_max = schema.exclusiveMaximum.is_a?(Numeric)
|
|
10
|
+
return [value, nil] unless has_min_or_max || has_numeric_exclusive_min || has_numeric_exclusive_max
|
|
9
11
|
|
|
10
12
|
validate(value, schema)
|
|
11
13
|
[value, nil]
|
|
@@ -18,16 +20,27 @@ class OpenAPIParser::SchemaValidator
|
|
|
18
20
|
def validate(value, schema)
|
|
19
21
|
reference = schema.object_reference
|
|
20
22
|
|
|
23
|
+
# 3.1: exclusiveMinimum is a standalone numeric bound.
|
|
24
|
+
if schema.exclusiveMinimum.is_a?(Numeric) && value <= schema.exclusiveMinimum
|
|
25
|
+
raise OpenAPIParser::LessThanExclusiveMinimum.new(value, reference)
|
|
26
|
+
end
|
|
27
|
+
|
|
21
28
|
if schema.minimum
|
|
22
|
-
|
|
29
|
+
# 3.0: exclusiveMinimum is a Boolean modifier on `minimum`.
|
|
30
|
+
if schema.exclusiveMinimum == true && value <= schema.minimum
|
|
23
31
|
raise OpenAPIParser::LessThanExclusiveMinimum.new(value, reference)
|
|
24
32
|
elsif value < schema.minimum
|
|
25
33
|
raise OpenAPIParser::LessThanMinimum.new(value, reference)
|
|
26
34
|
end
|
|
27
35
|
end
|
|
28
36
|
|
|
37
|
+
# 3.1: standalone numeric upper bound, mirror of exclusiveMinimum.
|
|
38
|
+
if schema.exclusiveMaximum.is_a?(Numeric) && value >= schema.exclusiveMaximum
|
|
39
|
+
raise OpenAPIParser::MoreThanExclusiveMaximum.new(value, reference)
|
|
40
|
+
end
|
|
41
|
+
|
|
29
42
|
if schema.maximum
|
|
30
|
-
if schema.exclusiveMaximum && value >= schema.maximum
|
|
43
|
+
if schema.exclusiveMaximum == true && value >= schema.maximum
|
|
31
44
|
raise OpenAPIParser::MoreThanExclusiveMaximum.new(value, reference)
|
|
32
45
|
elsif value > schema.maximum
|
|
33
46
|
raise OpenAPIParser::MoreThanMaximum.new(value, reference)
|
|
@@ -4,6 +4,10 @@ class OpenAPIParser::SchemaValidator
|
|
|
4
4
|
# @param [OpenAPIParser::Schemas::Schema] schema
|
|
5
5
|
def coerce_and_validate(value, schema, **_keyword_args)
|
|
6
6
|
return [value, nil] if schema.nullable
|
|
7
|
+
# 3.1: `type: "null"` makes nil the only valid value for the schema.
|
|
8
|
+
return [value, nil] if schema.type == 'null'
|
|
9
|
+
# 3.1: `type: [..., "null"]` lets nil coexist with another primitive.
|
|
10
|
+
return [value, nil] if schema.type.is_a?(Array) && schema.type.include?('null')
|
|
7
11
|
|
|
8
12
|
[nil, OpenAPIParser::NotNullError.new(schema.object_reference)]
|
|
9
13
|
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
class OpenAPIParser::SchemaValidator
|
|
2
|
+
# Emits a ValidateError unconditionally. Used when the dispatcher has
|
|
3
|
+
# already decided that no primitive applies to the value, e.g.:
|
|
4
|
+
# - schema declares `type: "null"` and the value is non-nil (3.1)
|
|
5
|
+
# - schema declares `type: [t1, t2, ...]` and the value matches none
|
|
6
|
+
class TypeMismatchValidator < Base
|
|
7
|
+
def coerce_and_validate(value, schema, **_keyword_args)
|
|
8
|
+
OpenAPIParser::ValidateError.build_error_result(value, schema)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -13,6 +13,7 @@ require_relative 'schema_validator/any_of_validator'
|
|
|
13
13
|
require_relative 'schema_validator/all_of_validator'
|
|
14
14
|
require_relative 'schema_validator/one_of_validator'
|
|
15
15
|
require_relative 'schema_validator/nil_validator'
|
|
16
|
+
require_relative 'schema_validator/type_mismatch_validator'
|
|
16
17
|
require_relative 'schema_validator/unspecified_type_validator'
|
|
17
18
|
|
|
18
19
|
class OpenAPIParser::SchemaValidator
|
|
@@ -74,6 +75,15 @@ class OpenAPIParser::SchemaValidator
|
|
|
74
75
|
def validate_schema(value, schema, **keyword_args)
|
|
75
76
|
return [value, nil] unless schema
|
|
76
77
|
|
|
78
|
+
# 3.1: `const` pins the value to exactly that constant. Checked before
|
|
79
|
+
# type dispatch so it applies uniformly across primitives. Detection
|
|
80
|
+
# uses raw_schema so an intentional `const: null` is honored.
|
|
81
|
+
if schema.respond_to?(:raw_schema) && schema.raw_schema.is_a?(Hash) && schema.raw_schema.key?('const')
|
|
82
|
+
return [nil, OpenAPIParser::ValidateError.new(value, "const #{schema.const.inspect}", schema.object_reference)] if value != schema.const
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
return coerce_into_type_array(value, schema, **keyword_args) if coerce_into_type_array?(value, schema)
|
|
86
|
+
|
|
77
87
|
if (v = validator(value, schema))
|
|
78
88
|
if keyword_args.empty?
|
|
79
89
|
return v.coerce_and_validate(value, schema)
|
|
@@ -103,7 +113,25 @@ class OpenAPIParser::SchemaValidator
|
|
|
103
113
|
return one_of_validator if schema.one_of
|
|
104
114
|
return nil_validator if value.nil?
|
|
105
115
|
|
|
106
|
-
|
|
116
|
+
# 3.1: pick the type from an Array whose primitive class matches the
|
|
117
|
+
# value, then dispatch as if it were a single-type schema. If nothing
|
|
118
|
+
# matches we still pick a candidate so the relevant validator emits
|
|
119
|
+
# a sensible error rather than silently accepting via the unspecified
|
|
120
|
+
# fallback.
|
|
121
|
+
effective_type = schema.type
|
|
122
|
+
if effective_type.is_a?(Array)
|
|
123
|
+
matched = pick_array_type(value, effective_type)
|
|
124
|
+
return type_mismatch_validator if matched.nil?
|
|
125
|
+
|
|
126
|
+
effective_type = matched
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
validator_for_type(effective_type)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# @return [OpenAPIParser::SchemaValidator::Base]
|
|
133
|
+
def validator_for_type(type)
|
|
134
|
+
case type
|
|
107
135
|
when 'string'
|
|
108
136
|
string_validator
|
|
109
137
|
when 'integer'
|
|
@@ -116,11 +144,64 @@ class OpenAPIParser::SchemaValidator
|
|
|
116
144
|
object_validator
|
|
117
145
|
when 'array'
|
|
118
146
|
array_validator
|
|
147
|
+
when 'null'
|
|
148
|
+
# 3.1: only nil values are valid here. nil is handled earlier in
|
|
149
|
+
# this method, so a non-nil value reaching this branch is a type
|
|
150
|
+
# mismatch that should fail validation.
|
|
151
|
+
type_mismatch_validator
|
|
119
152
|
else
|
|
120
153
|
unspecified_type_validator
|
|
121
154
|
end
|
|
122
155
|
end
|
|
123
156
|
|
|
157
|
+
# 3.1: request params arrive as Strings, so `type: [integer, "null"]`
|
|
158
|
+
# never matches by class. When coercing, try each listed type instead.
|
|
159
|
+
# @return [Boolean]
|
|
160
|
+
def coerce_into_type_array?(value, schema)
|
|
161
|
+
return false unless @coerce_value && value.is_a?(String)
|
|
162
|
+
return false if schema.any_of || schema.all_of || schema.one_of
|
|
163
|
+
|
|
164
|
+
schema.type.is_a?(Array) && pick_array_type(value, schema.type).nil?
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# 3.1: returns the first listed type that coerces, or the first type's error.
|
|
168
|
+
# @return [Array] coerced value and error
|
|
169
|
+
def coerce_into_type_array(value, schema, **keyword_args)
|
|
170
|
+
first_error = nil
|
|
171
|
+
(schema.type - ['null']).each do |type|
|
|
172
|
+
v = validator_for_type(type)
|
|
173
|
+
coerced, err = if keyword_args.empty?
|
|
174
|
+
v.coerce_and_validate(value, schema)
|
|
175
|
+
else
|
|
176
|
+
v.coerce_and_validate(value, schema, **keyword_args)
|
|
177
|
+
end
|
|
178
|
+
return [coerced, nil] if err.nil?
|
|
179
|
+
|
|
180
|
+
first_error ||= err
|
|
181
|
+
end
|
|
182
|
+
return [nil, first_error] if first_error
|
|
183
|
+
|
|
184
|
+
type_mismatch_validator.coerce_and_validate(value, schema)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def type_mismatch_validator
|
|
188
|
+
@type_mismatch_validator ||= OpenAPIParser::SchemaValidator::TypeMismatchValidator.new(self, @coerce_value)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def pick_array_type(value, types)
|
|
192
|
+
types.find do |t|
|
|
193
|
+
case t
|
|
194
|
+
when 'string' then value.is_a?(String)
|
|
195
|
+
when 'integer' then value.is_a?(Integer)
|
|
196
|
+
when 'number' then value.is_a?(Numeric)
|
|
197
|
+
when 'boolean' then value == true || value == false
|
|
198
|
+
when 'array' then value.is_a?(Array)
|
|
199
|
+
when 'object' then value.is_a?(Hash)
|
|
200
|
+
when 'null' then value.nil?
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
124
205
|
def string_validator
|
|
125
206
|
@string_validator ||= OpenAPIParser::SchemaValidator::StringValidator.new(self, @allow_empty_date_and_datetime, @coerce_value, @datetime_coerce_class, @date_coerce_class)
|
|
126
207
|
end
|
|
@@ -24,5 +24,9 @@ module OpenAPIParser::Schemas
|
|
|
24
24
|
# @!attribute [r] headers
|
|
25
25
|
# @return [Hash{String => Header}, nil] header objects
|
|
26
26
|
openapi_attr_hash_object :headers, Header, reference: true
|
|
27
|
+
|
|
28
|
+
# @!attribute [r] path_items
|
|
29
|
+
# @return [Hash{String => PathItem}, nil] path item objects (OpenAPI 3.1+)
|
|
30
|
+
openapi_attr_hash_object :path_items, PathItem, reference: true, schema_key: :pathItems
|
|
27
31
|
end
|
|
28
32
|
end
|
|
@@ -21,6 +21,18 @@ module OpenAPIParser::Schemas
|
|
|
21
21
|
# @return [String, nil]
|
|
22
22
|
openapi_attr_values :openapi
|
|
23
23
|
|
|
24
|
+
# Declared OpenAPI version as a comparable value.
|
|
25
|
+
# A prerelease tag is dropped ("3.1.0-rc1" => 3.1.0) because it does not
|
|
26
|
+
# change which version's rules the document is written against.
|
|
27
|
+
# @return [Gem::Version, nil] nil when the field is missing or not a
|
|
28
|
+
# major.minor[.patch] version string
|
|
29
|
+
def openapi_version
|
|
30
|
+
return nil unless openapi.is_a?(String)
|
|
31
|
+
return nil unless openapi.match?(/\A\d+\.\d+/) && Gem::Version.correct?(openapi)
|
|
32
|
+
|
|
33
|
+
Gem::Version.new(openapi).release
|
|
34
|
+
end
|
|
35
|
+
|
|
24
36
|
# @!attribute [r] paths
|
|
25
37
|
# @return [Paths, nil]
|
|
26
38
|
openapi_attr_object :paths, Paths, reference: false
|
|
@@ -33,6 +45,14 @@ module OpenAPIParser::Schemas
|
|
|
33
45
|
# @return [Info, nil]
|
|
34
46
|
openapi_attr_object :info, Info, reference: false
|
|
35
47
|
|
|
48
|
+
# @!attribute [r] webhooks
|
|
49
|
+
# @return [Hash{String => PathItem}, nil] webhook path items (OpenAPI 3.1+)
|
|
50
|
+
openapi_attr_hash_object :webhooks, PathItem, reference: true
|
|
51
|
+
|
|
52
|
+
# @!attribute [r] json_schema_dialect
|
|
53
|
+
# @return [String, nil] dialect URI for embedded JSON Schemas (OpenAPI 3.1+)
|
|
54
|
+
openapi_attr_value :json_schema_dialect, schema_key: :jsonSchemaDialect
|
|
55
|
+
|
|
36
56
|
# @return [OpenAPIParser::RequestOperation, nil]
|
|
37
57
|
def request_operation(http_method, request_path)
|
|
38
58
|
OpenAPIParser::RequestOperation.create(http_method, request_path, @path_item_finder, @config)
|
|
@@ -71,7 +71,10 @@ module OpenAPIParser::Schemas
|
|
|
71
71
|
:type,
|
|
72
72
|
:nullable,
|
|
73
73
|
:example,
|
|
74
|
-
:deprecated
|
|
74
|
+
:deprecated,
|
|
75
|
+
:const,
|
|
76
|
+
:contentMediaType,
|
|
77
|
+
:contentEncoding
|
|
75
78
|
|
|
76
79
|
# @!attribute [r] read_only
|
|
77
80
|
# @return [Boolean, nil]
|
|
@@ -97,6 +100,14 @@ module OpenAPIParser::Schemas
|
|
|
97
100
|
# @return [Schema, nil]
|
|
98
101
|
openapi_attr_object :items, Schema, reference: true
|
|
99
102
|
|
|
103
|
+
# @!attribute [r] prefix_items
|
|
104
|
+
# @return [Array<Schema>, nil] tuple-style positional schemas (OpenAPI 3.1+)
|
|
105
|
+
openapi_attr_list_object :prefix_items, Schema, reference: true, schema_key: :prefixItems
|
|
106
|
+
|
|
107
|
+
# @!attribute [r] content_schema
|
|
108
|
+
# @return [Schema, nil] schema describing the content payload (OpenAPI 3.1+)
|
|
109
|
+
openapi_attr_object :content_schema, Schema, reference: true, schema_key: :contentSchema
|
|
110
|
+
|
|
100
111
|
# @!attribute [r] properties
|
|
101
112
|
# @return [Hash{String => Schema}, nil]
|
|
102
113
|
openapi_attr_hash_object :properties, Schema, reference: true
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module OpenAPIParser
|
|
2
|
+
class SpecValidator
|
|
3
|
+
class Rule
|
|
4
|
+
def self.rule_name
|
|
5
|
+
name
|
|
6
|
+
.split('::')
|
|
7
|
+
.last
|
|
8
|
+
.gsub(/([A-Z])/) { "_#{Regexp.last_match(1).downcase}" }
|
|
9
|
+
.sub(/^_/, '')
|
|
10
|
+
.to_sym
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(version)
|
|
14
|
+
@version = version
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @return [Gem::Version, nil] declared OpenAPI version, nil when unknown
|
|
18
|
+
attr_reader :version
|
|
19
|
+
|
|
20
|
+
def check(_root)
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param [String] boundary like '3.1'
|
|
25
|
+
# @return [Boolean] false when the version is unknown
|
|
26
|
+
def version_before?(boundary)
|
|
27
|
+
!version.nil? && version < Gem::Version.new(boundary)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @param [String] boundary like '3.1'
|
|
31
|
+
# @return [Boolean] false when the version is unknown
|
|
32
|
+
def version_at_least?(boundary)
|
|
33
|
+
!version.nil? && version >= Gem::Version.new(boundary)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def violation(path:, message:)
|
|
39
|
+
OpenAPIParser::SpecViolation.new(
|
|
40
|
+
message: message,
|
|
41
|
+
path: path,
|
|
42
|
+
rule_name: self.class.rule_name,
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def each_schema(root, &block)
|
|
47
|
+
return enum_for(:each_schema, root) unless block
|
|
48
|
+
|
|
49
|
+
visited = {}
|
|
50
|
+
walk(root, visited) do |node|
|
|
51
|
+
yield node if node.is_a?(OpenAPIParser::Schemas::Schema)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def walk(node, visited, &block)
|
|
56
|
+
return unless node.respond_to?(:_openapi_all_child_objects)
|
|
57
|
+
return if visited[node.object_id]
|
|
58
|
+
|
|
59
|
+
visited[node.object_id] = true
|
|
60
|
+
block.call(node)
|
|
61
|
+
|
|
62
|
+
node._openapi_all_child_objects.each_value do |child|
|
|
63
|
+
walk(child, visited, &block)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module OpenAPIParser
|
|
2
|
+
class SpecValidator
|
|
3
|
+
module Rules
|
|
4
|
+
# `const` is a JSON Schema 2020-12 keyword adopted by OpenAPI 3.1.
|
|
5
|
+
# 3.0 does not recognize it. Detection inspects raw_schema so a
|
|
6
|
+
# literal `const: null` (deliberate) still flags.
|
|
7
|
+
class ConstIn30 < Rule
|
|
8
|
+
def check(root)
|
|
9
|
+
return [] unless version_before?('3.1')
|
|
10
|
+
|
|
11
|
+
violations = []
|
|
12
|
+
each_schema(root) do |schema|
|
|
13
|
+
next unless schema.raw_schema.is_a?(Hash) && schema.raw_schema.key?('const')
|
|
14
|
+
|
|
15
|
+
violations << violation(
|
|
16
|
+
path: schema.object_reference,
|
|
17
|
+
message: '`const` is a 3.1 addition (from JSON Schema 2020-12); 3.0 has no equivalent',
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
violations
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module OpenAPIParser
|
|
2
|
+
class SpecValidator
|
|
3
|
+
module Rules
|
|
4
|
+
# `contentEncoding` is a JSON Schema 2020-12 annotation adopted by
|
|
5
|
+
# 3.1. Metadata only, no runtime side-effects.
|
|
6
|
+
class ContentEncodingIn30 < Rule
|
|
7
|
+
def check(root)
|
|
8
|
+
return [] unless version_before?('3.1')
|
|
9
|
+
|
|
10
|
+
violations = []
|
|
11
|
+
each_schema(root) do |schema|
|
|
12
|
+
next unless schema.raw_schema.is_a?(Hash) && schema.raw_schema.key?('contentEncoding')
|
|
13
|
+
|
|
14
|
+
violations << violation(
|
|
15
|
+
path: schema.object_reference,
|
|
16
|
+
message: '`contentEncoding` is a 3.1 addition (from JSON Schema 2020-12); 3.0 has no equivalent',
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
violations
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module OpenAPIParser
|
|
2
|
+
class SpecValidator
|
|
3
|
+
module Rules
|
|
4
|
+
# `contentMediaType` is a JSON Schema 2020-12 annotation adopted by
|
|
5
|
+
# 3.1. It is metadata, so it has no runtime side-effects; we only
|
|
6
|
+
# report version mismatches.
|
|
7
|
+
class ContentMediaTypeIn30 < Rule
|
|
8
|
+
def check(root)
|
|
9
|
+
return [] unless version_before?('3.1')
|
|
10
|
+
|
|
11
|
+
violations = []
|
|
12
|
+
each_schema(root) do |schema|
|
|
13
|
+
next unless schema.raw_schema.is_a?(Hash) && schema.raw_schema.key?('contentMediaType')
|
|
14
|
+
|
|
15
|
+
violations << violation(
|
|
16
|
+
path: schema.object_reference,
|
|
17
|
+
message: '`contentMediaType` is a 3.1 addition (from JSON Schema 2020-12); 3.0 has no equivalent',
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
violations
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module OpenAPIParser
|
|
2
|
+
class SpecValidator
|
|
3
|
+
module Rules
|
|
4
|
+
# `contentSchema` is a JSON Schema 2020-12 annotation adopted by 3.1.
|
|
5
|
+
# The parse layer accepts it permissively; this rule reports the
|
|
6
|
+
# version mismatch on 3.0 documents.
|
|
7
|
+
class ContentSchemaIn30 < Rule
|
|
8
|
+
def check(root)
|
|
9
|
+
return [] unless version_before?('3.1')
|
|
10
|
+
|
|
11
|
+
violations = []
|
|
12
|
+
each_schema(root) do |schema|
|
|
13
|
+
next unless schema.raw_schema.is_a?(Hash) && schema.raw_schema.key?('contentSchema')
|
|
14
|
+
|
|
15
|
+
violations << violation(
|
|
16
|
+
path: schema.object_reference,
|
|
17
|
+
message: '`contentSchema` is a 3.1 addition (from JSON Schema 2020-12); 3.0 has no equivalent',
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
violations
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module OpenAPIParser
|
|
2
|
+
class SpecValidator
|
|
3
|
+
module Rules
|
|
4
|
+
# `$dynamicAnchor` is the companion of `$dynamicRef` from JSON Schema
|
|
5
|
+
# 2020-12, declaring the dynamic resolution point. 3.0 has nothing
|
|
6
|
+
# equivalent; the rule flags it on 3.0 documents.
|
|
7
|
+
class DynamicAnchorIn30 < Rule
|
|
8
|
+
def check(root)
|
|
9
|
+
return [] unless version_before?('3.1')
|
|
10
|
+
|
|
11
|
+
violations = []
|
|
12
|
+
each_schema(root) do |schema|
|
|
13
|
+
next unless schema.raw_schema.is_a?(Hash) && schema.raw_schema.key?('$dynamicAnchor')
|
|
14
|
+
|
|
15
|
+
violations << violation(
|
|
16
|
+
path: schema.object_reference,
|
|
17
|
+
message: '`$dynamicAnchor` is a 3.1 addition (from JSON Schema 2020-12); 3.0 has no equivalent',
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
violations
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module OpenAPIParser
|
|
2
|
+
class SpecValidator
|
|
3
|
+
module Rules
|
|
4
|
+
# `$dynamicRef` is a JSON Schema 2020-12 referencing mechanism that
|
|
5
|
+
# 3.1 adopted. 3.0 does not recognize it. The parse layer keeps the
|
|
6
|
+
# value as raw schema data; this rule reports the version mismatch.
|
|
7
|
+
class DynamicRefIn30 < Rule
|
|
8
|
+
def check(root)
|
|
9
|
+
return [] unless version_before?('3.1')
|
|
10
|
+
|
|
11
|
+
violations = []
|
|
12
|
+
each_schema(root) do |schema|
|
|
13
|
+
next unless schema.raw_schema.is_a?(Hash) && schema.raw_schema.key?('$dynamicRef')
|
|
14
|
+
|
|
15
|
+
violations << violation(
|
|
16
|
+
path: schema.object_reference,
|
|
17
|
+
message: '`$dynamicRef` is a 3.1 addition (from JSON Schema 2020-12); 3.0 only knows `$ref`',
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
violations
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module OpenAPIParser
|
|
2
|
+
class SpecValidator
|
|
3
|
+
module Rules
|
|
4
|
+
# In 3.1 the singular `example` keyword on a Schema is deprecated in
|
|
5
|
+
# favor of the JSON Schema `examples` array. The field is still
|
|
6
|
+
# allowed but discouraged, so we report it as a violation.
|
|
7
|
+
class ExampleSingularDeprecation < Rule
|
|
8
|
+
def check(root)
|
|
9
|
+
return [] unless version_at_least?('3.1')
|
|
10
|
+
|
|
11
|
+
violations = []
|
|
12
|
+
each_schema(root) do |schema|
|
|
13
|
+
next unless schema.raw_schema.is_a?(Hash) && schema.raw_schema.key?('example')
|
|
14
|
+
|
|
15
|
+
violations << violation(
|
|
16
|
+
path: schema.object_reference,
|
|
17
|
+
message: 'singular `example` on a Schema is deprecated in 3.1; use the `examples` array',
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
violations
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module OpenAPIParser
|
|
2
|
+
class SpecValidator
|
|
3
|
+
module Rules
|
|
4
|
+
class ExclusiveMaximum < Rule
|
|
5
|
+
def check(root)
|
|
6
|
+
return [] if version.nil?
|
|
7
|
+
|
|
8
|
+
violations = []
|
|
9
|
+
each_schema(root) do |schema|
|
|
10
|
+
value = schema.exclusiveMaximum
|
|
11
|
+
next if value.nil?
|
|
12
|
+
|
|
13
|
+
if version_before?('3.1')
|
|
14
|
+
if value.is_a?(Numeric)
|
|
15
|
+
violations << violation(
|
|
16
|
+
path: schema.object_reference,
|
|
17
|
+
message: 'numeric exclusiveMaximum is a 3.1-only form; in 3.0 use a Boolean modifier paired with `maximum`',
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
elsif value == true || value == false
|
|
21
|
+
violations << violation(
|
|
22
|
+
path: schema.object_reference,
|
|
23
|
+
message: 'Boolean exclusiveMaximum is a 3.0-only form; in 3.1 use a standalone numeric bound',
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
violations
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module OpenAPIParser
|
|
2
|
+
class SpecValidator
|
|
3
|
+
module Rules
|
|
4
|
+
class ExclusiveMinimum < Rule
|
|
5
|
+
def check(root)
|
|
6
|
+
return [] if version.nil?
|
|
7
|
+
|
|
8
|
+
violations = []
|
|
9
|
+
each_schema(root) do |schema|
|
|
10
|
+
value = schema.exclusiveMinimum
|
|
11
|
+
next if value.nil?
|
|
12
|
+
|
|
13
|
+
if version_before?('3.1')
|
|
14
|
+
if value.is_a?(Numeric)
|
|
15
|
+
violations << violation(
|
|
16
|
+
path: schema.object_reference,
|
|
17
|
+
message: 'numeric exclusiveMinimum is a 3.1-only form; in 3.0 use a Boolean modifier paired with `minimum`',
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
elsif value == true || value == false
|
|
21
|
+
violations << violation(
|
|
22
|
+
path: schema.object_reference,
|
|
23
|
+
message: 'Boolean exclusiveMinimum is a 3.0-only form; in 3.1 use a standalone numeric bound',
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
violations
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module OpenAPIParser
|
|
2
|
+
class SpecValidator
|
|
3
|
+
module Rules
|
|
4
|
+
# `jsonSchemaDialect` is a 3.1 root-level addition; 3.0 has no
|
|
5
|
+
# equivalent.
|
|
6
|
+
class JsonSchemaDialectIn30 < Rule
|
|
7
|
+
def check(root)
|
|
8
|
+
return [] unless version_before?('3.1')
|
|
9
|
+
return [] unless root.raw_schema.is_a?(Hash) && root.raw_schema.key?('jsonSchemaDialect')
|
|
10
|
+
|
|
11
|
+
[violation(
|
|
12
|
+
path: '#/jsonSchemaDialect',
|
|
13
|
+
message: '`jsonSchemaDialect` is a 3.1 root-level addition; 3.0 documents have no such field',
|
|
14
|
+
)]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module OpenAPIParser
|
|
2
|
+
class SpecValidator
|
|
3
|
+
module Rules
|
|
4
|
+
# `nullable` is a 3.0 keyword that 3.1 removed in favor of
|
|
5
|
+
# `type: [..., "null"]`. The field's mere presence on a 3.1 document
|
|
6
|
+
# is a spec violation, regardless of true/false.
|
|
7
|
+
class NullableDeprecation < Rule
|
|
8
|
+
def check(root)
|
|
9
|
+
return [] unless version_at_least?('3.1')
|
|
10
|
+
|
|
11
|
+
violations = []
|
|
12
|
+
each_schema(root) do |schema|
|
|
13
|
+
next unless schema.raw_schema.is_a?(Hash) && schema.raw_schema.key?('nullable')
|
|
14
|
+
|
|
15
|
+
violations << violation(
|
|
16
|
+
path: schema.object_reference,
|
|
17
|
+
message: '`nullable` was removed in 3.1; use `type: [..., "null"]` instead',
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
violations
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module OpenAPIParser
|
|
2
|
+
class SpecValidator
|
|
3
|
+
module Rules
|
|
4
|
+
# `components.pathItems` is a 3.1 addition. The parse layer accepts it
|
|
5
|
+
# in 3.0 documents permissively; this rule reports the version
|
|
6
|
+
# mismatch.
|
|
7
|
+
class PathItemsIn30 < Rule
|
|
8
|
+
def check(root)
|
|
9
|
+
return [] unless version_before?('3.1')
|
|
10
|
+
|
|
11
|
+
components = root.components
|
|
12
|
+
return [] unless components
|
|
13
|
+
|
|
14
|
+
raw = components.raw_schema
|
|
15
|
+
return [] unless raw.is_a?(Hash) && raw.key?('pathItems')
|
|
16
|
+
|
|
17
|
+
[violation(
|
|
18
|
+
path: "#{components.object_reference}/pathItems",
|
|
19
|
+
message: '`components.pathItems` is a 3.1 addition; 3.0 documents should not declare it',
|
|
20
|
+
)]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module OpenAPIParser
|
|
2
|
+
class SpecValidator
|
|
3
|
+
module Rules
|
|
4
|
+
# `prefixItems` is JSON Schema 2020-12's positional tuple keyword.
|
|
5
|
+
# 3.1 adopts it; 3.0 has no equivalent and parsing it is a spec
|
|
6
|
+
# mismatch the validator should report.
|
|
7
|
+
class PrefixItemsIn30 < Rule
|
|
8
|
+
def check(root)
|
|
9
|
+
return [] unless version_before?('3.1')
|
|
10
|
+
|
|
11
|
+
violations = []
|
|
12
|
+
each_schema(root) do |schema|
|
|
13
|
+
next unless schema.raw_schema.is_a?(Hash) && schema.raw_schema.key?('prefixItems')
|
|
14
|
+
|
|
15
|
+
violations << violation(
|
|
16
|
+
path: schema.object_reference,
|
|
17
|
+
message: '`prefixItems` is a 3.1 addition (from JSON Schema 2020-12); 3.0 has no equivalent',
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
violations
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module OpenAPIParser
|
|
2
|
+
class SpecValidator
|
|
3
|
+
module Rules
|
|
4
|
+
# 3.1 lets `type` be an Array of primitive type names (e.g.
|
|
5
|
+
# `["string", "null"]`). 3.0 only allowed a single String, so the
|
|
6
|
+
# array form on a 3.0 document is a spec violation.
|
|
7
|
+
class TypeArrayIn30 < Rule
|
|
8
|
+
def check(root)
|
|
9
|
+
return [] unless version_before?('3.1')
|
|
10
|
+
|
|
11
|
+
violations = []
|
|
12
|
+
each_schema(root) do |schema|
|
|
13
|
+
next unless schema.type.is_a?(Array)
|
|
14
|
+
|
|
15
|
+
violations << violation(
|
|
16
|
+
path: schema.object_reference,
|
|
17
|
+
message: '`type` as an Array of primitive names is a 3.1 addition; 3.0 expects a single type String',
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
violations
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module OpenAPIParser
|
|
2
|
+
class SpecValidator
|
|
3
|
+
module Rules
|
|
4
|
+
# `type: "null"` was added by 3.1; 3.0 only allowed the original six
|
|
5
|
+
# primitive type names. The parse layer accepts the literal anyway,
|
|
6
|
+
# so we report the mismatch here. Array-form types like
|
|
7
|
+
# `["string", "null"]` are covered by a separate rule.
|
|
8
|
+
class TypeNullIn30 < Rule
|
|
9
|
+
def check(root)
|
|
10
|
+
return [] unless version_before?('3.1')
|
|
11
|
+
|
|
12
|
+
violations = []
|
|
13
|
+
each_schema(root) do |schema|
|
|
14
|
+
next unless schema.type == 'null'
|
|
15
|
+
|
|
16
|
+
violations << violation(
|
|
17
|
+
path: schema.object_reference,
|
|
18
|
+
message: '`type: "null"` is a 3.1 addition; 3.0 documents have no such primitive',
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
violations
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module OpenAPIParser
|
|
2
|
+
class SpecValidator
|
|
3
|
+
module Rules
|
|
4
|
+
# `webhooks` is a 3.1 root-level addition; 3.0 has no equivalent.
|
|
5
|
+
class WebhooksIn30 < Rule
|
|
6
|
+
def check(root)
|
|
7
|
+
return [] unless version_before?('3.1')
|
|
8
|
+
return [] unless root.raw_schema.is_a?(Hash) && root.raw_schema.key?('webhooks')
|
|
9
|
+
|
|
10
|
+
[violation(
|
|
11
|
+
path: '#/webhooks',
|
|
12
|
+
message: '`webhooks` is a 3.1 root-level addition; 3.0 documents have no such field',
|
|
13
|
+
)]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
require_relative 'spec_validator/spec_violation'
|
|
2
|
+
require_relative 'spec_validator/rule'
|
|
3
|
+
require_relative 'spec_validator/rules/exclusive_minimum'
|
|
4
|
+
require_relative 'spec_validator/rules/exclusive_maximum'
|
|
5
|
+
require_relative 'spec_validator/rules/content_encoding_in_30'
|
|
6
|
+
require_relative 'spec_validator/rules/content_media_type_in_30'
|
|
7
|
+
require_relative 'spec_validator/rules/prefix_items_in_30'
|
|
8
|
+
require_relative 'spec_validator/rules/json_schema_dialect_in_30'
|
|
9
|
+
require_relative 'spec_validator/rules/type_array_in_30'
|
|
10
|
+
require_relative 'spec_validator/rules/path_items_in_30'
|
|
11
|
+
require_relative 'spec_validator/rules/nullable_deprecation'
|
|
12
|
+
require_relative 'spec_validator/rules/example_singular_deprecation'
|
|
13
|
+
require_relative 'spec_validator/rules/type_null_in_30'
|
|
14
|
+
require_relative 'spec_validator/rules/webhooks_in_30'
|
|
15
|
+
require_relative 'spec_validator/rules/const_in_30'
|
|
16
|
+
require_relative 'spec_validator/rules/dynamic_ref_in_30'
|
|
17
|
+
require_relative 'spec_validator/rules/dynamic_anchor_in_30'
|
|
18
|
+
require_relative 'spec_validator/rules/content_schema_in_30'
|
|
19
|
+
|
|
20
|
+
module OpenAPIParser
|
|
21
|
+
class SpecViolationError < OpenAPIError
|
|
22
|
+
attr_reader :violations
|
|
23
|
+
|
|
24
|
+
def initialize(violations)
|
|
25
|
+
@violations = violations
|
|
26
|
+
super(nil)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def message
|
|
30
|
+
@violations.map(&:to_s).join("\n")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class SpecValidator
|
|
35
|
+
def self.run(root)
|
|
36
|
+
new(root).run
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.run!(root, policy:)
|
|
40
|
+
return if policy == :silent
|
|
41
|
+
|
|
42
|
+
violations = run(root)
|
|
43
|
+
return if violations.empty?
|
|
44
|
+
|
|
45
|
+
case policy
|
|
46
|
+
when :warn
|
|
47
|
+
violations.each { |v| warn(v.to_s) }
|
|
48
|
+
when :raise
|
|
49
|
+
raise OpenAPIParser::SpecViolationError.new(violations)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def initialize(root)
|
|
54
|
+
@root = root
|
|
55
|
+
@version = root.openapi_version
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def run
|
|
59
|
+
rules.flat_map { |klass| klass.new(@version).check(@root) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def rules
|
|
65
|
+
[
|
|
66
|
+
Rules::ExclusiveMinimum,
|
|
67
|
+
Rules::ExclusiveMaximum,
|
|
68
|
+
Rules::ContentEncodingIn30,
|
|
69
|
+
Rules::ContentMediaTypeIn30,
|
|
70
|
+
Rules::PrefixItemsIn30,
|
|
71
|
+
Rules::JsonSchemaDialectIn30,
|
|
72
|
+
Rules::TypeArrayIn30,
|
|
73
|
+
Rules::PathItemsIn30,
|
|
74
|
+
Rules::NullableDeprecation,
|
|
75
|
+
Rules::ExampleSingularDeprecation,
|
|
76
|
+
Rules::TypeNullIn30,
|
|
77
|
+
Rules::WebhooksIn30,
|
|
78
|
+
Rules::ConstIn30,
|
|
79
|
+
Rules::DynamicRefIn30,
|
|
80
|
+
Rules::DynamicAnchorIn30,
|
|
81
|
+
Rules::ContentSchemaIn30,
|
|
82
|
+
]
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
data/lib/openapi_parser.rb
CHANGED
|
@@ -15,6 +15,7 @@ require 'openapi_parser/request_operation'
|
|
|
15
15
|
require 'openapi_parser/schema_validator'
|
|
16
16
|
require 'openapi_parser/parameter_validator'
|
|
17
17
|
require 'openapi_parser/reference_expander'
|
|
18
|
+
require 'openapi_parser/spec_validator'
|
|
18
19
|
|
|
19
20
|
module OpenAPIParser
|
|
20
21
|
class << self
|
|
@@ -99,6 +100,8 @@ module OpenAPIParser
|
|
|
99
100
|
path_item.set_path_item_to_operation
|
|
100
101
|
end
|
|
101
102
|
|
|
103
|
+
OpenAPIParser::SpecValidator.run!(root, policy: config.strict_specification_version)
|
|
104
|
+
|
|
102
105
|
root
|
|
103
106
|
end
|
|
104
107
|
end
|
|
@@ -15,6 +15,8 @@ module OpenAPIParser
|
|
|
15
15
|
def expand_reference: -> bool
|
|
16
16
|
def strict_response_validation: -> bool
|
|
17
17
|
def strict_reference_validation: -> bool
|
|
18
|
+
KNOWN_STRICT_SPECIFICATION_VERSION_VALUES: Array[Symbol]
|
|
19
|
+
def strict_specification_version: -> (:silent | :warn | :raise)
|
|
18
20
|
def validate_header: -> bool
|
|
19
21
|
def request_validator_options: -> OpenAPIParser::SchemaValidator::Options
|
|
20
22
|
def response_validate_options: -> OpenAPIParser::SchemaValidator::ResponseValidateOptions
|
|
@@ -27,6 +27,9 @@ module OpenAPIParser
|
|
|
27
27
|
|
|
28
28
|
private
|
|
29
29
|
def validator: (Object value, OpenAPIParser::Schemas::Schema schema) -> [OpenAPIParser::SchemaValidator::Base, OpenAPIParser::validate_error]
|
|
30
|
+
def validator_for_type: (String? type) -> OpenAPIParser::SchemaValidator::Base
|
|
31
|
+
def coerce_into_type_array?: (Object value, OpenAPIParser::Schemas::Schema schema) -> bool
|
|
32
|
+
def coerce_into_type_array: (Object value, OpenAPIParser::Schemas::Schema schema, **untyped) -> [Object, OpenAPIParser::validate_error]
|
|
30
33
|
def string_validator: -> OpenAPIParser::SchemaValidator::StringValidator
|
|
31
34
|
def integer_validator: -> OpenAPIParser::SchemaValidator::IntegerValidator
|
|
32
35
|
def float_validator: -> OpenAPIParser::SchemaValidator::FloatValidator
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
module OpenAPIParser
|
|
2
|
+
class SpecViolationError < OpenAPIError
|
|
3
|
+
@violations: Array[OpenAPIParser::SpecValidator::SpecViolation]
|
|
4
|
+
attr_reader violations: Array[OpenAPIParser::SpecValidator::SpecViolation]
|
|
5
|
+
|
|
6
|
+
def initialize: (Array[OpenAPIParser::SpecValidator::SpecViolation] violations) -> void
|
|
7
|
+
def message: () -> String
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class SpecValidator
|
|
11
|
+
@root: OpenAPIParser::Schemas::OpenAPI
|
|
12
|
+
@version: Gem::Version?
|
|
13
|
+
|
|
14
|
+
def self.run: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecViolation]
|
|
15
|
+
def self.run!: (OpenAPIParser::Schemas::OpenAPI root, policy: (:silent | :warn | :raise)) -> void
|
|
16
|
+
def initialize: (OpenAPIParser::Schemas::OpenAPI root) -> void
|
|
17
|
+
def run: () -> Array[SpecViolation]
|
|
18
|
+
private def rules: () -> Array[singleton(Rule)]
|
|
19
|
+
|
|
20
|
+
class SpecViolation < ::Struct[untyped]
|
|
21
|
+
attr_accessor message: String
|
|
22
|
+
attr_accessor path: String
|
|
23
|
+
attr_accessor rule_name: Symbol
|
|
24
|
+
def to_s: () -> String
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class Rule
|
|
28
|
+
@version: Gem::Version?
|
|
29
|
+
attr_reader version: Gem::Version?
|
|
30
|
+
|
|
31
|
+
def self.rule_name: () -> Symbol
|
|
32
|
+
def initialize: (Gem::Version? version) -> void
|
|
33
|
+
def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecViolation]
|
|
34
|
+
def version_before?: (String boundary) -> bool
|
|
35
|
+
def version_at_least?: (String boundary) -> bool
|
|
36
|
+
private def violation: (path: String, message: String) -> SpecViolation
|
|
37
|
+
private def each_schema: (untyped root) ?{ (untyped) -> void } -> untyped
|
|
38
|
+
private def walk: (untyped node, Hash[Integer, bool] visited) { (untyped) -> void } -> void
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
module Rules
|
|
42
|
+
class ExclusiveMinimum < Rule
|
|
43
|
+
def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class ExclusiveMaximum < Rule
|
|
47
|
+
def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class ContentEncodingIn30 < Rule
|
|
51
|
+
def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class ContentMediaTypeIn30 < Rule
|
|
55
|
+
def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class PrefixItemsIn30 < Rule
|
|
59
|
+
def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
class JsonSchemaDialectIn30 < Rule
|
|
63
|
+
def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
class TypeArrayIn30 < Rule
|
|
67
|
+
def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class PathItemsIn30 < Rule
|
|
71
|
+
def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class NullableDeprecation < Rule
|
|
75
|
+
def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
class ExampleSingularDeprecation < Rule
|
|
79
|
+
def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
class TypeNullIn30 < Rule
|
|
83
|
+
def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
class WebhooksIn30 < Rule
|
|
87
|
+
def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
class ConstIn30 < Rule
|
|
91
|
+
def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
class DynamicRefIn30 < Rule
|
|
95
|
+
def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
class DynamicAnchorIn30 < Rule
|
|
99
|
+
def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
class ContentSchemaIn30 < Rule
|
|
103
|
+
def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
SpecViolation: singleton(SpecValidator::SpecViolation)
|
|
109
|
+
end
|
data/sig/openapi_parser.rbs
CHANGED
|
@@ -14,6 +14,8 @@ module OpenAPIParser
|
|
|
14
14
|
module Schemas
|
|
15
15
|
class OpenAPI
|
|
16
16
|
def initialize: (Hash[bot, bot] hash, untyped config, uri: OpenAPIParser::readable_uri?, schema_registry: Hash[bot, bot]) -> OpenAPIParser::Schemas::OpenAPI
|
|
17
|
+
def openapi: () -> String?
|
|
18
|
+
def openapi_version: () -> Gem::Version?
|
|
17
19
|
end
|
|
18
20
|
end
|
|
19
21
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: openapi_parser
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ota42y
|
|
@@ -188,6 +188,7 @@ files:
|
|
|
188
188
|
- lib/openapi_parser/schema_validator/options.rb
|
|
189
189
|
- lib/openapi_parser/schema_validator/properties_number.rb
|
|
190
190
|
- lib/openapi_parser/schema_validator/string_validator.rb
|
|
191
|
+
- lib/openapi_parser/schema_validator/type_mismatch_validator.rb
|
|
191
192
|
- lib/openapi_parser/schema_validator/unspecified_type_validator.rb
|
|
192
193
|
- lib/openapi_parser/schemas.rb
|
|
193
194
|
- lib/openapi_parser/schemas/base.rb
|
|
@@ -207,6 +208,25 @@ files:
|
|
|
207
208
|
- lib/openapi_parser/schemas/response.rb
|
|
208
209
|
- lib/openapi_parser/schemas/responses.rb
|
|
209
210
|
- lib/openapi_parser/schemas/schema.rb
|
|
211
|
+
- lib/openapi_parser/spec_validator.rb
|
|
212
|
+
- lib/openapi_parser/spec_validator/rule.rb
|
|
213
|
+
- lib/openapi_parser/spec_validator/rules/const_in_30.rb
|
|
214
|
+
- lib/openapi_parser/spec_validator/rules/content_encoding_in_30.rb
|
|
215
|
+
- lib/openapi_parser/spec_validator/rules/content_media_type_in_30.rb
|
|
216
|
+
- lib/openapi_parser/spec_validator/rules/content_schema_in_30.rb
|
|
217
|
+
- lib/openapi_parser/spec_validator/rules/dynamic_anchor_in_30.rb
|
|
218
|
+
- lib/openapi_parser/spec_validator/rules/dynamic_ref_in_30.rb
|
|
219
|
+
- lib/openapi_parser/spec_validator/rules/example_singular_deprecation.rb
|
|
220
|
+
- lib/openapi_parser/spec_validator/rules/exclusive_maximum.rb
|
|
221
|
+
- lib/openapi_parser/spec_validator/rules/exclusive_minimum.rb
|
|
222
|
+
- lib/openapi_parser/spec_validator/rules/json_schema_dialect_in_30.rb
|
|
223
|
+
- lib/openapi_parser/spec_validator/rules/nullable_deprecation.rb
|
|
224
|
+
- lib/openapi_parser/spec_validator/rules/path_items_in_30.rb
|
|
225
|
+
- lib/openapi_parser/spec_validator/rules/prefix_items_in_30.rb
|
|
226
|
+
- lib/openapi_parser/spec_validator/rules/type_array_in_30.rb
|
|
227
|
+
- lib/openapi_parser/spec_validator/rules/type_null_in_30.rb
|
|
228
|
+
- lib/openapi_parser/spec_validator/rules/webhooks_in_30.rb
|
|
229
|
+
- lib/openapi_parser/spec_validator/spec_violation.rb
|
|
210
230
|
- lib/openapi_parser/version.rb
|
|
211
231
|
- openapi_parser.gemspec
|
|
212
232
|
- sig/openapi_parser.rbs
|
|
@@ -217,6 +237,7 @@ files:
|
|
|
217
237
|
- sig/openapi_parser/schema_validators/base.rbs
|
|
218
238
|
- sig/openapi_parser/schema_validators/options.rbs
|
|
219
239
|
- sig/openapi_parser/schemas/base.rbs
|
|
240
|
+
- sig/openapi_parser/spec_validator.rbs
|
|
220
241
|
- sig/openapi_parser/version.rbs
|
|
221
242
|
- sig/types.rbs
|
|
222
243
|
- sig/wip_types.rbs
|
|
@@ -238,7 +259,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
238
259
|
- !ruby/object:Gem::Version
|
|
239
260
|
version: '0'
|
|
240
261
|
requirements: []
|
|
241
|
-
rubygems_version:
|
|
262
|
+
rubygems_version: 4.0.3
|
|
242
263
|
specification_version: 4
|
|
243
264
|
summary: OpenAPI3 parser
|
|
244
265
|
test_files: []
|