openapi_parser 2.6.0 → 2.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 265685ba8a1e648f9b4611356022227b5d80917f46d2450dae55747ffec40a23
4
- data.tar.gz: 9ba7d7d561d7ec84a5ce6a8c42cb13beffa09e1250699b206f6e4ec639350575
3
+ metadata.gz: 6332e2e24a629533ff6ae7159cff7b51db53a2e451bd527743610122ed3bb994
4
+ data.tar.gz: 88b477bdea9749ebe10b0bf7214529152cbed08e53290ea3657ca17e6d1970d6
5
5
  SHA512:
6
- metadata.gz: 4168f6e1dcedcd2c8be552937692215f8214511df0979f7bcc3d4ec00181980e5d397bc5cb0288cf9fc498eafc3a1322f0f1579bad89fb4915221b6b62a4f667
7
- data.tar.gz: 6fe2b4ae0877312fcdd0f6fc56643c7658e9e6325cffaa066caec1b263e6657c88db5a7233d6bd24ce7d8270ff8d5e1d7d8c7b38188e4ac237ca86cb5f374a4c
6
+ metadata.gz: 69fb1000151935a4636ba3bd98edf9f1e1e221ebea86e4a3cf23348b7452ecf49e2047e9be42322713f438ed56855a20cf52647f0cc08077efc95631839d8e49
7
+ data.tar.gz: 72287aa48edfe2dae9894006b2da9e90c14c5b552bb5cbc598bd166f931ab25e6283167c0b6afda3d7270761487edc83739f73686c3c9e739724049ac23f6d10
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  ## Unreleased
2
2
 
3
+ ## 2.7.0 (2026-11-06)
4
+ * match `additionalOperations` method names case-sensitively ([OpenAPI 3.2](https://spec.openapis.org/oas/v3.2.0.html#path-item-object), [RFC 9110 §9.1](https://www.rfc-editor.org/rfc/rfc9110.html#section-9.1)). Standard methods still match their fixed fields in any case
5
+ * check the keywords next to `allOf`, `anyOf` and `oneOf` (OpenAPI 3.0 and 3.1), such as `{ allOf: [...], required: [a] }`, and all of these keywords when a schema has more than one. They were ignored before, so a value that passed may now fail. In particular, `additionalProperties: false` next to `allOf` now rejects the properties defined only in `allOf`, as in JSON Schema
6
+ * return the coerced value from `allOf` and `oneOf` when `coerce_value` is on. They returned the original value before
7
+ * support the `dependentRequired` keyword (OpenAPI 3.1): an object that has a property but not the properties it depends on now fails with `NotExistDependentRequiredKey`. It was ignored before, so a value that passed may now fail
8
+ * add `SpecValidator` rule `DependentRequiredIn30`: detect `dependentRequired` in 3.0 documents (3.1 addition)
9
+
3
10
  ## 2.6.0 (2026-11-04)
4
11
  * 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
12
  * 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
data/README.md CHANGED
@@ -127,7 +127,7 @@ OpenAPIParser.parse(schema, strict_specification_version: :raise)
127
127
 
128
128
  Detected mismatches:
129
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
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`, `dependentRequired`, boolean schemas
131
131
  - the 3.0 Boolean vs 3.1 numeric forms of `exclusiveMinimum` / `exclusiveMaximum`
132
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
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
@@ -56,6 +56,18 @@ module OpenAPIParser
56
56
  end
57
57
  end
58
58
 
59
+ class NotExistDependentRequiredKey < OpenAPIError
60
+ def initialize(property, keys, reference)
61
+ super(reference)
62
+ @property = property
63
+ @keys = keys
64
+ end
65
+
66
+ def message
67
+ "#{@reference} missing parameters required by #{@property}: #{@keys.join(", ")}"
68
+ end
69
+ end
70
+
59
71
  class NotExistPropertyDefinition < OpenAPIError
60
72
  def initialize(keys, reference)
61
73
  super(reference)
@@ -10,17 +10,24 @@ class OpenAPIParser::SchemaValidator
10
10
  end
11
11
 
12
12
  # if any schema return error, it's not a valid all_of
13
+ # Members with `type` go first, so that the other members see the coerced value.
14
+ steps = schema.all_of.partition(&:type).flatten.map do |s|
15
+ lambda do |input|
16
+ # We need to store the reference to all of, so we can perform strict check on allowed properties
17
+ validatable.validate_schema(
18
+ input,
19
+ s,
20
+ :parent_all_of => true,
21
+ parent_discriminator_schemas: keyword_args[:parent_discriminator_schemas] || []
22
+ )
23
+ end
24
+ end
25
+ coerced, err = validatable.validate_in_order(value, steps)
26
+ return [nil, err] if err
27
+
13
28
  remaining_keys = value.kind_of?(Hash) ? value.keys : []
14
29
  nested_additional_properties = false
15
30
  schema.all_of.each do |s|
16
- # We need to store the reference to all of, so we can perform strict check on allowed properties
17
- _coerced, err = validatable.validate_schema(
18
- value,
19
- s,
20
- :parent_all_of => true,
21
- parent_discriminator_schemas: keyword_args[:parent_discriminator_schemas] || []
22
- )
23
-
24
31
  if s.type == "object"
25
32
  remaining_keys -= (s.properties || {}).keys
26
33
  nested_additional_properties = true if s.additional_properties
@@ -28,8 +35,6 @@ class OpenAPIParser::SchemaValidator
28
35
  # If this is not allOf having array of objects inside, but e.g. having another anyOf/oneOf nested
29
36
  remaining_keys.clear
30
37
  end
31
-
32
- return [nil, err] if err
33
38
  end
34
39
 
35
40
  # If there are nested additionalProperties, we allow not defined extra properties and lean on the specific
@@ -38,7 +43,7 @@ class OpenAPIParser::SchemaValidator
38
43
  return [nil, OpenAPIParser::NotExistPropertyDefinition.new(remaining_keys, schema.object_reference)]
39
44
  end
40
45
 
41
- [value, nil]
46
+ [coerced, nil]
42
47
  end
43
48
  end
44
49
  end
@@ -53,9 +53,30 @@ class OpenAPIParser::SchemaValidator
53
53
  end
54
54
  return [nil, OpenAPIParser::NotExistRequiredKey.new(required_set.to_a, schema.object_reference)] unless required_set.empty?
55
55
 
56
+ dependent_required_error = check_dependent_required(value, schema)
57
+ return [nil, dependent_required_error] if dependent_required_error
58
+
56
59
  value.merge!(coerced_values.to_h) if @coerce_value
57
60
 
58
61
  check_properties_number(value, schema)
59
62
  end
63
+
64
+ private
65
+
66
+ # @return [OpenAPIParser::NotExistDependentRequiredKey, nil]
67
+ def check_dependent_required(value, schema)
68
+ dependent_required = schema.dependent_required
69
+ return unless dependent_required.is_a?(Hash)
70
+
71
+ dependent_required.each do |property, keys|
72
+ next unless value.key?(property) && keys.is_a?(Array)
73
+
74
+ missing_keys = keys.reject { |key| value.key?(key) }
75
+ next if missing_keys.empty?
76
+
77
+ return OpenAPIParser::NotExistDependentRequiredKey.new(property, missing_keys, schema.object_reference)
78
+ end
79
+ nil
80
+ end
60
81
  end
61
82
  end
@@ -11,12 +11,9 @@ class OpenAPIParser::SchemaValidator
11
11
  end
12
12
 
13
13
  # if multiple schemas are satisfied, it's not valid
14
- result = schema.one_of.one? do |s|
15
- _coerced, err = validatable.validate_schema(value, s)
16
- err.nil?
17
- end
18
- if result
19
- [value, nil]
14
+ matched = schema.one_of.map { |s| validatable.validate_schema(value, s) }.select { |_coerced, err| err.nil? }
15
+ if matched.one?
16
+ [matched.first.first, nil]
20
17
  else
21
18
  [nil, OpenAPIParser::NotOneOf.new(value, schema.object_reference)]
22
19
  end
@@ -40,6 +40,7 @@ class OpenAPIParser::SchemaValidator
40
40
 
41
41
  # Types to pick from for a schema without `type`. integer comes before
42
42
  # number so that an Integer is checked as an integer.
43
+ JSON_VALUE_CLASSES = [Hash, Array, String, Numeric, TrueClass, FalseClass, NilClass].freeze
43
44
  VALUE_TYPES = %w[object array string integer number boolean].freeze
44
45
 
45
46
  class << self
@@ -100,18 +101,9 @@ class OpenAPIParser::SchemaValidator
100
101
  return [nil, OpenAPIParser::NotSchemaMatched.new(value, schema.not.object_reference)] unless not_error
101
102
  end
102
103
 
103
- return coerce_into_type_array(value, schema, **keyword_args) if coerce_into_type_array?(value, schema)
104
+ return validate_composition(value, schema, **keyword_args) if composition_validators(schema).any?
104
105
 
105
- if (v = validator(value, schema))
106
- if keyword_args.empty?
107
- return v.coerce_and_validate(value, schema)
108
- else
109
- return v.coerce_and_validate(value, schema, **keyword_args)
110
- end
111
- end
112
-
113
- # unknown return error
114
- OpenAPIParser::ValidateError.build_error_result(value, schema)
106
+ validate_without_composition(value, schema, **keyword_args)
115
107
  end
116
108
 
117
109
  # validate integer value by schema
@@ -122,13 +114,71 @@ class OpenAPIParser::SchemaValidator
122
114
  integer_validator.coerce_and_validate(value, schema)
123
115
  end
124
116
 
117
+ # Run the steps in order, each with the value coerced by the steps before it.
118
+ # Keywords check JSON values. A coerced value of another class, like DateTime,
119
+ # is only the output form, so the later steps check the original value.
120
+ # @param [Object] value
121
+ # @param [Array<Proc>] steps each takes a value and returns coerced value and error
122
+ # @return [Array] coerced value and error
123
+ def validate_in_order(value, steps)
124
+ current = value
125
+ steps.each do |step|
126
+ returned, err = step.call(json_value?(current) ? current : value)
127
+ return [nil, err] if err
128
+
129
+ current = returned if json_value?(current)
130
+ end
131
+ [current, nil]
132
+ end
133
+
125
134
  private
126
135
 
136
+ def json_value?(value)
137
+ JSON_VALUE_CLASSES.any? { |klass| value.is_a?(klass) }
138
+ end
139
+
140
+ # @return [Array<OpenAPIParser::SchemaValidator::Base>]
141
+ def composition_validators(schema)
142
+ validators = []
143
+ validators << any_of_validator if schema.any_of
144
+ validators << all_of_validator if schema.all_of
145
+ validators << one_of_validator if schema.one_of
146
+ validators
147
+ end
148
+
149
+ # All the keywords in a schema apply to the value, so the composition
150
+ # keywords and the keywords next to them are all checked.
151
+ # `type` goes first, so that the other steps see the coerced value.
152
+ # @return [Array] coerced value and error
153
+ def validate_composition(value, schema, **keyword_args)
154
+ composition_steps = composition_validators(schema).map do |v|
155
+ ->(input) { v.coerce_and_validate(input, schema, **keyword_args) }
156
+ end
157
+ # the composition keyword already used discriminator
158
+ parents = (keyword_args[:parent_discriminator_schemas] || []) + [schema]
159
+ other_step = ->(input) { validate_without_composition(input, schema, **keyword_args, parent_discriminator_schemas: parents) }
160
+
161
+ validate_in_order(value, schema.type ? [other_step, *composition_steps] : [*composition_steps, other_step])
162
+ end
163
+
164
+ # @return [Array] coerced value and error
165
+ def validate_without_composition(value, schema, **keyword_args)
166
+ return coerce_into_type_array(value, schema, **keyword_args) if coerce_into_type_array?(value, schema)
167
+
168
+ if (v = validator(value, schema))
169
+ if keyword_args.empty?
170
+ return v.coerce_and_validate(value, schema)
171
+ else
172
+ return v.coerce_and_validate(value, schema, **keyword_args)
173
+ end
174
+ end
175
+
176
+ # unknown return error
177
+ OpenAPIParser::ValidateError.build_error_result(value, schema)
178
+ end
179
+
127
180
  # @return [OpenAPIParser::SchemaValidator::Base, nil]
128
181
  def validator(value, schema)
129
- return any_of_validator if schema.any_of
130
- return all_of_validator if schema.all_of
131
- return one_of_validator if schema.one_of
132
182
  return nil_validator if value.nil?
133
183
 
134
184
  # 3.1: `format: binary` with `string` in an array-form `type` accepts a
@@ -197,7 +247,6 @@ class OpenAPIParser::SchemaValidator
197
247
  # @return [Boolean]
198
248
  def coerce_into_type_array?(value, schema)
199
249
  return false unless @coerce_value && !value.nil?
200
- return false if schema.any_of || schema.all_of || schema.one_of
201
250
 
202
251
  schema.type.is_a?(Array) && pick_type(value, schema.type, schema).nil?
203
252
  end
@@ -36,13 +36,10 @@ module OpenAPIParser::Schemas
36
36
 
37
37
  private
38
38
 
39
- # additionalOperations keys are method names as sent on the wire
40
- # (conventionally uppercase); prefer an exact match, else ignore case
39
+ # additionalOperations keys use the method's case as sent on the wire,
40
+ # and method names are case-sensitive (RFC 9110 §9.1)
41
41
  def additional_operation(name)
42
- operations = additional_operations
43
- return nil unless operations
44
-
45
- operations.fetch(name) { operations.find { |key, _| key.casecmp?(name) }&.last }
42
+ additional_operations&.[](name)
46
43
  end
47
44
  end
48
45
  end
@@ -83,6 +83,10 @@ module OpenAPIParser::Schemas
83
83
  # @return [Boolean, nil]
84
84
  openapi_attr_value :write_only, schema_key: :writeOnly
85
85
 
86
+ # @!attribute [r] dependent_required
87
+ # @return [Hash{String => Array<String>}, nil]
88
+ openapi_attr_value :dependent_required, schema_key: :dependentRequired
89
+
86
90
  # @!attribute [r] all_of
87
91
  # @return [Array<Schema, Reference>, nil]
88
92
  openapi_attr_list_object :all_of, Schema, reference: true, schema_key: :allOf
@@ -0,0 +1,25 @@
1
+ module OpenAPIParser
2
+ class SpecValidator
3
+ module Rules
4
+ # `dependentRequired` is a JSON Schema 2020-12 keyword that 3.1 adopted.
5
+ # 3.0 Schema Objects do not define it. The validator checks it regardless
6
+ # of version; this rule reports the version mismatch.
7
+ class DependentRequiredIn30 < 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?('dependentRequired')
14
+
15
+ violations << violation(
16
+ path: schema.object_reference,
17
+ message: '`dependentRequired` is a 3.1 addition (from JSON Schema 2020-12); 3.0 Schema Objects do not define it',
18
+ )
19
+ end
20
+ violations
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -18,6 +18,7 @@ require_relative 'spec_validator/rules/dynamic_ref_in_30'
18
18
  require_relative 'spec_validator/rules/dynamic_anchor_in_30'
19
19
  require_relative 'spec_validator/rules/content_schema_in_30'
20
20
  require_relative 'spec_validator/rules/defs_in_30'
21
+ require_relative 'spec_validator/rules/dependent_required_in_30'
21
22
  require_relative 'spec_validator/rules/allow_empty_value_deprecation'
22
23
  require_relative 'spec_validator/rules/xml_node_type_before_32'
23
24
  require_relative 'spec_validator/rules/xml_attribute_deprecation'
@@ -100,6 +101,7 @@ module OpenAPIParser
100
101
  Rules::DynamicAnchorIn30,
101
102
  Rules::ContentSchemaIn30,
102
103
  Rules::DefsIn30,
104
+ Rules::DependentRequiredIn30,
103
105
  Rules::AllowEmptyValueDeprecation,
104
106
  Rules::XmlNodeTypeBefore32,
105
107
  Rules::XmlAttributeDeprecation,
@@ -1,3 +1,3 @@
1
1
  module OpenAPIParser
2
- VERSION = '2.6.0'.freeze
2
+ VERSION = '2.7.0'.freeze
3
3
  end
@@ -25,6 +25,7 @@ module OpenAPIParser
25
25
  def validate_data: -> Object
26
26
  def validate_schema: (Object value, OpenAPIParser::Schemas::Schema schema, **bot) -> [Object, OpenAPIParser::validate_error]
27
27
  def validate_integer: (Object value, OpenAPIParser::Schemas::Schema schema) -> [Object, OpenAPIParser::validate_error]
28
+ def validate_in_order: (Object value, Array[untyped] steps) -> [Object, OpenAPIParser::validate_error]
28
29
 
29
30
  private
30
31
  def validator: (Object value, OpenAPIParser::Schemas::Schema schema) -> [OpenAPIParser::SchemaValidator::Base, OpenAPIParser::validate_error]
@@ -114,6 +114,10 @@ module OpenAPIParser
114
114
  def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
115
115
  end
116
116
 
117
+ class DependentRequiredIn30 < Rule
118
+ def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
119
+ end
120
+
117
121
  class AllowEmptyValueDeprecation < Rule
118
122
  def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
119
123
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openapi_parser
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.6.0
4
+ version: 2.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ota42y
@@ -221,6 +221,7 @@ files:
221
221
  - lib/openapi_parser/spec_validator/rules/cookie_style_before_32.rb
222
222
  - lib/openapi_parser/spec_validator/rules/default_mapping_before_32.rb
223
223
  - lib/openapi_parser/spec_validator/rules/defs_in_30.rb
224
+ - lib/openapi_parser/spec_validator/rules/dependent_required_in_30.rb
224
225
  - lib/openapi_parser/spec_validator/rules/dynamic_anchor_in_30.rb
225
226
  - lib/openapi_parser/spec_validator/rules/dynamic_ref_in_30.rb
226
227
  - lib/openapi_parser/spec_validator/rules/example_singular_deprecation.rb