openapi_parser 2.5.0 → 2.6.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 -3
- data/CHANGELOG.md +34 -0
- data/README.md +44 -1
- data/lib/openapi_parser/concerns/media_type_selectable.rb +18 -0
- data/lib/openapi_parser/concerns/schema_loader/creator.rb +7 -2
- data/lib/openapi_parser/concerns/schema_loader/hash_body_loader.rb +1 -1
- data/lib/openapi_parser/config.rb +5 -0
- data/lib/openapi_parser/errors.rb +24 -0
- data/lib/openapi_parser/schema_validator/any_of_validator.rb +1 -1
- data/lib/openapi_parser/schema_validator/base.rb +35 -9
- data/lib/openapi_parser/schema_validator/float_validator.rb +4 -0
- data/lib/openapi_parser/schema_validator/integer_validator.rb +17 -1
- data/lib/openapi_parser/schema_validator/multiple_of.rb +26 -0
- data/lib/openapi_parser/schema_validator/nil_validator.rb +4 -0
- data/lib/openapi_parser/schema_validator/one_of_validator.rb +1 -1
- data/lib/openapi_parser/schema_validator.rb +32 -4
- data/lib/openapi_parser/schemas/components.rb +4 -0
- data/lib/openapi_parser/schemas/discriminator.rb +4 -0
- data/lib/openapi_parser/schemas/media_type.rb +4 -0
- data/lib/openapi_parser/schemas/openapi.rb +36 -3
- data/lib/openapi_parser/schemas/path_item.rb +32 -6
- data/lib/openapi_parser/schemas/request_body.rb +1 -1
- data/lib/openapi_parser/schemas/response.rb +5 -1
- data/lib/openapi_parser/schemas/schema.rb +8 -1
- data/lib/openapi_parser/spec_validator/rule.rb +24 -3
- data/lib/openapi_parser/spec_validator/rules/additional_operations_before_32.rb +26 -0
- data/lib/openapi_parser/spec_validator/rules/allow_empty_value_deprecation.rb +26 -0
- data/lib/openapi_parser/spec_validator/rules/cookie_style_before_32.rb +26 -0
- data/lib/openapi_parser/spec_validator/rules/default_mapping_before_32.rb +29 -0
- data/lib/openapi_parser/spec_validator/rules/defs_in_30.rb +25 -0
- data/lib/openapi_parser/spec_validator/rules/example_value_fields_before_32.rb +48 -0
- data/lib/openapi_parser/spec_validator/rules/media_types_before_32.rb +24 -0
- data/lib/openapi_parser/spec_validator/rules/query_method_before_32.rb +26 -0
- data/lib/openapi_parser/spec_validator/rules/querystring_before_32.rb +26 -0
- data/lib/openapi_parser/spec_validator/rules/response_summary_before_32.rb +25 -0
- data/lib/openapi_parser/spec_validator/rules/security_scheme_fields_before_32.rb +41 -0
- data/lib/openapi_parser/spec_validator/rules/server_name_before_32.rb +35 -0
- data/lib/openapi_parser/spec_validator/rules/streaming_fields_before_32.rb +56 -0
- data/lib/openapi_parser/spec_validator/rules/tag_fields_before_32.rb +34 -0
- data/lib/openapi_parser/spec_validator/rules/xml_attribute_deprecation.rb +29 -0
- data/lib/openapi_parser/spec_validator/rules/xml_node_type_before_32.rb +29 -0
- data/lib/openapi_parser/spec_validator/rules/xml_wrapped_deprecation.rb +29 -0
- data/lib/openapi_parser/spec_validator.rb +34 -0
- data/lib/openapi_parser/version.rb +1 -1
- data/lib/openapi_parser.rb +5 -4
- data/sig/openapi_parser/config.rbs +1 -0
- data/sig/openapi_parser/schema_validator.rbs +1 -0
- data/sig/openapi_parser/schema_validators/base.rbs +4 -2
- data/sig/openapi_parser/spec_validator.rbs +77 -0
- data/sig/openapi_parser.rbs +5 -3
- data/sig/wip_types.rbs +6 -0
- metadata +19 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 265685ba8a1e648f9b4611356022227b5d80917f46d2450dae55747ffec40a23
|
|
4
|
+
data.tar.gz: 9ba7d7d561d7ec84a5ce6a8c42cb13beffa09e1250699b206f6e4ec639350575
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4168f6e1dcedcd2c8be552937692215f8214511df0979f7bcc3d4ec00181980e5d397bc5cb0288cf9fc498eafc3a1322f0f1579bad89fb4915221b6b62a4f667
|
|
7
|
+
data.tar.gz: 6fe2b4ae0877312fcdd0f6fc56643c7658e9e6325cffaa066caec1b263e6657c88db5a7233d6bd24ce7d8270ff8d5e1d7d8c7b38188e4ac237ca86cb5f374a4c
|
data/.github/workflows/ci.yaml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
## Unreleased
|
|
2
2
|
|
|
3
|
+
## 2.6.0 (2026-11-04)
|
|
4
|
+
* support the `multipleOf` keyword (OpenAPI 3.0 and 3.1): a number that is not a multiple of it now fails with `NotMultipleOf`. Decimals are compared exactly, so `19.99` is a multiple of `0.01`. A `multipleOf` that is not a positive number is still ignored. It was ignored before, so a value that passed may now fail
|
|
5
|
+
* support the `not` keyword (OpenAPI 3.0 and 3.1): a value that is valid against the `not` schema now fails with `NotSchemaMatched`. It was ignored before, so a value that passed may now fail
|
|
6
|
+
* add OpenAPI 3.2 `SpecValidator` rules
|
|
7
|
+
* `CookieStyleBefore32`: detect Parameter `style: cookie` usage in pre-3.2 documents (3.2 addition)
|
|
8
|
+
* `DefaultMappingBefore32`: detect Discriminator `defaultMapping` usage in pre-3.2 documents (3.2 addition)
|
|
9
|
+
* `MediaTypesBefore32`: detect `components.mediaTypes` usage in pre-3.2 documents (3.2 addition)
|
|
10
|
+
* `XmlNodeTypeBefore32`: detect XML Object `nodeType` usage in pre-3.2 documents (3.2 addition)
|
|
11
|
+
* `XmlAttributeDeprecation` / `XmlWrappedDeprecation`: detect XML Object `attribute: true` / `wrapped: true` in 3.2 documents (deprecated in 3.2 in favor of `nodeType`)
|
|
12
|
+
* `QueryMethodBefore32`: detect Path Item `query` usage in pre-3.2 documents (3.2 addition)
|
|
13
|
+
* `AdditionalOperationsBefore32`: detect Path Item `additionalOperations` usage in pre-3.2 documents (3.2 addition)
|
|
14
|
+
* `ResponseSummaryBefore32`: detect Response Object `summary` usage in pre-3.2 documents (3.2 addition)
|
|
15
|
+
* `TagFieldsBefore32`: detect Tag Object `summary` / `parent` / `kind` usage in pre-3.2 documents (3.2 additions)
|
|
16
|
+
* `ServerNameBefore32`: detect Server Object `name` usage in pre-3.2 documents (3.2 addition)
|
|
17
|
+
* `AllowEmptyValueDeprecation`: detect Parameter `allowEmptyValue` usage in 3.2 documents (deprecated in 3.2)
|
|
18
|
+
* `ExampleValueFieldsBefore32`: detect Example Object `dataValue` / `serializedValue` usage in pre-3.2 documents (3.2 additions)
|
|
19
|
+
* `SecuritySchemeFieldsBefore32`: detect Security Scheme `deprecated` / `oauth2MetadataUrl` and the `deviceAuthorization` OAuth flow in pre-3.2 documents (3.2 additions)
|
|
20
|
+
* `StreamingFieldsBefore32`: detect Media Type `itemSchema` / `itemEncoding` / `prefixEncoding` and nested Encoding Object `encoding` / `itemEncoding` / `prefixEncoding` usage in pre-3.2 documents (3.2 additions)
|
|
21
|
+
* `QuerystringBefore32`: detect Parameter `in: querystring` usage in pre-3.2 documents (3.2 addition; not validated at runtime)
|
|
22
|
+
* return nil from `request_operation` for a document without `paths` (3.1 allows a document with only `webhooks`), instead of raising `NoMethodError`
|
|
23
|
+
* support `$defs` (OpenAPI 3.1) in Schema Objects so `$ref`s into it, such as `#/components/schemas/Pet/$defs/Id`, resolve
|
|
24
|
+
* add `SpecValidator` rule `DefsIn30`: detect `$defs` in 3.0 documents (3.1 addition)
|
|
25
|
+
* validate a value against a schema without `type`: keywords such as `properties`, `required`, `items`, `maxLength`, `enum` and `minimum` now apply when the value has the type the keyword is for, as in JSON Schema. They were all skipped before, so a value that breaks them may now fail validation
|
|
26
|
+
* support Discriminator `defaultMapping` (OpenAPI 3.2): fallback schema when the discriminator property is absent or its value has no explicit or implicit mapping; honored in 3.2 documents
|
|
27
|
+
* add `allow_3_2_features` config to apply OpenAPI 3.2 runtime behavior to documents that declare an earlier version (default `false`: pre-3.2 documents behave as before)
|
|
28
|
+
* accept `null` for a schema without `type`, as in JSON Schema. `enum` still applies, so `null` must be listed there when `enum` is set. A schema without `type` used to reject `null` with `NotNullError`, so a value that failed before may now pass
|
|
29
|
+
* accept a Float with no fractional part, such as `1.0`, for `type: integer` in OpenAPI 3.1 documents, as in JSON Schema. OpenAPI 3.0 defines an integer as a number without a fraction, so 3.0 documents still reject it. In 3.1 documents a value that failed before may now pass, and `oneOf` with both `integer` and `number` now rejects `1.0` because it matches both
|
|
30
|
+
* support `components.mediaTypes` (OpenAPI 3.2), resolving `$ref`s in request body and response `content` in 3.2 documents; an unresolved one accepts any body unless `strict_reference_validation` is set
|
|
31
|
+
* support the `query` HTTP method and `additionalOperations` (OpenAPI 3.2) on Path Items, including request-operation lookup, in 3.2 documents
|
|
32
|
+
* match HTTP methods case-insensitively in request-operation lookup in 3.2 documents (`request_operation("GET", path)` still returns nil before 3.2)
|
|
33
|
+
* support Response Object `summary` (OpenAPI 3.2) in the parse layer
|
|
34
|
+
* accept a file object for array-form `type` with `format: binary` (3.1) when `coerce_value` is off, the same way as a single `type: string`
|
|
35
|
+
* support `itemSchema` (OpenAPI 3.2) in the parse layer; it is not yet used to validate sequential media type bodies
|
|
36
|
+
|
|
3
37
|
## 2.5.0 (2026-10-21)
|
|
4
38
|
* add OpenAPI 3.2 `SpecValidator` rules
|
|
5
39
|
* `SelfBefore32`: detect `$self` usage in pre-3.2 documents (3.2 addition)
|
data/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://www.rubydoc.info/gems/openapi_parser)
|
|
5
5
|
[](https://inch-ci.org/github/ota42y/openapi_parser)
|
|
6
6
|
|
|
7
|
-
This is OpenAPI3 parser and validator.
|
|
7
|
+
This is OpenAPI3 parser and validator (OpenAPI 3.0, 3.1 and 3.2).
|
|
8
8
|
|
|
9
9
|
## Usage
|
|
10
10
|
|
|
@@ -89,6 +89,49 @@ options = {
|
|
|
89
89
|
OpenAPIParser.parse(yaml_file, options)
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
+
### Supported OpenAPI versions
|
|
93
|
+
|
|
94
|
+
The parser accepts OpenAPI 3.0, 3.1, and 3.2 documents. Parsing and runtime request/response validation are version-permissive: supported keywords are honored regardless of the document's declared `openapi:` version (for example, both `nullable` and `type: [..., "null"]` work everywhere).
|
|
95
|
+
|
|
96
|
+
The exception is 3.2 behavior that would change how an existing 3.0 or 3.1 document validates. It applies only to documents that declare 3.2 or later:
|
|
97
|
+
|
|
98
|
+
- `$ref`s in request body and response `content` (e.g. into `components.mediaTypes`)
|
|
99
|
+
- the `query` operation, `additionalOperations`, and case-insensitive method matching in `request_operation`
|
|
100
|
+
- Discriminator `defaultMapping`
|
|
101
|
+
|
|
102
|
+
A file loaded through a `$ref` that declares no `openapi:` version follows the document that loaded it. To opt earlier documents in, pass `allow_3_2_features: true`:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
OpenAPIParser.parse(schema, allow_3_2_features: true)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Some 3.1 and 3.2 features are parsed or detected but do not affect runtime validation:
|
|
109
|
+
|
|
110
|
+
- `contentEncoding`, `contentMediaType`, and `contentSchema` (3.1) are parsed but not validated
|
|
111
|
+
- `$dynamicRef` and `$dynamicAnchor` (3.1) are not resolved
|
|
112
|
+
- `$self` (3.2) is parsed (`OpenAPI#self_uri`) but not used as the base URI for `$ref` resolution
|
|
113
|
+
- `itemSchema` (3.2) is parsed but not used to validate sequential media type bodies (SSE, JSON Lines, JSON Sequences)
|
|
114
|
+
- parameters with `in: querystring` (3.2) are not validated
|
|
115
|
+
- Tag, Server, Example, XML, and Security Scheme objects are not modeled; they are only checked by the `SpecValidator`
|
|
116
|
+
|
|
117
|
+
Version-specific correctness is checked separately by the `SpecValidator`, driven by the `strict_specification_version` option:
|
|
118
|
+
|
|
119
|
+
- `:silent` (default) — no version checks
|
|
120
|
+
- `:warn` — print a warning for each mismatch (e.g. a 3.1-only keyword in a 3.0 document)
|
|
121
|
+
- `:raise` — raise `OpenAPIParser::SpecViolationError` listing all mismatches
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
OpenAPIParser.parse(schema, strict_specification_version: :raise)
|
|
125
|
+
# => raises OpenAPIParser::SpecViolationError if e.g. a 3.0 document uses `prefixItems`
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Detected mismatches:
|
|
129
|
+
|
|
130
|
+
- 3.1 additions in 3.0 documents: `const`, `prefixItems`, array-form `type`, `type: "null"`, `webhooks`, `jsonSchemaDialect`, `components.pathItems`, `$dynamicRef`, `$dynamicAnchor`, `contentEncoding`, `contentMediaType`, `contentSchema`, `$defs`, boolean schemas
|
|
131
|
+
- the 3.0 Boolean vs 3.1 numeric forms of `exclusiveMinimum` / `exclusiveMaximum`
|
|
132
|
+
- 3.2 additions in earlier documents: `$self`, the `query` operation, `additionalOperations`, `components.mediaTypes`, Tag `summary` / `parent` / `kind`, Server `name`, Example `dataValue` / `serializedValue`, XML `nodeType`, Security Scheme `deprecated` / `oauth2MetadataUrl` and the `deviceAuthorization` flow, Media Type and Encoding `itemSchema` / `itemEncoding` / `prefixEncoding`, Discriminator `defaultMapping`, Parameter `in: querystring` and `style: cookie`, Response `summary`
|
|
133
|
+
- keywords removed or deprecated by the declared version: `nullable` and singular schema `example` in 3.1+, XML `attribute: true` / `wrapped: true` and Parameter `allowEmptyValue` in 3.2
|
|
134
|
+
|
|
92
135
|
## ToDo
|
|
93
136
|
- correct schema checker
|
|
94
137
|
- more detailed validator
|
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
module OpenAPIParser::MediaTypeSelectable
|
|
2
|
+
# `content` accepts $refs only where 3.2 runtime behavior applies; before
|
|
3
|
+
# 3.2 a `$ref` there is parsed as a MediaType with no schema, as it always was
|
|
4
|
+
CONTENT_REFERENCE = ->(target) { target.root.use_3_2_features? }
|
|
5
|
+
|
|
6
|
+
# A `content` $ref left unresolved (strict_reference_validation off) falls
|
|
7
|
+
# back to a schemaless MediaType instead of staying a Reference
|
|
8
|
+
def expand_reference(root, validate_references)
|
|
9
|
+
super
|
|
10
|
+
|
|
11
|
+
content&.each do |key, media_type|
|
|
12
|
+
next unless media_type.kind_of?(OpenAPIParser::Schemas::Reference)
|
|
13
|
+
|
|
14
|
+
fallback = OpenAPIParser::Schemas::MediaType.new(media_type.object_reference, self, root, media_type.raw_schema)
|
|
15
|
+
_update_child_object(media_type, fallback)
|
|
16
|
+
content[key] = fallback
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
2
20
|
private
|
|
3
21
|
|
|
4
22
|
# select media type by content_type (consider wild card definition)
|
|
@@ -26,12 +26,17 @@ class OpenAPIParser::SchemaLoader::Creator < OpenAPIParser::SchemaLoader::Base
|
|
|
26
26
|
check_object_schema?(check_schema) && !check_schema['$ref'].nil?
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
# `reference:` is a Boolean, or a Proc taking the owning object
|
|
30
|
+
def allow_reference?(target_object)
|
|
31
|
+
@allow_reference.respond_to?(:call) ? @allow_reference.call(target_object) : @allow_reference
|
|
32
|
+
end
|
|
33
|
+
|
|
29
34
|
def check_object_schema?(check_schema)
|
|
30
35
|
check_schema.kind_of?(::Hash)
|
|
31
36
|
end
|
|
32
37
|
|
|
33
38
|
def escape_reference(str)
|
|
34
|
-
str.to_s.gsub('/', '~1')
|
|
39
|
+
str.to_s.gsub('~', '~0').gsub('/', '~1')
|
|
35
40
|
end
|
|
36
41
|
|
|
37
42
|
def build_openapi_object_from_option(target_object, ref, schema)
|
|
@@ -39,7 +44,7 @@ class OpenAPIParser::SchemaLoader::Creator < OpenAPIParser::SchemaLoader::Base
|
|
|
39
44
|
|
|
40
45
|
if @allow_data_type && !check_object_schema?(schema)
|
|
41
46
|
schema
|
|
42
|
-
elsif
|
|
47
|
+
elsif allow_reference?(target_object) && check_reference_schema?(schema)
|
|
43
48
|
OpenAPIParser::Schemas::Reference.new(ref, target_object, target_object.root, schema)
|
|
44
49
|
else
|
|
45
50
|
@klass.new(ref, target_object, target_object.root, schema)
|
|
@@ -21,7 +21,7 @@ class OpenAPIParser::SchemaLoader::HashBodyLoader < OpenAPIParser::SchemaLoader:
|
|
|
21
21
|
# for responses and paths object
|
|
22
22
|
def create_hash_body_objects(target_object, raw_schema)
|
|
23
23
|
object_list = raw_schema.reject { |k, _| reject_keys.include?(k) }.map do |child_name, child_schema|
|
|
24
|
-
ref = build_object_reference_from_base(target_object.object_reference,
|
|
24
|
+
ref = build_object_reference_from_base(target_object.object_reference, child_name)
|
|
25
25
|
[
|
|
26
26
|
child_name.to_s, # support string key only in OpenAPI3
|
|
27
27
|
build_openapi_object_from_option(target_object, ref, child_schema),
|
|
@@ -15,6 +15,11 @@ class OpenAPIParser::Config
|
|
|
15
15
|
@config.fetch(:allow_empty_date_and_datetime, false)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
# apply OpenAPI 3.2 runtime behavior to documents that declare an earlier version
|
|
19
|
+
def allow_3_2_features
|
|
20
|
+
@config.fetch(:allow_3_2_features, false)
|
|
21
|
+
end
|
|
22
|
+
|
|
18
23
|
def datetime_coerce_class
|
|
19
24
|
@config[:datetime_coerce_class]
|
|
20
25
|
end
|
|
@@ -112,6 +112,18 @@ module OpenAPIParser
|
|
|
112
112
|
end
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
+
# raised for a value that is valid against the `not` schema
|
|
116
|
+
class NotSchemaMatched < OpenAPIError
|
|
117
|
+
def initialize(value, reference)
|
|
118
|
+
super(reference)
|
|
119
|
+
@value = value
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def message
|
|
123
|
+
"#{@value.inspect} must not be valid against #{@reference}"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
115
127
|
class NotEnumInclude < OpenAPIError
|
|
116
128
|
def initialize(value, reference)
|
|
117
129
|
super(reference)
|
|
@@ -123,6 +135,18 @@ module OpenAPIParser
|
|
|
123
135
|
end
|
|
124
136
|
end
|
|
125
137
|
|
|
138
|
+
class NotMultipleOf < OpenAPIError
|
|
139
|
+
def initialize(value, multiple_of, reference)
|
|
140
|
+
super(reference)
|
|
141
|
+
@value = value
|
|
142
|
+
@multiple_of = multiple_of
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def message
|
|
146
|
+
"#{@value.inspect} isn't a multiple of #{@multiple_of.inspect} in #{@reference}"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
126
150
|
class LessThanMinimum < OpenAPIError
|
|
127
151
|
def initialize(value, reference)
|
|
128
152
|
super(reference)
|
|
@@ -7,7 +7,7 @@ class OpenAPIParser::SchemaValidator
|
|
|
7
7
|
return [value, nil]
|
|
8
8
|
end
|
|
9
9
|
if schema.discriminator
|
|
10
|
-
return validate_discriminator_schema(schema.discriminator, value)
|
|
10
|
+
return validate_discriminator_schema(schema.discriminator, value, candidate_schemas: schema.any_of)
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
# in all schema return error (=true) not any of data
|
|
@@ -12,19 +12,32 @@ class OpenAPIParser::SchemaValidator
|
|
|
12
12
|
raise 'need implement'
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def validate_discriminator_schema(discriminator, value, parent_discriminator_schemas: [])
|
|
15
|
+
def validate_discriminator_schema(discriminator, value, candidate_schemas: nil, parent_discriminator_schemas: [])
|
|
16
16
|
property_name = discriminator.property_name
|
|
17
|
-
if property_name
|
|
18
|
-
|
|
17
|
+
if property_name && value.key?(property_name)
|
|
18
|
+
mapping_key = value[property_name]
|
|
19
|
+
explicit_target = discriminator.mapping&.[](mapping_key)
|
|
20
|
+
if explicit_target
|
|
21
|
+
mapping_target = explicit_target
|
|
22
|
+
resolved_schema = discriminator.root.find_object(mapping_target)
|
|
23
|
+
else
|
|
24
|
+
mapping_target = "#/components/schemas/#{mapping_key}"
|
|
25
|
+
# Implicit values are component names, not reference paths.
|
|
26
|
+
resolved_schema = discriminator.root.components&.schemas&.[](mapping_key.to_s)
|
|
27
|
+
resolved_schema = nil if candidate_schemas && resolved_schema && !candidate_schemas.include?(resolved_schema)
|
|
28
|
+
end
|
|
19
29
|
end
|
|
20
|
-
mapping_key = value[property_name]
|
|
21
30
|
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
|
|
31
|
+
# defaultMapping (3.2) applies when the property is absent or its value
|
|
32
|
+
# has neither an explicit mapping nor an implicit schema match
|
|
33
|
+
if resolved_schema.nil? && explicit_target.nil? && (default_target = default_mapping_target(discriminator))
|
|
34
|
+
mapping_target = default_target
|
|
35
|
+
resolved_schema = discriminator.root.find_object(default_target)
|
|
36
|
+
end
|
|
25
37
|
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
unless mapping_target
|
|
39
|
+
return [nil, OpenAPIParser::NotExistDiscriminatorPropertyName.new(discriminator.property_name, value, discriminator.object_reference)]
|
|
40
|
+
end
|
|
28
41
|
|
|
29
42
|
unless resolved_schema
|
|
30
43
|
return [nil, OpenAPIParser::NotExistDiscriminatorMappedSchema.new(mapping_target, discriminator.object_reference)]
|
|
@@ -35,5 +48,18 @@ class OpenAPIParser::SchemaValidator
|
|
|
35
48
|
**{discriminator_property_name: discriminator.property_name, parent_discriminator_schemas: parent_discriminator_schemas}
|
|
36
49
|
)
|
|
37
50
|
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# defaultMapping holds a schema name or a URI reference; only
|
|
55
|
+
# same-document references resolve. Ignored where 3.2 behavior doesn't apply
|
|
56
|
+
def default_mapping_target(discriminator)
|
|
57
|
+
return nil unless discriminator.root.use_3_2_features?
|
|
58
|
+
|
|
59
|
+
target = discriminator.default_mapping
|
|
60
|
+
return nil unless target.is_a?(String)
|
|
61
|
+
|
|
62
|
+
target.start_with?('#') || target.include?('/') ? target : "#/components/schemas/#{target}"
|
|
63
|
+
end
|
|
38
64
|
end
|
|
39
65
|
end
|
|
@@ -2,6 +2,7 @@ class OpenAPIParser::SchemaValidator
|
|
|
2
2
|
class FloatValidator < Base
|
|
3
3
|
include ::OpenAPIParser::SchemaValidator::Enumable
|
|
4
4
|
include ::OpenAPIParser::SchemaValidator::MinimumMaximum
|
|
5
|
+
include ::OpenAPIParser::SchemaValidator::MultipleOf
|
|
5
6
|
|
|
6
7
|
# validate float value by schema
|
|
7
8
|
# @param [Object] value
|
|
@@ -22,6 +23,9 @@ class OpenAPIParser::SchemaValidator
|
|
|
22
23
|
value, err = check_enum_include(value, schema)
|
|
23
24
|
return [nil, err] if err
|
|
24
25
|
|
|
26
|
+
value, err = check_multiple_of(value, schema)
|
|
27
|
+
return [nil, err] if err
|
|
28
|
+
|
|
25
29
|
check_minimum_maximum(value, schema)
|
|
26
30
|
end
|
|
27
31
|
|
|
@@ -2,6 +2,7 @@ class OpenAPIParser::SchemaValidator
|
|
|
2
2
|
class IntegerValidator < Base
|
|
3
3
|
include ::OpenAPIParser::SchemaValidator::Enumable
|
|
4
4
|
include ::OpenAPIParser::SchemaValidator::MinimumMaximum
|
|
5
|
+
include ::OpenAPIParser::SchemaValidator::MultipleOf
|
|
5
6
|
|
|
6
7
|
# validate integer value by schema
|
|
7
8
|
# @param [Object] value
|
|
@@ -9,14 +10,29 @@ class OpenAPIParser::SchemaValidator
|
|
|
9
10
|
def coerce_and_validate(value, schema, **_keyword_args)
|
|
10
11
|
value = coerce(value) if @coerce_value
|
|
11
12
|
|
|
12
|
-
return OpenAPIParser::ValidateError.build_error_result(value, schema) unless
|
|
13
|
+
return OpenAPIParser::ValidateError.build_error_result(value, schema) unless integer?(value, schema)
|
|
13
14
|
|
|
14
15
|
value, err = check_enum_include(value, schema)
|
|
15
16
|
return [nil, err] if err
|
|
16
17
|
|
|
18
|
+
value, err = check_multiple_of(value, schema)
|
|
19
|
+
return [nil, err] if err
|
|
20
|
+
|
|
17
21
|
check_minimum_maximum(value, schema)
|
|
18
22
|
end
|
|
19
23
|
|
|
24
|
+
# 3.1 follows JSON Schema, where 1.0 is an integer. 3.0 defines an
|
|
25
|
+
# integer as a number without a fraction, so 1.0 is not one there.
|
|
26
|
+
# @param [Object] value
|
|
27
|
+
# @param [OpenAPIParser::Schemas::Schema] schema
|
|
28
|
+
# @return [Boolean]
|
|
29
|
+
def integer?(value, schema)
|
|
30
|
+
return true if value.kind_of?(Integer)
|
|
31
|
+
return false unless value.kind_of?(Float) && value.finite? && value == value.floor
|
|
32
|
+
|
|
33
|
+
schema.root.use_3_1_features?
|
|
34
|
+
end
|
|
35
|
+
|
|
20
36
|
private
|
|
21
37
|
|
|
22
38
|
def coerce(value)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class OpenAPIParser::SchemaValidator
|
|
2
|
+
module MultipleOf
|
|
3
|
+
# check multipleOf by schema
|
|
4
|
+
# @param [Numeric] value
|
|
5
|
+
# @param [OpenAPIParser::Schemas::Schema] schema
|
|
6
|
+
def check_multiple_of(value, schema)
|
|
7
|
+
multiple_of = schema.multipleOf
|
|
8
|
+
# multipleOf MUST be a number strictly greater than 0; ignore an invalid one.
|
|
9
|
+
return [value, nil] unless multiple_of.is_a?(Numeric) && multiple_of.positive?
|
|
10
|
+
return [value, nil] if (exact(value) % exact(multiple_of)).zero?
|
|
11
|
+
|
|
12
|
+
[nil, OpenAPIParser::NotMultipleOf.new(value, multiple_of, schema.object_reference)]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
# A Float such as 0.01 is not exact in binary, so 19.99 % 0.01 is not zero.
|
|
18
|
+
# Both numbers come from decimal text in JSON or YAML, so divide them as
|
|
19
|
+
# the decimals that Float#to_s gives back.
|
|
20
|
+
def exact(number)
|
|
21
|
+
return Rational(number.to_s) if number.is_a?(Float) && number.finite?
|
|
22
|
+
|
|
23
|
+
number
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
class OpenAPIParser::SchemaValidator
|
|
2
2
|
class NilValidator < Base
|
|
3
|
+
include ::OpenAPIParser::SchemaValidator::Enumable
|
|
4
|
+
|
|
3
5
|
# @param [Object] value
|
|
4
6
|
# @param [OpenAPIParser::Schemas::Schema] schema
|
|
5
7
|
def coerce_and_validate(value, schema, **_keyword_args)
|
|
@@ -8,6 +10,8 @@ class OpenAPIParser::SchemaValidator
|
|
|
8
10
|
return [value, nil] if schema.type == 'null'
|
|
9
11
|
# 3.1: `type: [..., "null"]` lets nil coexist with another primitive.
|
|
10
12
|
return [value, nil] if schema.type.is_a?(Array) && schema.type.include?('null')
|
|
13
|
+
# A schema without `type` accepts null, as in JSON Schema. `enum` still applies.
|
|
14
|
+
return check_enum_include(value, schema) if schema.type.nil?
|
|
11
15
|
|
|
12
16
|
[nil, OpenAPIParser::NotNullError.new(schema.object_reference)]
|
|
13
17
|
end
|
|
@@ -7,7 +7,7 @@ class OpenAPIParser::SchemaValidator
|
|
|
7
7
|
return [value, nil]
|
|
8
8
|
end
|
|
9
9
|
if schema.discriminator
|
|
10
|
-
return validate_discriminator_schema(schema.discriminator, value)
|
|
10
|
+
return validate_discriminator_schema(schema.discriminator, value, candidate_schemas: schema.one_of)
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
# if multiple schemas are satisfied, it's not valid
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require_relative 'schema_validator/options'
|
|
2
2
|
require_relative 'schema_validator/enumable'
|
|
3
3
|
require_relative 'schema_validator/minimum_maximum'
|
|
4
|
+
require_relative 'schema_validator/multiple_of'
|
|
4
5
|
require_relative 'schema_validator/properties_number'
|
|
5
6
|
require_relative 'schema_validator/base'
|
|
6
7
|
require_relative 'schema_validator/string_validator'
|
|
@@ -37,6 +38,10 @@ class OpenAPIParser::SchemaValidator
|
|
|
37
38
|
|
|
38
39
|
include Validatable
|
|
39
40
|
|
|
41
|
+
# Types to pick from for a schema without `type`. integer comes before
|
|
42
|
+
# number so that an Integer is checked as an integer.
|
|
43
|
+
VALUE_TYPES = %w[object array string integer number boolean].freeze
|
|
44
|
+
|
|
40
45
|
class << self
|
|
41
46
|
# validate schema data
|
|
42
47
|
# @param [Hash] value
|
|
@@ -89,6 +94,12 @@ class OpenAPIParser::SchemaValidator
|
|
|
89
94
|
return [nil, OpenAPIParser::ValidateError.new(value, "const #{schema.const.inspect}", schema.object_reference)] if value != schema.const
|
|
90
95
|
end
|
|
91
96
|
|
|
97
|
+
# `not` rejects a value that is valid against its schema.
|
|
98
|
+
if schema.respond_to?(:not) && schema.not.is_a?(OpenAPIParser::Schemas::Schema)
|
|
99
|
+
_, not_error = validate_schema(value, schema.not)
|
|
100
|
+
return [nil, OpenAPIParser::NotSchemaMatched.new(value, schema.not.object_reference)] unless not_error
|
|
101
|
+
end
|
|
102
|
+
|
|
92
103
|
return coerce_into_type_array(value, schema, **keyword_args) if coerce_into_type_array?(value, schema)
|
|
93
104
|
|
|
94
105
|
if (v = validator(value, schema))
|
|
@@ -120,6 +131,11 @@ class OpenAPIParser::SchemaValidator
|
|
|
120
131
|
return one_of_validator if schema.one_of
|
|
121
132
|
return nil_validator if value.nil?
|
|
122
133
|
|
|
134
|
+
# 3.1: `format: binary` with `string` in an array-form `type` accepts a
|
|
135
|
+
# non-String object such as an uploaded file, the same way as a single
|
|
136
|
+
# `type: string`. The string validator skips the check for `binary`.
|
|
137
|
+
return string_validator if binary_string_in_type_array?(value, schema)
|
|
138
|
+
|
|
123
139
|
# 3.1: pick the type from an Array whose primitive class matches the
|
|
124
140
|
# value, then dispatch as if it were a single-type schema. If nothing
|
|
125
141
|
# matches we still pick a candidate so the relevant validator emits
|
|
@@ -127,12 +143,16 @@ class OpenAPIParser::SchemaValidator
|
|
|
127
143
|
# fallback.
|
|
128
144
|
effective_type = schema.type
|
|
129
145
|
if effective_type.is_a?(Array)
|
|
130
|
-
matched =
|
|
146
|
+
matched = pick_type(value, effective_type, schema)
|
|
131
147
|
return type_mismatch_validator if matched.nil?
|
|
132
148
|
|
|
133
149
|
effective_type = matched
|
|
134
150
|
end
|
|
135
151
|
|
|
152
|
+
# JSON Schema applies each keyword when the value has the type the
|
|
153
|
+
# keyword is for, whether or not `type` is set.
|
|
154
|
+
effective_type ||= pick_type(value, VALUE_TYPES, schema)
|
|
155
|
+
|
|
136
156
|
validator_for_type(effective_type)
|
|
137
157
|
end
|
|
138
158
|
|
|
@@ -161,6 +181,14 @@ class OpenAPIParser::SchemaValidator
|
|
|
161
181
|
end
|
|
162
182
|
end
|
|
163
183
|
|
|
184
|
+
# 3.1: a non-String value for an array-form `type` that lists `string`
|
|
185
|
+
# with `format: binary`, such as an uploaded file object.
|
|
186
|
+
def binary_string_in_type_array?(value, schema)
|
|
187
|
+
return false if value.is_a?(String)
|
|
188
|
+
|
|
189
|
+
schema.format == 'binary' && schema.type.is_a?(Array) && schema.type.include?('string')
|
|
190
|
+
end
|
|
191
|
+
|
|
164
192
|
# 3.1: when coercing, a value whose class matches no listed type is tried
|
|
165
193
|
# against each listed type, the same way a single `type` coerces it.
|
|
166
194
|
# Request params arrive as Strings ("5" for `type: [integer, "null"]`),
|
|
@@ -171,7 +199,7 @@ class OpenAPIParser::SchemaValidator
|
|
|
171
199
|
return false unless @coerce_value && !value.nil?
|
|
172
200
|
return false if schema.any_of || schema.all_of || schema.one_of
|
|
173
201
|
|
|
174
|
-
schema.type.is_a?(Array) &&
|
|
202
|
+
schema.type.is_a?(Array) && pick_type(value, schema.type, schema).nil?
|
|
175
203
|
end
|
|
176
204
|
|
|
177
205
|
# 3.1: returns the first listed type that coerces, or the first type's error.
|
|
@@ -198,11 +226,11 @@ class OpenAPIParser::SchemaValidator
|
|
|
198
226
|
@type_mismatch_validator ||= OpenAPIParser::SchemaValidator::TypeMismatchValidator.new(self, @coerce_value)
|
|
199
227
|
end
|
|
200
228
|
|
|
201
|
-
def
|
|
229
|
+
def pick_type(value, types, schema)
|
|
202
230
|
types.find do |t|
|
|
203
231
|
case t
|
|
204
232
|
when 'string' then value.is_a?(String)
|
|
205
|
-
when 'integer' then
|
|
233
|
+
when 'integer' then integer_validator.integer?(value, schema)
|
|
206
234
|
when 'number' then value.is_a?(Numeric)
|
|
207
235
|
when 'boolean' then value == true || value == false
|
|
208
236
|
when 'array' then value.is_a?(Array)
|
|
@@ -28,5 +28,9 @@ module OpenAPIParser::Schemas
|
|
|
28
28
|
# @!attribute [r] path_items
|
|
29
29
|
# @return [Hash{String => PathItem}, nil] path item objects (OpenAPI 3.1+)
|
|
30
30
|
openapi_attr_hash_object :path_items, PathItem, reference: true, schema_key: :pathItems
|
|
31
|
+
|
|
32
|
+
# @!attribute [r] media_types
|
|
33
|
+
# @return [Hash{String => MediaType}, nil] media type objects (OpenAPI 3.2+)
|
|
34
|
+
openapi_attr_hash_object :media_types, MediaType, reference: true, schema_key: :mediaTypes
|
|
31
35
|
end
|
|
32
36
|
end
|
|
@@ -7,5 +7,9 @@ module OpenAPIParser::Schemas
|
|
|
7
7
|
# @!attribute [r] mapping
|
|
8
8
|
# @return [Hash{String => String]
|
|
9
9
|
openapi_attr_value :mapping
|
|
10
|
+
|
|
11
|
+
# @!attribute [r] default_mapping
|
|
12
|
+
# @return [String, nil] fallback schema name or reference (OpenAPI 3.2+)
|
|
13
|
+
openapi_attr_value :default_mapping, schema_key: :defaultMapping
|
|
10
14
|
end
|
|
11
15
|
end
|
|
@@ -8,6 +8,10 @@ module OpenAPIParser::Schemas
|
|
|
8
8
|
# @return [Schema, nil] OpenAPI3 Schema object
|
|
9
9
|
openapi_attr_object :schema, Schema, reference: true
|
|
10
10
|
|
|
11
|
+
# @!attribute [r] item_schema
|
|
12
|
+
# @return [Schema, nil] schema for each item of a sequential media type (OpenAPI 3.2+); not yet used for validation
|
|
13
|
+
openapi_attr_object :item_schema, Schema, reference: true, schema_key: :itemSchema
|
|
14
|
+
|
|
11
15
|
# validate params by schema definitions
|
|
12
16
|
# @param [Hash] params
|
|
13
17
|
# @param [OpenAPIParser::SchemaValidator::Options] options
|
|
@@ -5,11 +5,14 @@
|
|
|
5
5
|
|
|
6
6
|
module OpenAPIParser::Schemas
|
|
7
7
|
class OpenAPI < Base
|
|
8
|
-
|
|
8
|
+
# @param [OpenAPIParser::Schemas::OpenAPI, nil] referrer the document whose $ref loads this one
|
|
9
|
+
def initialize(raw_schema, config, uri: nil, schema_registry: {}, referrer: nil)
|
|
10
|
+
# set before super: child objects consult use_3_2_features? while they're built
|
|
11
|
+
@config = config
|
|
12
|
+
@referrer = referrer
|
|
9
13
|
super('#', nil, self, raw_schema)
|
|
10
14
|
@find_object_cache = {}
|
|
11
15
|
@path_item_finder = OpenAPIParser::PathItemFinder.new(paths) if paths # invalid definition
|
|
12
|
-
@config = config
|
|
13
16
|
@uri = uri
|
|
14
17
|
@schema_registry = schema_registry
|
|
15
18
|
|
|
@@ -33,6 +36,33 @@ module OpenAPIParser::Schemas
|
|
|
33
36
|
Gem::Version.new(openapi).release
|
|
34
37
|
end
|
|
35
38
|
|
|
39
|
+
# Whether OpenAPI 3.1 runtime behavior applies where 3.0 and 3.1 define
|
|
40
|
+
# the same thing differently: the document declares 3.1 or later. A
|
|
41
|
+
# referenced file that declares no version follows the document that
|
|
42
|
+
# loaded it.
|
|
43
|
+
# @return [Boolean]
|
|
44
|
+
def use_3_1_features?
|
|
45
|
+
version = openapi_version
|
|
46
|
+
return version >= Gem::Version.new('3.1') if version
|
|
47
|
+
return @referrer.use_3_1_features? if openapi.nil? && @referrer
|
|
48
|
+
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Whether OpenAPI 3.2 runtime behavior applies: the document declares 3.2
|
|
53
|
+
# or later, or the allow_3_2_features config is set. A referenced file
|
|
54
|
+
# that declares no version follows the document that loaded it.
|
|
55
|
+
# @return [Boolean]
|
|
56
|
+
def use_3_2_features?
|
|
57
|
+
return true if @config.allow_3_2_features
|
|
58
|
+
|
|
59
|
+
version = openapi_version
|
|
60
|
+
return version >= Gem::Version.new('3.2') if version
|
|
61
|
+
return @referrer.use_3_2_features? if openapi.nil? && @referrer
|
|
62
|
+
|
|
63
|
+
false
|
|
64
|
+
end
|
|
65
|
+
|
|
36
66
|
# @!attribute [r] paths
|
|
37
67
|
# @return [Paths, nil]
|
|
38
68
|
openapi_attr_object :paths, Paths, reference: false
|
|
@@ -59,6 +89,9 @@ module OpenAPIParser::Schemas
|
|
|
59
89
|
|
|
60
90
|
# @return [OpenAPIParser::RequestOperation, nil]
|
|
61
91
|
def request_operation(http_method, request_path)
|
|
92
|
+
# 3.1: `paths` is optional (a document may have only `webhooks`), so there is nothing to find
|
|
93
|
+
return nil unless @path_item_finder
|
|
94
|
+
|
|
62
95
|
OpenAPIParser::RequestOperation.create(http_method, request_path, @path_item_finder, @config)
|
|
63
96
|
end
|
|
64
97
|
|
|
@@ -71,7 +104,7 @@ module OpenAPIParser::Schemas
|
|
|
71
104
|
loaded = @schema_registry[resolved_uri]
|
|
72
105
|
return loaded if loaded
|
|
73
106
|
|
|
74
|
-
OpenAPIParser.load_uri(resolved_uri, config: @config, schema_registry: @schema_registry)
|
|
107
|
+
OpenAPIParser.load_uri(resolved_uri, config: @config, schema_registry: @schema_registry, referrer: self)
|
|
75
108
|
end
|
|
76
109
|
|
|
77
110
|
private
|