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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yaml +1 -3
  3. data/CHANGELOG.md +34 -0
  4. data/README.md +44 -1
  5. data/lib/openapi_parser/concerns/media_type_selectable.rb +18 -0
  6. data/lib/openapi_parser/concerns/schema_loader/creator.rb +7 -2
  7. data/lib/openapi_parser/concerns/schema_loader/hash_body_loader.rb +1 -1
  8. data/lib/openapi_parser/config.rb +5 -0
  9. data/lib/openapi_parser/errors.rb +24 -0
  10. data/lib/openapi_parser/schema_validator/any_of_validator.rb +1 -1
  11. data/lib/openapi_parser/schema_validator/base.rb +35 -9
  12. data/lib/openapi_parser/schema_validator/float_validator.rb +4 -0
  13. data/lib/openapi_parser/schema_validator/integer_validator.rb +17 -1
  14. data/lib/openapi_parser/schema_validator/multiple_of.rb +26 -0
  15. data/lib/openapi_parser/schema_validator/nil_validator.rb +4 -0
  16. data/lib/openapi_parser/schema_validator/one_of_validator.rb +1 -1
  17. data/lib/openapi_parser/schema_validator.rb +32 -4
  18. data/lib/openapi_parser/schemas/components.rb +4 -0
  19. data/lib/openapi_parser/schemas/discriminator.rb +4 -0
  20. data/lib/openapi_parser/schemas/media_type.rb +4 -0
  21. data/lib/openapi_parser/schemas/openapi.rb +36 -3
  22. data/lib/openapi_parser/schemas/path_item.rb +32 -6
  23. data/lib/openapi_parser/schemas/request_body.rb +1 -1
  24. data/lib/openapi_parser/schemas/response.rb +5 -1
  25. data/lib/openapi_parser/schemas/schema.rb +8 -1
  26. data/lib/openapi_parser/spec_validator/rule.rb +24 -3
  27. data/lib/openapi_parser/spec_validator/rules/additional_operations_before_32.rb +26 -0
  28. data/lib/openapi_parser/spec_validator/rules/allow_empty_value_deprecation.rb +26 -0
  29. data/lib/openapi_parser/spec_validator/rules/cookie_style_before_32.rb +26 -0
  30. data/lib/openapi_parser/spec_validator/rules/default_mapping_before_32.rb +29 -0
  31. data/lib/openapi_parser/spec_validator/rules/defs_in_30.rb +25 -0
  32. data/lib/openapi_parser/spec_validator/rules/example_value_fields_before_32.rb +48 -0
  33. data/lib/openapi_parser/spec_validator/rules/media_types_before_32.rb +24 -0
  34. data/lib/openapi_parser/spec_validator/rules/query_method_before_32.rb +26 -0
  35. data/lib/openapi_parser/spec_validator/rules/querystring_before_32.rb +26 -0
  36. data/lib/openapi_parser/spec_validator/rules/response_summary_before_32.rb +25 -0
  37. data/lib/openapi_parser/spec_validator/rules/security_scheme_fields_before_32.rb +41 -0
  38. data/lib/openapi_parser/spec_validator/rules/server_name_before_32.rb +35 -0
  39. data/lib/openapi_parser/spec_validator/rules/streaming_fields_before_32.rb +56 -0
  40. data/lib/openapi_parser/spec_validator/rules/tag_fields_before_32.rb +34 -0
  41. data/lib/openapi_parser/spec_validator/rules/xml_attribute_deprecation.rb +29 -0
  42. data/lib/openapi_parser/spec_validator/rules/xml_node_type_before_32.rb +29 -0
  43. data/lib/openapi_parser/spec_validator/rules/xml_wrapped_deprecation.rb +29 -0
  44. data/lib/openapi_parser/spec_validator.rb +34 -0
  45. data/lib/openapi_parser/version.rb +1 -1
  46. data/lib/openapi_parser.rb +5 -4
  47. data/sig/openapi_parser/config.rbs +1 -0
  48. data/sig/openapi_parser/schema_validator.rbs +1 -0
  49. data/sig/openapi_parser/schema_validators/base.rbs +4 -2
  50. data/sig/openapi_parser/spec_validator.rbs +77 -0
  51. data/sig/openapi_parser.rbs +5 -3
  52. data/sig/wip_types.rbs +6 -0
  53. metadata +19 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e570c33d05a24e9b11a3f5b86f5321f895088a672d404df68f3d14b4d3f9fb8f
4
- data.tar.gz: fadcedd0e7b7c5ce057f2b0fa36f313218f151ae524dd2290c1242fbdfca02da
3
+ metadata.gz: 265685ba8a1e648f9b4611356022227b5d80917f46d2450dae55747ffec40a23
4
+ data.tar.gz: 9ba7d7d561d7ec84a5ce6a8c42cb13beffa09e1250699b206f6e4ec639350575
5
5
  SHA512:
6
- metadata.gz: 680325ae11c7dce38425d23c89918d3ce5cb768261dbd48f6d21c166ca0d89b2dbf9304297b2f754830f47cdf8d45dd2501371b396fe972957474e171f4b3b49
7
- data.tar.gz: 27b757f70c102671ff1f2891918f5e7abb044796e280027b097214f4f0bbb2ff538b7f7c84d7d1bb7f1fc8b46b63591ad62e37985dab50ebb35c0603b854e92e
6
+ metadata.gz: 4168f6e1dcedcd2c8be552937692215f8214511df0979f7bcc3d4ec00181980e5d397bc5cb0288cf9fc498eafc3a1322f0f1579bad89fb4915221b6b62a4f667
7
+ data.tar.gz: 6fe2b4ae0877312fcdd0f6fc56643c7658e9e6325cffaa066caec1b263e6657c88db5a7233d6bd24ce7d8270ff8d5e1d7d8c7b38188e4ac237ca86cb5f374a4c
@@ -11,13 +11,11 @@ jobs:
11
11
  - ubuntu-latest
12
12
  - macos-latest
13
13
  ruby:
14
- - "2.7"
15
- - "3.0"
16
- - "3.1"
17
14
  - "3.2"
18
15
  - "3.3"
19
16
  - "3.4"
20
17
  - "4.0"
18
+ - "4.1"
21
19
  - ruby-head
22
20
  runs-on: ${{ matrix.os }}
23
21
  steps:
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
  [![Yard Docs](https://img.shields.io/badge/yard-docs-blue.svg)](https://www.rubydoc.info/gems/openapi_parser)
5
5
  [![Inch CI](https://inch-ci.org/github/ota42y/openapi_parser.svg?branch=master)](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 @allow_reference && check_reference_schema?(schema)
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, escape_reference(child_name))
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.nil? || !value.key?(property_name)
18
- return [nil, OpenAPIParser::NotExistDiscriminatorPropertyName.new(discriminator.property_name, value, discriminator.object_reference)]
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
- # it's allowed to have discriminator without mapping, then we need to lookup discriminator.property_name
23
- # but the format is not the full path, just model name in the components
24
- mapping_target = discriminator.mapping&.[](mapping_key) || "#/components/schemas/#{mapping_key}"
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
- # Find object does O(n) search at worst, then caches the result, so this is ok for repeated search
27
- resolved_schema = discriminator.root.find_object(mapping_target)
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 value.kind_of?(Integer)
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 = pick_array_type(value, effective_type)
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) && pick_array_type(value, schema.type).nil?
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 pick_array_type(value, types)
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 value.is_a?(Integer)
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
- def initialize(raw_schema, config, uri: nil, schema_registry: {})
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