openapi_parser 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yaml +4 -2
  3. data/.ruby-version +1 -1
  4. data/CHANGELOG.md +34 -0
  5. data/README.md +1 -1
  6. data/lib/openapi_parser/config.rb +17 -0
  7. data/lib/openapi_parser/schema_validator/array_validator.rb +7 -3
  8. data/lib/openapi_parser/schema_validator/minimum_maximum.rb +17 -4
  9. data/lib/openapi_parser/schema_validator/nil_validator.rb +4 -0
  10. data/lib/openapi_parser/schema_validator/options.rb +5 -2
  11. data/lib/openapi_parser/schema_validator/string_validator.rb +12 -9
  12. data/lib/openapi_parser/schema_validator/type_mismatch_validator.rb +11 -0
  13. data/lib/openapi_parser/schema_validator.rb +84 -2
  14. data/lib/openapi_parser/schemas/components.rb +4 -0
  15. data/lib/openapi_parser/schemas/openapi.rb +20 -0
  16. data/lib/openapi_parser/schemas/schema.rb +12 -1
  17. data/lib/openapi_parser/spec_validator/rule.rb +68 -0
  18. data/lib/openapi_parser/spec_validator/rules/const_in_30.rb +25 -0
  19. data/lib/openapi_parser/spec_validator/rules/content_encoding_in_30.rb +24 -0
  20. data/lib/openapi_parser/spec_validator/rules/content_media_type_in_30.rb +25 -0
  21. data/lib/openapi_parser/spec_validator/rules/content_schema_in_30.rb +25 -0
  22. data/lib/openapi_parser/spec_validator/rules/dynamic_anchor_in_30.rb +25 -0
  23. data/lib/openapi_parser/spec_validator/rules/dynamic_ref_in_30.rb +25 -0
  24. data/lib/openapi_parser/spec_validator/rules/example_singular_deprecation.rb +25 -0
  25. data/lib/openapi_parser/spec_validator/rules/exclusive_maximum.rb +32 -0
  26. data/lib/openapi_parser/spec_validator/rules/exclusive_minimum.rb +32 -0
  27. data/lib/openapi_parser/spec_validator/rules/json_schema_dialect_in_30.rb +19 -0
  28. data/lib/openapi_parser/spec_validator/rules/nullable_deprecation.rb +25 -0
  29. data/lib/openapi_parser/spec_validator/rules/path_items_in_30.rb +25 -0
  30. data/lib/openapi_parser/spec_validator/rules/prefix_items_in_30.rb +25 -0
  31. data/lib/openapi_parser/spec_validator/rules/type_array_in_30.rb +25 -0
  32. data/lib/openapi_parser/spec_validator/rules/type_null_in_30.rb +26 -0
  33. data/lib/openapi_parser/spec_validator/rules/webhooks_in_30.rb +18 -0
  34. data/lib/openapi_parser/spec_validator/spec_violation.rb +11 -0
  35. data/lib/openapi_parser/spec_validator.rb +85 -0
  36. data/lib/openapi_parser/version.rb +1 -1
  37. data/lib/openapi_parser.rb +3 -0
  38. data/sig/openapi_parser/config.rbs +3 -0
  39. data/sig/openapi_parser/schema_validator.rbs +4 -0
  40. data/sig/openapi_parser/schema_validators/options.rbs +1 -1
  41. data/sig/openapi_parser/spec_validator.rbs +109 -0
  42. data/sig/openapi_parser.rbs +2 -0
  43. metadata +23 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c7f08706ff7f3eeb957f73f1352879e4234c0c2cb5a2906a12ace5e75c0517d3
4
- data.tar.gz: 9267f25fcfa1cb600b0373980a011086b2b70ceefba042658b4a20d80c57b851
3
+ metadata.gz: d03c17295ee85e0cf17e06e83f6e28c1bed8028e7de1ae68574e2d69ad6160b2
4
+ data.tar.gz: 6cae65df5bab290cff5d7c014d181f5eb631e153863f5eac82c7ce460d224dd9
5
5
  SHA512:
6
- metadata.gz: 1a9213679b51be326c333e040f3de339d6aec2e1c74de0efa685f685da8846ca1b09926829177c62c4ebdc5a877b528919ad2a9a293179fe20d45cfd5f01e3c4
7
- data.tar.gz: f9e3d895124ccfeb70fabc1a0ee22395fc38ff2c1447b6529d996adb5cedaa87b924028db4b07ee1e1aeabaab799906dfc488126e3cb8ac4439152ac34f4ff8d
6
+ metadata.gz: 3074ba856acef7b064c0b1d2cf7b79b274e00766d90797ddfe95626f19ea09571fe72e2346e397bc7cac18f986959c659b62de2933b42d0a403074d5e41a73a9
7
+ data.tar.gz: 7c373bb6e33c6bf221b7d3dbc41f89951109b8adc7bc9aef362eb5c820733e5c1203279c272e92e96b1cd4a6278b09f70aab5ffee809cf635e9253d6e616bb06
@@ -1,5 +1,7 @@
1
1
  name: ci
2
- on: [push]
2
+ on:
3
+ - pull_request
4
+ - push
3
5
  jobs:
4
6
  test:
5
7
  strategy:
@@ -18,7 +20,7 @@ jobs:
18
20
  - ruby-head
19
21
  runs-on: ${{ matrix.os }}
20
22
  steps:
21
- - uses: actions/checkout@v5
23
+ - uses: actions/checkout@v7
22
24
  - uses: ruby/setup-ruby@v1
23
25
  with:
24
26
  ruby-version: ${{ matrix.ruby }}
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.1.0
1
+ 3.3.12
data/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  ## Unreleased
2
2
 
3
+ ## 2.4.0 (2026-10-14)
4
+ * support `components.pathItems` so `$ref`s into it resolve, unblocking OpenAPI 3.1 documents that use reusable path items
5
+ * support array-form `type` (3.1) in value validation
6
+ * support root-level `jsonSchemaDialect` (OpenAPI 3.1) in the parse layer
7
+ * support `prefixItems` (OpenAPI 3.1) with positional tuple validation
8
+ * support `contentMediaType` (OpenAPI 3.1) in the parse layer
9
+ * support `contentEncoding` (OpenAPI 3.1) in the parse layer
10
+ * add `SpecValidator` with `strict_specification_version` config (`:silent` / `:warn` / `:raise`) to detect version mismatches between declared OpenAPI version and actual field usage
11
+ * `ContentEncodingIn30`: detect `contentEncoding` usage in 3.0 documents (3.1 addition)
12
+ * `ContentMediaTypeIn30`: detect `contentMediaType` usage in 3.0 documents (3.1 addition)
13
+ * `PrefixItemsIn30`: detect `prefixItems` usage in 3.0 documents (3.1 addition)
14
+ * `JsonSchemaDialectIn30`: detect root-level `jsonSchemaDialect` usage in 3.0 documents (3.1 addition)
15
+ * `TypeArrayIn30`: detect array-form `type` usage in 3.0 documents (3.1 form)
16
+ * `NullableDeprecation`: detect `nullable` usage in 3.1 documents (removed in 3.1)
17
+ * `ExampleSingularDeprecation`: detect singular `example` on schemas in 3.1 documents (deprecated in 3.1)
18
+ * `PathItemsIn30`: detect `components.pathItems` usage in 3.0 documents (3.1 addition)
19
+ * `ExclusiveMinimum` / `ExclusiveMaximum`: detect 3.0 Boolean vs 3.1 numeric form mismatch
20
+ * `TypeNullIn30`: detect `type: "null"` usage in 3.0 documents (3.1 primitive)
21
+ * `WebhooksIn30`: detect root-level `webhooks` usage in 3.0 documents (3.1 addition)
22
+ * `ConstIn30`: detect `const` usage in 3.0 documents (3.1 addition)
23
+ * `DynamicRefIn30`: detect `$dynamicRef` usage in 3.0 documents (3.1 addition)
24
+ * `DynamicAnchorIn30`: detect `$dynamicAnchor` usage in 3.0 documents (3.1 addition)
25
+ * `ContentSchemaIn30`: detect `contentSchema` usage in 3.0 documents (3.1 addition)
26
+ * expose the declared version as `OpenAPI#openapi_version` (a `Gem::Version`, or nil when the field is missing or malformed) so `SpecValidator` rules compare version ranges; a 3.2 document is checked by the 3.1-or-later rules
27
+ * support 3.1-style numeric `exclusiveMinimum` / `exclusiveMaximum` in value validation (standalone bound, not a Boolean modifier on `minimum` / `maximum`)
28
+ * support `type: "null"` (3.1 primitive) in value validation
29
+ * support root-level `webhooks` (OpenAPI 3.1) in the parse layer
30
+ * support `const` (OpenAPI 3.1) with exact-equality value validation
31
+ * support `contentSchema` (OpenAPI 3.1) in the parse layer
32
+ * coerce String values into array-form `type` (3.1) when `coerce_value` is on, so params like `type: [integer, "null"]` accept `"5"`
33
+
34
+ ## 2.3.1 (2025-11-14)
35
+ * add optional date coercion with behavior matching existing datetime coercion
36
+
3
37
  ## 2.3.0 (2025-10-01)
4
38
  * bump base64 dep
5
39
  * bump actions checkout dep
data/README.md CHANGED
@@ -27,7 +27,7 @@ request_operation.path_params
27
27
  # => {"template_name"=>"1"}
28
28
 
29
29
  # coerce parameter
30
- root = OpenAPIParser.parse(YAML.load_file('open_api_3/schema.yml'), {coerce_value: true, datetime_coerce_class: DateTime})
30
+ root = OpenAPIParser.parse(YAML.load_file('open_api_3/schema.yml'), {coerce_value: true, datetime_coerce_class: DateTime, date_coerce_class: Date})
31
31
  request_operation = root.request_operation(:get, '/string_params_coercer')
32
32
  request_operation.validate_request_parameter({'integer_1' => '1', 'datetime_string' => '2016-04-01T16:00:00+09:00'})
33
33
  # => {"integer_1"=>1, "datetime_string"=>#<DateTime: 2016-04-01T16:00:00+09:00 ((2457480j,25200s,0n),+32400s,2299161j)>
@@ -19,6 +19,10 @@ class OpenAPIParser::Config
19
19
  @config[:datetime_coerce_class]
20
20
  end
21
21
 
22
+ def date_coerce_class
23
+ @config[:date_coerce_class]
24
+ end
25
+
22
26
  def coerce_value
23
27
  @config[:coerce_value]
24
28
  end
@@ -37,6 +41,18 @@ class OpenAPIParser::Config
37
41
  @config.fetch(:strict_reference_validation, false)
38
42
  end
39
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
+
40
56
  def validate_header
41
57
  @config.fetch(:validate_header, true)
42
58
  end
@@ -46,6 +62,7 @@ class OpenAPIParser::Config
46
62
  @request_validator_options ||= OpenAPIParser::SchemaValidator::Options.new(allow_empty_date_and_datetime: allow_empty_date_and_datetime,
47
63
  coerce_value: coerce_value,
48
64
  datetime_coerce_class: datetime_coerce_class,
65
+ date_coerce_class: date_coerce_class,
49
66
  validate_header: validate_header)
50
67
  end
51
68
 
@@ -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
- # array type have an schema in items property
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
- coerced, err = validatable.validate_schema(v, items_schema)
19
+ coerced_values = value.each_with_index.map do |v, idx|
20
+ sub_schema = prefix_schemas[idx] || items_schema
21
+ coerced, err = validatable.validate_schema(v, sub_schema)
18
22
  return [nil, err] if err
19
23
 
20
24
  coerced
@@ -4,8 +4,10 @@ class OpenAPIParser::SchemaValidator
4
4
  # @param [Object] value
5
5
  # @param [OpenAPIParser::Schemas::Schema] schema
6
6
  def check_minimum_maximum(value, schema)
7
- include_min_max = schema.minimum || schema.maximum
8
- return [value, nil] unless include_min_max
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
- if schema.exclusiveMinimum && value <= schema.minimum
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
@@ -6,14 +6,17 @@ class OpenAPIParser::SchemaValidator
6
6
  # @return [Boolean] coerce value option on/off
7
7
  # @!attribute [r] datetime_coerce_class
8
8
  # @return [Object, nil] coerce datetime string by this Object class
9
+ # @!attribute [r] date_coerce_class
10
+ # @return [Object, nil] coerce date string by this Object class
9
11
  # @!attribute [r] validate_header
10
12
  # @return [Boolean] validate header or not
11
- attr_reader :allow_empty_date_and_datetime, :coerce_value, :datetime_coerce_class, :validate_header
13
+ attr_reader :allow_empty_date_and_datetime, :coerce_value, :datetime_coerce_class, :validate_header, :date_coerce_class
12
14
 
13
- def initialize(allow_empty_date_and_datetime: false, coerce_value: nil, datetime_coerce_class: nil, validate_header: true)
15
+ def initialize(allow_empty_date_and_datetime: false, coerce_value: nil, datetime_coerce_class: nil, validate_header: true, date_coerce_class: nil)
14
16
  @allow_empty_date_and_datetime = allow_empty_date_and_datetime
15
17
  @coerce_value = coerce_value
16
18
  @datetime_coerce_class = datetime_coerce_class
19
+ @date_coerce_class = date_coerce_class
17
20
  @validate_header = validate_header
18
21
  end
19
22
  end
@@ -2,10 +2,11 @@ class OpenAPIParser::SchemaValidator
2
2
  class StringValidator < Base
3
3
  include ::OpenAPIParser::SchemaValidator::Enumable
4
4
 
5
- def initialize(validator, allow_empty_date_and_datetime, coerce_value, datetime_coerce_class)
5
+ def initialize(validator, allow_empty_date_and_datetime, coerce_value, datetime_coerce_class, date_coerce_class)
6
6
  super(validator, coerce_value)
7
7
  @allow_empty_date_and_datetime = allow_empty_date_and_datetime
8
8
  @datetime_coerce_class = datetime_coerce_class
9
+ @date_coerce_class = date_coerce_class
9
10
  end
10
11
 
11
12
  def coerce_and_validate(value, schema, **_keyword_args)
@@ -90,16 +91,18 @@ class OpenAPIParser::SchemaValidator
90
91
  return [nil, OpenAPIParser::InvalidDateFormat.new(value, schema.object_reference)] unless value =~ /^\d{4}-\d{2}-\d{2}$/
91
92
 
92
93
  begin
93
- parsed_date = Date.iso8601(value)
94
+ if @date_coerce_class.nil?
95
+ # validate only
96
+ Date.iso8601(value)
97
+ [value, nil]
98
+ else
99
+ # validate and coerce
100
+ [@date_coerce_class.iso8601(value), nil]
101
+ end
94
102
  rescue ArgumentError
95
- return [nil, OpenAPIParser::InvalidDateFormat.new(value, schema.object_reference)]
96
- end
97
-
98
- unless parsed_date.strftime('%Y-%m-%d') == value
99
- return [nil, OpenAPIParser::InvalidDateFormat.new(value, schema.object_reference)]
103
+ # when rfc3339(value) failed
104
+ [nil, OpenAPIParser::InvalidDateFormat.new(value, schema.object_reference)]
100
105
  end
101
-
102
- return [value, nil]
103
106
  end
104
107
 
105
108
  def validate_datetime_format(value, schema)
@@ -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
@@ -56,6 +57,7 @@ class OpenAPIParser::SchemaValidator
56
57
  @allow_empty_date_and_datetime = options.allow_empty_date_and_datetime
57
58
  @coerce_value = options.coerce_value
58
59
  @datetime_coerce_class = options.datetime_coerce_class
60
+ @date_coerce_class = options.date_coerce_class
59
61
  end
60
62
 
61
63
  # execute validate data
@@ -73,6 +75,15 @@ class OpenAPIParser::SchemaValidator
73
75
  def validate_schema(value, schema, **keyword_args)
74
76
  return [value, nil] unless schema
75
77
 
78
+ # 3.1: `const` pins the value to exactly that constant. Checked before
79
+ # type dispatch so it applies uniformly across primitives. Detection
80
+ # uses raw_schema so an intentional `const: null` is honored.
81
+ if schema.respond_to?(:raw_schema) && schema.raw_schema.is_a?(Hash) && schema.raw_schema.key?('const')
82
+ return [nil, OpenAPIParser::ValidateError.new(value, "const #{schema.const.inspect}", schema.object_reference)] if value != schema.const
83
+ end
84
+
85
+ return coerce_into_type_array(value, schema, **keyword_args) if coerce_into_type_array?(value, schema)
86
+
76
87
  if (v = validator(value, schema))
77
88
  if keyword_args.empty?
78
89
  return v.coerce_and_validate(value, schema)
@@ -102,7 +113,25 @@ class OpenAPIParser::SchemaValidator
102
113
  return one_of_validator if schema.one_of
103
114
  return nil_validator if value.nil?
104
115
 
105
- case schema.type
116
+ # 3.1: pick the type from an Array whose primitive class matches the
117
+ # value, then dispatch as if it were a single-type schema. If nothing
118
+ # matches we still pick a candidate so the relevant validator emits
119
+ # a sensible error rather than silently accepting via the unspecified
120
+ # fallback.
121
+ effective_type = schema.type
122
+ if effective_type.is_a?(Array)
123
+ matched = pick_array_type(value, effective_type)
124
+ return type_mismatch_validator if matched.nil?
125
+
126
+ effective_type = matched
127
+ end
128
+
129
+ validator_for_type(effective_type)
130
+ end
131
+
132
+ # @return [OpenAPIParser::SchemaValidator::Base]
133
+ def validator_for_type(type)
134
+ case type
106
135
  when 'string'
107
136
  string_validator
108
137
  when 'integer'
@@ -115,13 +144,66 @@ class OpenAPIParser::SchemaValidator
115
144
  object_validator
116
145
  when 'array'
117
146
  array_validator
147
+ when 'null'
148
+ # 3.1: only nil values are valid here. nil is handled earlier in
149
+ # this method, so a non-nil value reaching this branch is a type
150
+ # mismatch that should fail validation.
151
+ type_mismatch_validator
118
152
  else
119
153
  unspecified_type_validator
120
154
  end
121
155
  end
122
156
 
157
+ # 3.1: request params arrive as Strings, so `type: [integer, "null"]`
158
+ # never matches by class. When coercing, try each listed type instead.
159
+ # @return [Boolean]
160
+ def coerce_into_type_array?(value, schema)
161
+ return false unless @coerce_value && value.is_a?(String)
162
+ return false if schema.any_of || schema.all_of || schema.one_of
163
+
164
+ schema.type.is_a?(Array) && pick_array_type(value, schema.type).nil?
165
+ end
166
+
167
+ # 3.1: returns the first listed type that coerces, or the first type's error.
168
+ # @return [Array] coerced value and error
169
+ def coerce_into_type_array(value, schema, **keyword_args)
170
+ first_error = nil
171
+ (schema.type - ['null']).each do |type|
172
+ v = validator_for_type(type)
173
+ coerced, err = if keyword_args.empty?
174
+ v.coerce_and_validate(value, schema)
175
+ else
176
+ v.coerce_and_validate(value, schema, **keyword_args)
177
+ end
178
+ return [coerced, nil] if err.nil?
179
+
180
+ first_error ||= err
181
+ end
182
+ return [nil, first_error] if first_error
183
+
184
+ type_mismatch_validator.coerce_and_validate(value, schema)
185
+ end
186
+
187
+ def type_mismatch_validator
188
+ @type_mismatch_validator ||= OpenAPIParser::SchemaValidator::TypeMismatchValidator.new(self, @coerce_value)
189
+ end
190
+
191
+ def pick_array_type(value, types)
192
+ types.find do |t|
193
+ case t
194
+ when 'string' then value.is_a?(String)
195
+ when 'integer' then value.is_a?(Integer)
196
+ when 'number' then value.is_a?(Numeric)
197
+ when 'boolean' then value == true || value == false
198
+ when 'array' then value.is_a?(Array)
199
+ when 'object' then value.is_a?(Hash)
200
+ when 'null' then value.nil?
201
+ end
202
+ end
203
+ end
204
+
123
205
  def string_validator
124
- @string_validator ||= OpenAPIParser::SchemaValidator::StringValidator.new(self, @allow_empty_date_and_datetime, @coerce_value, @datetime_coerce_class)
206
+ @string_validator ||= OpenAPIParser::SchemaValidator::StringValidator.new(self, @allow_empty_date_and_datetime, @coerce_value, @datetime_coerce_class, @date_coerce_class)
125
207
  end
126
208
 
127
209
  def integer_validator
@@ -24,5 +24,9 @@ module OpenAPIParser::Schemas
24
24
  # @!attribute [r] headers
25
25
  # @return [Hash{String => Header}, nil] header objects
26
26
  openapi_attr_hash_object :headers, Header, reference: true
27
+
28
+ # @!attribute [r] path_items
29
+ # @return [Hash{String => PathItem}, nil] path item objects (OpenAPI 3.1+)
30
+ openapi_attr_hash_object :path_items, PathItem, reference: true, schema_key: :pathItems
27
31
  end
28
32
  end
@@ -21,6 +21,18 @@ module OpenAPIParser::Schemas
21
21
  # @return [String, nil]
22
22
  openapi_attr_values :openapi
23
23
 
24
+ # Declared OpenAPI version as a comparable value.
25
+ # A prerelease tag is dropped ("3.1.0-rc1" => 3.1.0) because it does not
26
+ # change which version's rules the document is written against.
27
+ # @return [Gem::Version, nil] nil when the field is missing or not a
28
+ # major.minor[.patch] version string
29
+ def openapi_version
30
+ return nil unless openapi.is_a?(String)
31
+ return nil unless openapi.match?(/\A\d+\.\d+/) && Gem::Version.correct?(openapi)
32
+
33
+ Gem::Version.new(openapi).release
34
+ end
35
+
24
36
  # @!attribute [r] paths
25
37
  # @return [Paths, nil]
26
38
  openapi_attr_object :paths, Paths, reference: false
@@ -33,6 +45,14 @@ module OpenAPIParser::Schemas
33
45
  # @return [Info, nil]
34
46
  openapi_attr_object :info, Info, reference: false
35
47
 
48
+ # @!attribute [r] webhooks
49
+ # @return [Hash{String => PathItem}, nil] webhook path items (OpenAPI 3.1+)
50
+ openapi_attr_hash_object :webhooks, PathItem, reference: true
51
+
52
+ # @!attribute [r] json_schema_dialect
53
+ # @return [String, nil] dialect URI for embedded JSON Schemas (OpenAPI 3.1+)
54
+ openapi_attr_value :json_schema_dialect, schema_key: :jsonSchemaDialect
55
+
36
56
  # @return [OpenAPIParser::RequestOperation, nil]
37
57
  def request_operation(http_method, request_path)
38
58
  OpenAPIParser::RequestOperation.create(http_method, request_path, @path_item_finder, @config)
@@ -71,7 +71,10 @@ module OpenAPIParser::Schemas
71
71
  :type,
72
72
  :nullable,
73
73
  :example,
74
- :deprecated
74
+ :deprecated,
75
+ :const,
76
+ :contentMediaType,
77
+ :contentEncoding
75
78
 
76
79
  # @!attribute [r] read_only
77
80
  # @return [Boolean, nil]
@@ -97,6 +100,14 @@ module OpenAPIParser::Schemas
97
100
  # @return [Schema, nil]
98
101
  openapi_attr_object :items, Schema, reference: true
99
102
 
103
+ # @!attribute [r] prefix_items
104
+ # @return [Array<Schema>, nil] tuple-style positional schemas (OpenAPI 3.1+)
105
+ openapi_attr_list_object :prefix_items, Schema, reference: true, schema_key: :prefixItems
106
+
107
+ # @!attribute [r] content_schema
108
+ # @return [Schema, nil] schema describing the content payload (OpenAPI 3.1+)
109
+ openapi_attr_object :content_schema, Schema, reference: true, schema_key: :contentSchema
110
+
100
111
  # @!attribute [r] properties
101
112
  # @return [Hash{String => Schema}, nil]
102
113
  openapi_attr_hash_object :properties, Schema, reference: true
@@ -0,0 +1,68 @@
1
+ module OpenAPIParser
2
+ class SpecValidator
3
+ class Rule
4
+ def self.rule_name
5
+ name
6
+ .split('::')
7
+ .last
8
+ .gsub(/([A-Z])/) { "_#{Regexp.last_match(1).downcase}" }
9
+ .sub(/^_/, '')
10
+ .to_sym
11
+ end
12
+
13
+ def initialize(version)
14
+ @version = version
15
+ end
16
+
17
+ # @return [Gem::Version, nil] declared OpenAPI version, nil when unknown
18
+ attr_reader :version
19
+
20
+ def check(_root)
21
+ raise NotImplementedError
22
+ end
23
+
24
+ # @param [String] boundary like '3.1'
25
+ # @return [Boolean] false when the version is unknown
26
+ def version_before?(boundary)
27
+ !version.nil? && version < Gem::Version.new(boundary)
28
+ end
29
+
30
+ # @param [String] boundary like '3.1'
31
+ # @return [Boolean] false when the version is unknown
32
+ def version_at_least?(boundary)
33
+ !version.nil? && version >= Gem::Version.new(boundary)
34
+ end
35
+
36
+ private
37
+
38
+ def violation(path:, message:)
39
+ OpenAPIParser::SpecViolation.new(
40
+ message: message,
41
+ path: path,
42
+ rule_name: self.class.rule_name,
43
+ )
44
+ end
45
+
46
+ def each_schema(root, &block)
47
+ return enum_for(:each_schema, root) unless block
48
+
49
+ visited = {}
50
+ walk(root, visited) do |node|
51
+ yield node if node.is_a?(OpenAPIParser::Schemas::Schema)
52
+ end
53
+ end
54
+
55
+ def walk(node, visited, &block)
56
+ return unless node.respond_to?(:_openapi_all_child_objects)
57
+ return if visited[node.object_id]
58
+
59
+ visited[node.object_id] = true
60
+ block.call(node)
61
+
62
+ node._openapi_all_child_objects.each_value do |child|
63
+ walk(child, visited, &block)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,25 @@
1
+ module OpenAPIParser
2
+ class SpecValidator
3
+ module Rules
4
+ # `const` is a JSON Schema 2020-12 keyword adopted by OpenAPI 3.1.
5
+ # 3.0 does not recognize it. Detection inspects raw_schema so a
6
+ # literal `const: null` (deliberate) still flags.
7
+ class ConstIn30 < Rule
8
+ def check(root)
9
+ return [] unless version_before?('3.1')
10
+
11
+ violations = []
12
+ each_schema(root) do |schema|
13
+ next unless schema.raw_schema.is_a?(Hash) && schema.raw_schema.key?('const')
14
+
15
+ violations << violation(
16
+ path: schema.object_reference,
17
+ message: '`const` is a 3.1 addition (from JSON Schema 2020-12); 3.0 has no equivalent',
18
+ )
19
+ end
20
+ violations
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ module OpenAPIParser
2
+ class SpecValidator
3
+ module Rules
4
+ # `contentEncoding` is a JSON Schema 2020-12 annotation adopted by
5
+ # 3.1. Metadata only, no runtime side-effects.
6
+ class ContentEncodingIn30 < Rule
7
+ def check(root)
8
+ return [] unless version_before?('3.1')
9
+
10
+ violations = []
11
+ each_schema(root) do |schema|
12
+ next unless schema.raw_schema.is_a?(Hash) && schema.raw_schema.key?('contentEncoding')
13
+
14
+ violations << violation(
15
+ path: schema.object_reference,
16
+ message: '`contentEncoding` is a 3.1 addition (from JSON Schema 2020-12); 3.0 has no equivalent',
17
+ )
18
+ end
19
+ violations
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,25 @@
1
+ module OpenAPIParser
2
+ class SpecValidator
3
+ module Rules
4
+ # `contentMediaType` is a JSON Schema 2020-12 annotation adopted by
5
+ # 3.1. It is metadata, so it has no runtime side-effects; we only
6
+ # report version mismatches.
7
+ class ContentMediaTypeIn30 < Rule
8
+ def check(root)
9
+ return [] unless version_before?('3.1')
10
+
11
+ violations = []
12
+ each_schema(root) do |schema|
13
+ next unless schema.raw_schema.is_a?(Hash) && schema.raw_schema.key?('contentMediaType')
14
+
15
+ violations << violation(
16
+ path: schema.object_reference,
17
+ message: '`contentMediaType` is a 3.1 addition (from JSON Schema 2020-12); 3.0 has no equivalent',
18
+ )
19
+ end
20
+ violations
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ module OpenAPIParser
2
+ class SpecValidator
3
+ module Rules
4
+ # `contentSchema` is a JSON Schema 2020-12 annotation adopted by 3.1.
5
+ # The parse layer accepts it permissively; this rule reports the
6
+ # version mismatch on 3.0 documents.
7
+ class ContentSchemaIn30 < Rule
8
+ def check(root)
9
+ return [] unless version_before?('3.1')
10
+
11
+ violations = []
12
+ each_schema(root) do |schema|
13
+ next unless schema.raw_schema.is_a?(Hash) && schema.raw_schema.key?('contentSchema')
14
+
15
+ violations << violation(
16
+ path: schema.object_reference,
17
+ message: '`contentSchema` is a 3.1 addition (from JSON Schema 2020-12); 3.0 has no equivalent',
18
+ )
19
+ end
20
+ violations
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end