openapi_parser 2.3.1 → 2.5.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 +2 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +40 -0
- data/lib/openapi_parser/config.rb +12 -0
- data/lib/openapi_parser/errors.rb +7 -0
- data/lib/openapi_parser/schema_validator/array_validator.rb +7 -3
- data/lib/openapi_parser/schema_validator/integer_validator.rb +2 -0
- 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 +92 -1
- data/lib/openapi_parser/schemas/components.rb +4 -0
- data/lib/openapi_parser/schemas/openapi.rb +24 -0
- data/lib/openapi_parser/schemas/schema.rb +20 -1
- data/lib/openapi_parser/spec_validator/rule.rb +68 -0
- data/lib/openapi_parser/spec_validator/rules/boolean_schema_in_30.rb +26 -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/self_before_32.rb +20 -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 +89 -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 +117 -0
- data/sig/openapi_parser.rbs +2 -0
- metadata +25 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e570c33d05a24e9b11a3f5b86f5321f895088a672d404df68f3d14b4d3f9fb8f
|
|
4
|
+
data.tar.gz: fadcedd0e7b7c5ce057f2b0fa36f313218f151ae524dd2290c1242fbdfca02da
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 680325ae11c7dce38425d23c89918d3ce5cb768261dbd48f6d21c166ca0d89b2dbf9304297b2f754830f47cdf8d45dd2501371b396fe972957474e171f4b3b49
|
|
7
|
+
data.tar.gz: 27b757f70c102671ff1f2891918f5e7abb044796e280027b097214f4f0bbb2ff538b7f7c84d7d1bb7f1fc8b46b63591ad62e37985dab50ebb35c0603b854e92e
|
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,45 @@
|
|
|
1
1
|
## Unreleased
|
|
2
2
|
|
|
3
|
+
## 2.5.0 (2026-10-21)
|
|
4
|
+
* add OpenAPI 3.2 `SpecValidator` rules
|
|
5
|
+
* `SelfBefore32`: detect `$self` usage in pre-3.2 documents (3.2 addition)
|
|
6
|
+
* support root-level `$self` (OpenAPI 3.2) in the parse layer (`self_uri`); it is not yet used as the base URI for `$ref` resolution
|
|
7
|
+
* support boolean schemas (OpenAPI 3.1): `true` accepts any value and `false` rejects any value with `FalseSchemaError`, instead of crashing the loader with `NoMethodError`
|
|
8
|
+
* add `SpecValidator` rule `BooleanSchemaIn30`: detect boolean schemas in 3.0 documents (3.1 addition; `additionalProperties` is not affected)
|
|
9
|
+
* coerce non-String values into array-form `type` (3.1) the same way as a single `type` when `coerce_value` is on, so `type: [integer, "null"]` handles a Float the same way as `type: integer`
|
|
10
|
+
* stop truncating a Float with a fractional part in integer coercion (`coerce_value`): `1.5` for `type: integer` now fails instead of becoming `1`, while a Float with no fractional part, like `1.0`, is still converted
|
|
11
|
+
|
|
12
|
+
## 2.4.0 (2026-10-14)
|
|
13
|
+
* support `components.pathItems` so `$ref`s into it resolve, unblocking OpenAPI 3.1 documents that use reusable path items
|
|
14
|
+
* support array-form `type` (3.1) in value validation
|
|
15
|
+
* support root-level `jsonSchemaDialect` (OpenAPI 3.1) in the parse layer
|
|
16
|
+
* support `prefixItems` (OpenAPI 3.1) with positional tuple validation
|
|
17
|
+
* support `contentMediaType` (OpenAPI 3.1) in the parse layer
|
|
18
|
+
* support `contentEncoding` (OpenAPI 3.1) in the parse layer
|
|
19
|
+
* add `SpecValidator` with `strict_specification_version` config (`:silent` / `:warn` / `:raise`) to detect version mismatches between declared OpenAPI version and actual field usage
|
|
20
|
+
* `ContentEncodingIn30`: detect `contentEncoding` usage in 3.0 documents (3.1 addition)
|
|
21
|
+
* `ContentMediaTypeIn30`: detect `contentMediaType` usage in 3.0 documents (3.1 addition)
|
|
22
|
+
* `PrefixItemsIn30`: detect `prefixItems` usage in 3.0 documents (3.1 addition)
|
|
23
|
+
* `JsonSchemaDialectIn30`: detect root-level `jsonSchemaDialect` usage in 3.0 documents (3.1 addition)
|
|
24
|
+
* `TypeArrayIn30`: detect array-form `type` usage in 3.0 documents (3.1 form)
|
|
25
|
+
* `NullableDeprecation`: detect `nullable` usage in 3.1 documents (removed in 3.1)
|
|
26
|
+
* `ExampleSingularDeprecation`: detect singular `example` on schemas in 3.1 documents (deprecated in 3.1)
|
|
27
|
+
* `PathItemsIn30`: detect `components.pathItems` usage in 3.0 documents (3.1 addition)
|
|
28
|
+
* `ExclusiveMinimum` / `ExclusiveMaximum`: detect 3.0 Boolean vs 3.1 numeric form mismatch
|
|
29
|
+
* `TypeNullIn30`: detect `type: "null"` usage in 3.0 documents (3.1 primitive)
|
|
30
|
+
* `WebhooksIn30`: detect root-level `webhooks` usage in 3.0 documents (3.1 addition)
|
|
31
|
+
* `ConstIn30`: detect `const` usage in 3.0 documents (3.1 addition)
|
|
32
|
+
* `DynamicRefIn30`: detect `$dynamicRef` usage in 3.0 documents (3.1 addition)
|
|
33
|
+
* `DynamicAnchorIn30`: detect `$dynamicAnchor` usage in 3.0 documents (3.1 addition)
|
|
34
|
+
* `ContentSchemaIn30`: detect `contentSchema` usage in 3.0 documents (3.1 addition)
|
|
35
|
+
* 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
|
|
36
|
+
* support 3.1-style numeric `exclusiveMinimum` / `exclusiveMaximum` in value validation (standalone bound, not a Boolean modifier on `minimum` / `maximum`)
|
|
37
|
+
* support `type: "null"` (3.1 primitive) in value validation
|
|
38
|
+
* support root-level `webhooks` (OpenAPI 3.1) in the parse layer
|
|
39
|
+
* support `const` (OpenAPI 3.1) with exact-equality value validation
|
|
40
|
+
* support `contentSchema` (OpenAPI 3.1) in the parse layer
|
|
41
|
+
* coerce String values into array-form `type` (3.1) when `coerce_value` is on, so params like `type: [integer, "null"]` accept `"5"`
|
|
42
|
+
|
|
3
43
|
## 2.3.1 (2025-11-14)
|
|
4
44
|
* add optional date coercion with behavior matching existing datetime coercion
|
|
5
45
|
|
|
@@ -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
|
|
@@ -38,6 +38,13 @@ module OpenAPIParser
|
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
# 3.1: raised for any value checked against the boolean schema `false`
|
|
42
|
+
class FalseSchemaError < OpenAPIError
|
|
43
|
+
def message
|
|
44
|
+
"#{@reference} does not allow any value"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
41
48
|
class NotExistRequiredKey < OpenAPIError
|
|
42
49
|
def initialize(keys, reference)
|
|
43
50
|
super(reference)
|
|
@@ -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
|
|
@@ -21,6 +21,8 @@ class OpenAPIParser::SchemaValidator
|
|
|
21
21
|
|
|
22
22
|
def coerce(value)
|
|
23
23
|
return value if value.kind_of?(Integer)
|
|
24
|
+
# Integer() truncates a Float, so convert only one with no fractional part.
|
|
25
|
+
return value.finite? && value == value.floor ? value.to_i : value if value.kind_of?(Float)
|
|
24
26
|
|
|
25
27
|
begin
|
|
26
28
|
Integer(value)
|
|
@@ -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,22 @@ class OpenAPIParser::SchemaValidator
|
|
|
74
75
|
def validate_schema(value, schema, **keyword_args)
|
|
75
76
|
return [value, nil] unless schema
|
|
76
77
|
|
|
78
|
+
# 3.1: a boolean schema accepts (`true`) or rejects (`false`) any value,
|
|
79
|
+
# including null.
|
|
80
|
+
if schema.respond_to?(:raw_schema)
|
|
81
|
+
return [value, nil] if schema.raw_schema == true
|
|
82
|
+
return [nil, OpenAPIParser::FalseSchemaError.new(schema.object_reference)] if schema.raw_schema == false
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# 3.1: `const` pins the value to exactly that constant. Checked before
|
|
86
|
+
# type dispatch so it applies uniformly across primitives. Detection
|
|
87
|
+
# uses raw_schema so an intentional `const: null` is honored.
|
|
88
|
+
if schema.respond_to?(:raw_schema) && schema.raw_schema.is_a?(Hash) && schema.raw_schema.key?('const')
|
|
89
|
+
return [nil, OpenAPIParser::ValidateError.new(value, "const #{schema.const.inspect}", schema.object_reference)] if value != schema.const
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
return coerce_into_type_array(value, schema, **keyword_args) if coerce_into_type_array?(value, schema)
|
|
93
|
+
|
|
77
94
|
if (v = validator(value, schema))
|
|
78
95
|
if keyword_args.empty?
|
|
79
96
|
return v.coerce_and_validate(value, schema)
|
|
@@ -103,7 +120,25 @@ class OpenAPIParser::SchemaValidator
|
|
|
103
120
|
return one_of_validator if schema.one_of
|
|
104
121
|
return nil_validator if value.nil?
|
|
105
122
|
|
|
106
|
-
|
|
123
|
+
# 3.1: pick the type from an Array whose primitive class matches the
|
|
124
|
+
# value, then dispatch as if it were a single-type schema. If nothing
|
|
125
|
+
# matches we still pick a candidate so the relevant validator emits
|
|
126
|
+
# a sensible error rather than silently accepting via the unspecified
|
|
127
|
+
# fallback.
|
|
128
|
+
effective_type = schema.type
|
|
129
|
+
if effective_type.is_a?(Array)
|
|
130
|
+
matched = pick_array_type(value, effective_type)
|
|
131
|
+
return type_mismatch_validator if matched.nil?
|
|
132
|
+
|
|
133
|
+
effective_type = matched
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
validator_for_type(effective_type)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# @return [OpenAPIParser::SchemaValidator::Base]
|
|
140
|
+
def validator_for_type(type)
|
|
141
|
+
case type
|
|
107
142
|
when 'string'
|
|
108
143
|
string_validator
|
|
109
144
|
when 'integer'
|
|
@@ -116,11 +151,67 @@ class OpenAPIParser::SchemaValidator
|
|
|
116
151
|
object_validator
|
|
117
152
|
when 'array'
|
|
118
153
|
array_validator
|
|
154
|
+
when 'null'
|
|
155
|
+
# 3.1: only nil values are valid here. nil is handled earlier in
|
|
156
|
+
# this method, so a non-nil value reaching this branch is a type
|
|
157
|
+
# mismatch that should fail validation.
|
|
158
|
+
type_mismatch_validator
|
|
119
159
|
else
|
|
120
160
|
unspecified_type_validator
|
|
121
161
|
end
|
|
122
162
|
end
|
|
123
163
|
|
|
164
|
+
# 3.1: when coercing, a value whose class matches no listed type is tried
|
|
165
|
+
# against each listed type, the same way a single `type` coerces it.
|
|
166
|
+
# Request params arrive as Strings ("5" for `type: [integer, "null"]`),
|
|
167
|
+
# and a single `type: integer` also coerces a Float such as 1.0.
|
|
168
|
+
# nil is left to the nil validator, as it is for a single `type`.
|
|
169
|
+
# @return [Boolean]
|
|
170
|
+
def coerce_into_type_array?(value, schema)
|
|
171
|
+
return false unless @coerce_value && !value.nil?
|
|
172
|
+
return false if schema.any_of || schema.all_of || schema.one_of
|
|
173
|
+
|
|
174
|
+
schema.type.is_a?(Array) && pick_array_type(value, schema.type).nil?
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# 3.1: returns the first listed type that coerces, or the first type's error.
|
|
178
|
+
# @return [Array] coerced value and error
|
|
179
|
+
def coerce_into_type_array(value, schema, **keyword_args)
|
|
180
|
+
first_error = nil
|
|
181
|
+
(schema.type - ['null']).each do |type|
|
|
182
|
+
v = validator_for_type(type)
|
|
183
|
+
coerced, err = if keyword_args.empty?
|
|
184
|
+
v.coerce_and_validate(value, schema)
|
|
185
|
+
else
|
|
186
|
+
v.coerce_and_validate(value, schema, **keyword_args)
|
|
187
|
+
end
|
|
188
|
+
return [coerced, nil] if err.nil?
|
|
189
|
+
|
|
190
|
+
first_error ||= err
|
|
191
|
+
end
|
|
192
|
+
return [nil, first_error] if first_error
|
|
193
|
+
|
|
194
|
+
type_mismatch_validator.coerce_and_validate(value, schema)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def type_mismatch_validator
|
|
198
|
+
@type_mismatch_validator ||= OpenAPIParser::SchemaValidator::TypeMismatchValidator.new(self, @coerce_value)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def pick_array_type(value, types)
|
|
202
|
+
types.find do |t|
|
|
203
|
+
case t
|
|
204
|
+
when 'string' then value.is_a?(String)
|
|
205
|
+
when 'integer' then value.is_a?(Integer)
|
|
206
|
+
when 'number' then value.is_a?(Numeric)
|
|
207
|
+
when 'boolean' then value == true || value == false
|
|
208
|
+
when 'array' then value.is_a?(Array)
|
|
209
|
+
when 'object' then value.is_a?(Hash)
|
|
210
|
+
when 'null' then value.nil?
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
124
215
|
def string_validator
|
|
125
216
|
@string_validator ||= OpenAPIParser::SchemaValidator::StringValidator.new(self, @allow_empty_date_and_datetime, @coerce_value, @datetime_coerce_class, @date_coerce_class)
|
|
126
217
|
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,18 @@ 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
|
+
|
|
56
|
+
# @!attribute [r] self_uri
|
|
57
|
+
# @return [String, nil] the document's own URI, `$self` (OpenAPI 3.2+)
|
|
58
|
+
openapi_attr_value :self_uri, schema_key: :'$self'
|
|
59
|
+
|
|
36
60
|
# @return [OpenAPIParser::RequestOperation, nil]
|
|
37
61
|
def request_operation(http_method, request_path)
|
|
38
62
|
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
|
|
@@ -114,5 +125,13 @@ module OpenAPIParser::Schemas
|
|
|
114
125
|
def additional_properties
|
|
115
126
|
@additional_properties.nil? ? true : @additional_properties
|
|
116
127
|
end
|
|
128
|
+
|
|
129
|
+
# 3.1: a boolean schema (`true` / `false`) has no keywords to load.
|
|
130
|
+
def load_data
|
|
131
|
+
return super if raw_schema.is_a?(::Hash)
|
|
132
|
+
|
|
133
|
+
@_openapi_all_child_objects = {}
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
117
136
|
end
|
|
118
137
|
end
|
|
@@ -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,26 @@
|
|
|
1
|
+
module OpenAPIParser
|
|
2
|
+
class SpecValidator
|
|
3
|
+
module Rules
|
|
4
|
+
# A boolean schema (`true` / `false` in place of a schema object) comes
|
|
5
|
+
# from JSON Schema 2020-12, which 3.1 adopts. 3.0 allows a boolean only
|
|
6
|
+
# for `additionalProperties`, which is not loaded as a Schema, so any
|
|
7
|
+
# Schema whose raw value is a boolean is a spec mismatch in 3.0.
|
|
8
|
+
class BooleanSchemaIn30 < 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.raw_schema == true || schema.raw_schema == false
|
|
15
|
+
|
|
16
|
+
violations << violation(
|
|
17
|
+
path: schema.object_reference,
|
|
18
|
+
message: 'a boolean schema is a 3.1 addition (from JSON Schema 2020-12); 3.0 allows a boolean only for `additionalProperties`',
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
violations
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
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
|