openapi_parser 2.3.1 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yaml +2 -1
  3. data/.ruby-version +1 -1
  4. data/CHANGELOG.md +40 -0
  5. data/lib/openapi_parser/config.rb +12 -0
  6. data/lib/openapi_parser/errors.rb +7 -0
  7. data/lib/openapi_parser/schema_validator/array_validator.rb +7 -3
  8. data/lib/openapi_parser/schema_validator/integer_validator.rb +2 -0
  9. data/lib/openapi_parser/schema_validator/minimum_maximum.rb +17 -4
  10. data/lib/openapi_parser/schema_validator/nil_validator.rb +4 -0
  11. data/lib/openapi_parser/schema_validator/type_mismatch_validator.rb +11 -0
  12. data/lib/openapi_parser/schema_validator.rb +92 -1
  13. data/lib/openapi_parser/schemas/components.rb +4 -0
  14. data/lib/openapi_parser/schemas/openapi.rb +24 -0
  15. data/lib/openapi_parser/schemas/schema.rb +20 -1
  16. data/lib/openapi_parser/spec_validator/rule.rb +68 -0
  17. data/lib/openapi_parser/spec_validator/rules/boolean_schema_in_30.rb +26 -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/self_before_32.rb +20 -0
  32. data/lib/openapi_parser/spec_validator/rules/type_array_in_30.rb +25 -0
  33. data/lib/openapi_parser/spec_validator/rules/type_null_in_30.rb +26 -0
  34. data/lib/openapi_parser/spec_validator/rules/webhooks_in_30.rb +18 -0
  35. data/lib/openapi_parser/spec_validator/spec_violation.rb +11 -0
  36. data/lib/openapi_parser/spec_validator.rb +89 -0
  37. data/lib/openapi_parser/version.rb +1 -1
  38. data/lib/openapi_parser.rb +3 -0
  39. data/sig/openapi_parser/config.rbs +2 -0
  40. data/sig/openapi_parser/schema_validator.rbs +3 -0
  41. data/sig/openapi_parser/spec_validator.rbs +117 -0
  42. data/sig/openapi_parser.rbs +2 -0
  43. metadata +25 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b3129b448382c0b36273808c25a197c9264ec23d4729c93f8c5594375cf16cc5
4
- data.tar.gz: 57aa683478c58dd684995e326687ef8e187a08fe4818e707812b1484a3e6e13b
3
+ metadata.gz: e570c33d05a24e9b11a3f5b86f5321f895088a672d404df68f3d14b4d3f9fb8f
4
+ data.tar.gz: fadcedd0e7b7c5ce057f2b0fa36f313218f151ae524dd2290c1242fbdfca02da
5
5
  SHA512:
6
- metadata.gz: 1e99aa6ea5ae7234c886c17c6d48bdeb75480db7698057aa77825301c0151caa986b59f6cc0b5a5a421c135fb47fb513e6440b6a3a18db4dae2f25620001f05a
7
- data.tar.gz: b5886770b412d8d73fe30a07039fcf76773880874e4762bd704e6ab3c4b545ade6ed722ebd010717af42c9e09e4e3ccd4fc0f53954c9d98bd24e64583ed99e48
6
+ metadata.gz: 680325ae11c7dce38425d23c89918d3ce5cb768261dbd48f6d21c166ca0d89b2dbf9304297b2f754830f47cdf8d45dd2501371b396fe972957474e171f4b3b49
7
+ data.tar.gz: 27b757f70c102671ff1f2891918f5e7abb044796e280027b097214f4f0bbb2ff538b7f7c84d7d1bb7f1fc8b46b63591ad62e37985dab50ebb35c0603b854e92e
@@ -17,10 +17,11 @@ jobs:
17
17
  - "3.2"
18
18
  - "3.3"
19
19
  - "3.4"
20
+ - "4.0"
20
21
  - ruby-head
21
22
  runs-on: ${{ matrix.os }}
22
23
  steps:
23
- - uses: actions/checkout@v5
24
+ - uses: actions/checkout@v7
24
25
  - uses: ruby/setup-ruby@v1
25
26
  with:
26
27
  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,45 @@
1
1
  ## Unreleased
2
2
 
3
+ ## 2.5.0 (2026-10-21)
4
+ * add OpenAPI 3.2 `SpecValidator` rules
5
+ * `SelfBefore32`: detect `$self` usage in pre-3.2 documents (3.2 addition)
6
+ * support root-level `$self` (OpenAPI 3.2) in the parse layer (`self_uri`); it is not yet used as the base URI for `$ref` resolution
7
+ * support boolean schemas (OpenAPI 3.1): `true` accepts any value and `false` rejects any value with `FalseSchemaError`, instead of crashing the loader with `NoMethodError`
8
+ * add `SpecValidator` rule `BooleanSchemaIn30`: detect boolean schemas in 3.0 documents (3.1 addition; `additionalProperties` is not affected)
9
+ * coerce non-String values into array-form `type` (3.1) the same way as a single `type` when `coerce_value` is on, so `type: [integer, "null"]` handles a Float the same way as `type: integer`
10
+ * stop truncating a Float with a fractional part in integer coercion (`coerce_value`): `1.5` for `type: integer` now fails instead of becoming `1`, while a Float with no fractional part, like `1.0`, is still converted
11
+
12
+ ## 2.4.0 (2026-10-14)
13
+ * support `components.pathItems` so `$ref`s into it resolve, unblocking OpenAPI 3.1 documents that use reusable path items
14
+ * support array-form `type` (3.1) in value validation
15
+ * support root-level `jsonSchemaDialect` (OpenAPI 3.1) in the parse layer
16
+ * support `prefixItems` (OpenAPI 3.1) with positional tuple validation
17
+ * support `contentMediaType` (OpenAPI 3.1) in the parse layer
18
+ * support `contentEncoding` (OpenAPI 3.1) in the parse layer
19
+ * add `SpecValidator` with `strict_specification_version` config (`:silent` / `:warn` / `:raise`) to detect version mismatches between declared OpenAPI version and actual field usage
20
+ * `ContentEncodingIn30`: detect `contentEncoding` usage in 3.0 documents (3.1 addition)
21
+ * `ContentMediaTypeIn30`: detect `contentMediaType` usage in 3.0 documents (3.1 addition)
22
+ * `PrefixItemsIn30`: detect `prefixItems` usage in 3.0 documents (3.1 addition)
23
+ * `JsonSchemaDialectIn30`: detect root-level `jsonSchemaDialect` usage in 3.0 documents (3.1 addition)
24
+ * `TypeArrayIn30`: detect array-form `type` usage in 3.0 documents (3.1 form)
25
+ * `NullableDeprecation`: detect `nullable` usage in 3.1 documents (removed in 3.1)
26
+ * `ExampleSingularDeprecation`: detect singular `example` on schemas in 3.1 documents (deprecated in 3.1)
27
+ * `PathItemsIn30`: detect `components.pathItems` usage in 3.0 documents (3.1 addition)
28
+ * `ExclusiveMinimum` / `ExclusiveMaximum`: detect 3.0 Boolean vs 3.1 numeric form mismatch
29
+ * `TypeNullIn30`: detect `type: "null"` usage in 3.0 documents (3.1 primitive)
30
+ * `WebhooksIn30`: detect root-level `webhooks` usage in 3.0 documents (3.1 addition)
31
+ * `ConstIn30`: detect `const` usage in 3.0 documents (3.1 addition)
32
+ * `DynamicRefIn30`: detect `$dynamicRef` usage in 3.0 documents (3.1 addition)
33
+ * `DynamicAnchorIn30`: detect `$dynamicAnchor` usage in 3.0 documents (3.1 addition)
34
+ * `ContentSchemaIn30`: detect `contentSchema` usage in 3.0 documents (3.1 addition)
35
+ * expose the declared version as `OpenAPI#openapi_version` (a `Gem::Version`, or nil when the field is missing or malformed) so `SpecValidator` rules compare version ranges; a 3.2 document is checked by the 3.1-or-later rules
36
+ * support 3.1-style numeric `exclusiveMinimum` / `exclusiveMaximum` in value validation (standalone bound, not a Boolean modifier on `minimum` / `maximum`)
37
+ * support `type: "null"` (3.1 primitive) in value validation
38
+ * support root-level `webhooks` (OpenAPI 3.1) in the parse layer
39
+ * support `const` (OpenAPI 3.1) with exact-equality value validation
40
+ * support `contentSchema` (OpenAPI 3.1) in the parse layer
41
+ * coerce String values into array-form `type` (3.1) when `coerce_value` is on, so params like `type: [integer, "null"]` accept `"5"`
42
+
3
43
  ## 2.3.1 (2025-11-14)
4
44
  * add optional date coercion with behavior matching existing datetime coercion
5
45
 
@@ -41,6 +41,18 @@ class OpenAPIParser::Config
41
41
  @config.fetch(:strict_reference_validation, false)
42
42
  end
43
43
 
44
+ KNOWN_STRICT_SPECIFICATION_VERSION_VALUES = %i[silent warn raise].freeze
45
+
46
+ # @return [Symbol] :silent / :warn / :raise
47
+ def strict_specification_version
48
+ value = @config.fetch(:strict_specification_version, :silent)
49
+ unless KNOWN_STRICT_SPECIFICATION_VERSION_VALUES.include?(value)
50
+ warn("[OpenAPIParser] unknown strict_specification_version: #{value.inspect}, falling back to :silent")
51
+ return :silent
52
+ end
53
+ value
54
+ end
55
+
44
56
  def validate_header
45
57
  @config.fetch(:validate_header, true)
46
58
  end
@@ -38,6 +38,13 @@ module OpenAPIParser
38
38
  end
39
39
  end
40
40
 
41
+ # 3.1: raised for any value checked against the boolean schema `false`
42
+ class FalseSchemaError < OpenAPIError
43
+ def message
44
+ "#{@reference} does not allow any value"
45
+ end
46
+ end
47
+
41
48
  class NotExistRequiredKey < OpenAPIError
42
49
  def initialize(keys, reference)
43
50
  super(reference)
@@ -11,10 +11,14 @@ class OpenAPIParser::SchemaValidator
11
11
  value, err = validate_unique_items(value, schema)
12
12
  return [nil, err] if err
13
13
 
14
- # 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
@@ -21,6 +21,8 @@ class OpenAPIParser::SchemaValidator
21
21
 
22
22
  def coerce(value)
23
23
  return value if value.kind_of?(Integer)
24
+ # Integer() truncates a Float, so convert only one with no fractional part.
25
+ return value.finite? && value == value.floor ? value.to_i : value if value.kind_of?(Float)
24
26
 
25
27
  begin
26
28
  Integer(value)
@@ -4,8 +4,10 @@ class OpenAPIParser::SchemaValidator
4
4
  # @param [Object] value
5
5
  # @param [OpenAPIParser::Schemas::Schema] schema
6
6
  def check_minimum_maximum(value, schema)
7
- 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
@@ -0,0 +1,11 @@
1
+ class OpenAPIParser::SchemaValidator
2
+ # Emits a ValidateError unconditionally. Used when the dispatcher has
3
+ # already decided that no primitive applies to the value, e.g.:
4
+ # - schema declares `type: "null"` and the value is non-nil (3.1)
5
+ # - schema declares `type: [t1, t2, ...]` and the value matches none
6
+ class TypeMismatchValidator < Base
7
+ def coerce_and_validate(value, schema, **_keyword_args)
8
+ OpenAPIParser::ValidateError.build_error_result(value, schema)
9
+ end
10
+ end
11
+ end
@@ -13,6 +13,7 @@ require_relative 'schema_validator/any_of_validator'
13
13
  require_relative 'schema_validator/all_of_validator'
14
14
  require_relative 'schema_validator/one_of_validator'
15
15
  require_relative 'schema_validator/nil_validator'
16
+ require_relative 'schema_validator/type_mismatch_validator'
16
17
  require_relative 'schema_validator/unspecified_type_validator'
17
18
 
18
19
  class OpenAPIParser::SchemaValidator
@@ -74,6 +75,22 @@ class OpenAPIParser::SchemaValidator
74
75
  def validate_schema(value, schema, **keyword_args)
75
76
  return [value, nil] unless schema
76
77
 
78
+ # 3.1: a boolean schema accepts (`true`) or rejects (`false`) any value,
79
+ # including null.
80
+ if schema.respond_to?(:raw_schema)
81
+ return [value, nil] if schema.raw_schema == true
82
+ return [nil, OpenAPIParser::FalseSchemaError.new(schema.object_reference)] if schema.raw_schema == false
83
+ end
84
+
85
+ # 3.1: `const` pins the value to exactly that constant. Checked before
86
+ # type dispatch so it applies uniformly across primitives. Detection
87
+ # uses raw_schema so an intentional `const: null` is honored.
88
+ if schema.respond_to?(:raw_schema) && schema.raw_schema.is_a?(Hash) && schema.raw_schema.key?('const')
89
+ return [nil, OpenAPIParser::ValidateError.new(value, "const #{schema.const.inspect}", schema.object_reference)] if value != schema.const
90
+ end
91
+
92
+ return coerce_into_type_array(value, schema, **keyword_args) if coerce_into_type_array?(value, schema)
93
+
77
94
  if (v = validator(value, schema))
78
95
  if keyword_args.empty?
79
96
  return v.coerce_and_validate(value, schema)
@@ -103,7 +120,25 @@ class OpenAPIParser::SchemaValidator
103
120
  return one_of_validator if schema.one_of
104
121
  return nil_validator if value.nil?
105
122
 
106
- case schema.type
123
+ # 3.1: pick the type from an Array whose primitive class matches the
124
+ # value, then dispatch as if it were a single-type schema. If nothing
125
+ # matches we still pick a candidate so the relevant validator emits
126
+ # a sensible error rather than silently accepting via the unspecified
127
+ # fallback.
128
+ effective_type = schema.type
129
+ if effective_type.is_a?(Array)
130
+ matched = pick_array_type(value, effective_type)
131
+ return type_mismatch_validator if matched.nil?
132
+
133
+ effective_type = matched
134
+ end
135
+
136
+ validator_for_type(effective_type)
137
+ end
138
+
139
+ # @return [OpenAPIParser::SchemaValidator::Base]
140
+ def validator_for_type(type)
141
+ case type
107
142
  when 'string'
108
143
  string_validator
109
144
  when 'integer'
@@ -116,11 +151,67 @@ class OpenAPIParser::SchemaValidator
116
151
  object_validator
117
152
  when 'array'
118
153
  array_validator
154
+ when 'null'
155
+ # 3.1: only nil values are valid here. nil is handled earlier in
156
+ # this method, so a non-nil value reaching this branch is a type
157
+ # mismatch that should fail validation.
158
+ type_mismatch_validator
119
159
  else
120
160
  unspecified_type_validator
121
161
  end
122
162
  end
123
163
 
164
+ # 3.1: when coercing, a value whose class matches no listed type is tried
165
+ # against each listed type, the same way a single `type` coerces it.
166
+ # Request params arrive as Strings ("5" for `type: [integer, "null"]`),
167
+ # and a single `type: integer` also coerces a Float such as 1.0.
168
+ # nil is left to the nil validator, as it is for a single `type`.
169
+ # @return [Boolean]
170
+ def coerce_into_type_array?(value, schema)
171
+ return false unless @coerce_value && !value.nil?
172
+ return false if schema.any_of || schema.all_of || schema.one_of
173
+
174
+ schema.type.is_a?(Array) && pick_array_type(value, schema.type).nil?
175
+ end
176
+
177
+ # 3.1: returns the first listed type that coerces, or the first type's error.
178
+ # @return [Array] coerced value and error
179
+ def coerce_into_type_array(value, schema, **keyword_args)
180
+ first_error = nil
181
+ (schema.type - ['null']).each do |type|
182
+ v = validator_for_type(type)
183
+ coerced, err = if keyword_args.empty?
184
+ v.coerce_and_validate(value, schema)
185
+ else
186
+ v.coerce_and_validate(value, schema, **keyword_args)
187
+ end
188
+ return [coerced, nil] if err.nil?
189
+
190
+ first_error ||= err
191
+ end
192
+ return [nil, first_error] if first_error
193
+
194
+ type_mismatch_validator.coerce_and_validate(value, schema)
195
+ end
196
+
197
+ def type_mismatch_validator
198
+ @type_mismatch_validator ||= OpenAPIParser::SchemaValidator::TypeMismatchValidator.new(self, @coerce_value)
199
+ end
200
+
201
+ def pick_array_type(value, types)
202
+ types.find do |t|
203
+ case t
204
+ when 'string' then value.is_a?(String)
205
+ when 'integer' then value.is_a?(Integer)
206
+ when 'number' then value.is_a?(Numeric)
207
+ when 'boolean' then value == true || value == false
208
+ when 'array' then value.is_a?(Array)
209
+ when 'object' then value.is_a?(Hash)
210
+ when 'null' then value.nil?
211
+ end
212
+ end
213
+ end
214
+
124
215
  def string_validator
125
216
  @string_validator ||= OpenAPIParser::SchemaValidator::StringValidator.new(self, @allow_empty_date_and_datetime, @coerce_value, @datetime_coerce_class, @date_coerce_class)
126
217
  end
@@ -24,5 +24,9 @@ module OpenAPIParser::Schemas
24
24
  # @!attribute [r] headers
25
25
  # @return [Hash{String => Header}, nil] header objects
26
26
  openapi_attr_hash_object :headers, Header, reference: true
27
+
28
+ # @!attribute [r] path_items
29
+ # @return [Hash{String => PathItem}, nil] path item objects (OpenAPI 3.1+)
30
+ openapi_attr_hash_object :path_items, PathItem, reference: true, schema_key: :pathItems
27
31
  end
28
32
  end
@@ -21,6 +21,18 @@ module OpenAPIParser::Schemas
21
21
  # @return [String, nil]
22
22
  openapi_attr_values :openapi
23
23
 
24
+ # Declared OpenAPI version as a comparable value.
25
+ # A prerelease tag is dropped ("3.1.0-rc1" => 3.1.0) because it does not
26
+ # change which version's rules the document is written against.
27
+ # @return [Gem::Version, nil] nil when the field is missing or not a
28
+ # major.minor[.patch] version string
29
+ def openapi_version
30
+ return nil unless openapi.is_a?(String)
31
+ return nil unless openapi.match?(/\A\d+\.\d+/) && Gem::Version.correct?(openapi)
32
+
33
+ Gem::Version.new(openapi).release
34
+ end
35
+
24
36
  # @!attribute [r] paths
25
37
  # @return [Paths, nil]
26
38
  openapi_attr_object :paths, Paths, reference: false
@@ -33,6 +45,18 @@ module OpenAPIParser::Schemas
33
45
  # @return [Info, nil]
34
46
  openapi_attr_object :info, Info, reference: false
35
47
 
48
+ # @!attribute [r] webhooks
49
+ # @return [Hash{String => PathItem}, nil] webhook path items (OpenAPI 3.1+)
50
+ openapi_attr_hash_object :webhooks, PathItem, reference: true
51
+
52
+ # @!attribute [r] json_schema_dialect
53
+ # @return [String, nil] dialect URI for embedded JSON Schemas (OpenAPI 3.1+)
54
+ openapi_attr_value :json_schema_dialect, schema_key: :jsonSchemaDialect
55
+
56
+ # @!attribute [r] self_uri
57
+ # @return [String, nil] the document's own URI, `$self` (OpenAPI 3.2+)
58
+ openapi_attr_value :self_uri, schema_key: :'$self'
59
+
36
60
  # @return [OpenAPIParser::RequestOperation, nil]
37
61
  def request_operation(http_method, request_path)
38
62
  OpenAPIParser::RequestOperation.create(http_method, request_path, @path_item_finder, @config)
@@ -71,7 +71,10 @@ module OpenAPIParser::Schemas
71
71
  :type,
72
72
  :nullable,
73
73
  :example,
74
- :deprecated
74
+ :deprecated,
75
+ :const,
76
+ :contentMediaType,
77
+ :contentEncoding
75
78
 
76
79
  # @!attribute [r] read_only
77
80
  # @return [Boolean, nil]
@@ -97,6 +100,14 @@ module OpenAPIParser::Schemas
97
100
  # @return [Schema, nil]
98
101
  openapi_attr_object :items, Schema, reference: true
99
102
 
103
+ # @!attribute [r] prefix_items
104
+ # @return [Array<Schema>, nil] tuple-style positional schemas (OpenAPI 3.1+)
105
+ openapi_attr_list_object :prefix_items, Schema, reference: true, schema_key: :prefixItems
106
+
107
+ # @!attribute [r] content_schema
108
+ # @return [Schema, nil] schema describing the content payload (OpenAPI 3.1+)
109
+ openapi_attr_object :content_schema, Schema, reference: true, schema_key: :contentSchema
110
+
100
111
  # @!attribute [r] properties
101
112
  # @return [Hash{String => Schema}, nil]
102
113
  openapi_attr_hash_object :properties, Schema, reference: true
@@ -114,5 +125,13 @@ module OpenAPIParser::Schemas
114
125
  def additional_properties
115
126
  @additional_properties.nil? ? true : @additional_properties
116
127
  end
128
+
129
+ # 3.1: a boolean schema (`true` / `false`) has no keywords to load.
130
+ def load_data
131
+ return super if raw_schema.is_a?(::Hash)
132
+
133
+ @_openapi_all_child_objects = {}
134
+ nil
135
+ end
117
136
  end
118
137
  end
@@ -0,0 +1,68 @@
1
+ module OpenAPIParser
2
+ class SpecValidator
3
+ class Rule
4
+ def self.rule_name
5
+ name
6
+ .split('::')
7
+ .last
8
+ .gsub(/([A-Z])/) { "_#{Regexp.last_match(1).downcase}" }
9
+ .sub(/^_/, '')
10
+ .to_sym
11
+ end
12
+
13
+ def initialize(version)
14
+ @version = version
15
+ end
16
+
17
+ # @return [Gem::Version, nil] declared OpenAPI version, nil when unknown
18
+ attr_reader :version
19
+
20
+ def check(_root)
21
+ raise NotImplementedError
22
+ end
23
+
24
+ # @param [String] boundary like '3.1'
25
+ # @return [Boolean] false when the version is unknown
26
+ def version_before?(boundary)
27
+ !version.nil? && version < Gem::Version.new(boundary)
28
+ end
29
+
30
+ # @param [String] boundary like '3.1'
31
+ # @return [Boolean] false when the version is unknown
32
+ def version_at_least?(boundary)
33
+ !version.nil? && version >= Gem::Version.new(boundary)
34
+ end
35
+
36
+ private
37
+
38
+ def violation(path:, message:)
39
+ OpenAPIParser::SpecViolation.new(
40
+ message: message,
41
+ path: path,
42
+ rule_name: self.class.rule_name,
43
+ )
44
+ end
45
+
46
+ def each_schema(root, &block)
47
+ return enum_for(:each_schema, root) unless block
48
+
49
+ visited = {}
50
+ walk(root, visited) do |node|
51
+ yield node if node.is_a?(OpenAPIParser::Schemas::Schema)
52
+ end
53
+ end
54
+
55
+ def walk(node, visited, &block)
56
+ return unless node.respond_to?(:_openapi_all_child_objects)
57
+ return if visited[node.object_id]
58
+
59
+ visited[node.object_id] = true
60
+ block.call(node)
61
+
62
+ node._openapi_all_child_objects.each_value do |child|
63
+ walk(child, visited, &block)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,26 @@
1
+ module OpenAPIParser
2
+ class SpecValidator
3
+ module Rules
4
+ # A boolean schema (`true` / `false` in place of a schema object) comes
5
+ # from JSON Schema 2020-12, which 3.1 adopts. 3.0 allows a boolean only
6
+ # for `additionalProperties`, which is not loaded as a Schema, so any
7
+ # Schema whose raw value is a boolean is a spec mismatch in 3.0.
8
+ class BooleanSchemaIn30 < Rule
9
+ def check(root)
10
+ return [] unless version_before?('3.1')
11
+
12
+ violations = []
13
+ each_schema(root) do |schema|
14
+ next unless schema.raw_schema == true || schema.raw_schema == false
15
+
16
+ violations << violation(
17
+ path: schema.object_reference,
18
+ message: 'a boolean schema is a 3.1 addition (from JSON Schema 2020-12); 3.0 allows a boolean only for `additionalProperties`',
19
+ )
20
+ end
21
+ violations
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ module OpenAPIParser
2
+ class SpecValidator
3
+ module Rules
4
+ # `const` is a JSON Schema 2020-12 keyword adopted by OpenAPI 3.1.
5
+ # 3.0 does not recognize it. Detection inspects raw_schema so a
6
+ # literal `const: null` (deliberate) still flags.
7
+ class ConstIn30 < Rule
8
+ def check(root)
9
+ return [] unless version_before?('3.1')
10
+
11
+ violations = []
12
+ each_schema(root) do |schema|
13
+ next unless schema.raw_schema.is_a?(Hash) && schema.raw_schema.key?('const')
14
+
15
+ violations << violation(
16
+ path: schema.object_reference,
17
+ message: '`const` is a 3.1 addition (from JSON Schema 2020-12); 3.0 has no equivalent',
18
+ )
19
+ end
20
+ violations
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ module OpenAPIParser
2
+ class SpecValidator
3
+ module Rules
4
+ # `contentEncoding` is a JSON Schema 2020-12 annotation adopted by
5
+ # 3.1. Metadata only, no runtime side-effects.
6
+ class ContentEncodingIn30 < Rule
7
+ def check(root)
8
+ return [] unless version_before?('3.1')
9
+
10
+ violations = []
11
+ each_schema(root) do |schema|
12
+ next unless schema.raw_schema.is_a?(Hash) && schema.raw_schema.key?('contentEncoding')
13
+
14
+ violations << violation(
15
+ path: schema.object_reference,
16
+ message: '`contentEncoding` is a 3.1 addition (from JSON Schema 2020-12); 3.0 has no equivalent',
17
+ )
18
+ end
19
+ violations
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,25 @@
1
+ module OpenAPIParser
2
+ class SpecValidator
3
+ module Rules
4
+ # `contentMediaType` is a JSON Schema 2020-12 annotation adopted by
5
+ # 3.1. It is metadata, so it has no runtime side-effects; we only
6
+ # report version mismatches.
7
+ class ContentMediaTypeIn30 < Rule
8
+ def check(root)
9
+ return [] unless version_before?('3.1')
10
+
11
+ violations = []
12
+ each_schema(root) do |schema|
13
+ next unless schema.raw_schema.is_a?(Hash) && schema.raw_schema.key?('contentMediaType')
14
+
15
+ violations << violation(
16
+ path: schema.object_reference,
17
+ message: '`contentMediaType` is a 3.1 addition (from JSON Schema 2020-12); 3.0 has no equivalent',
18
+ )
19
+ end
20
+ violations
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ module OpenAPIParser
2
+ class SpecValidator
3
+ module Rules
4
+ # `contentSchema` is a JSON Schema 2020-12 annotation adopted by 3.1.
5
+ # The parse layer accepts it permissively; this rule reports the
6
+ # version mismatch on 3.0 documents.
7
+ class ContentSchemaIn30 < Rule
8
+ def check(root)
9
+ return [] unless version_before?('3.1')
10
+
11
+ violations = []
12
+ each_schema(root) do |schema|
13
+ next unless schema.raw_schema.is_a?(Hash) && schema.raw_schema.key?('contentSchema')
14
+
15
+ violations << violation(
16
+ path: schema.object_reference,
17
+ message: '`contentSchema` is a 3.1 addition (from JSON Schema 2020-12); 3.0 has no equivalent',
18
+ )
19
+ end
20
+ violations
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ module OpenAPIParser
2
+ class SpecValidator
3
+ module Rules
4
+ # `$dynamicAnchor` is the companion of `$dynamicRef` from JSON Schema
5
+ # 2020-12, declaring the dynamic resolution point. 3.0 has nothing
6
+ # equivalent; the rule flags it on 3.0 documents.
7
+ class DynamicAnchorIn30 < Rule
8
+ def check(root)
9
+ return [] unless version_before?('3.1')
10
+
11
+ violations = []
12
+ each_schema(root) do |schema|
13
+ next unless schema.raw_schema.is_a?(Hash) && schema.raw_schema.key?('$dynamicAnchor')
14
+
15
+ violations << violation(
16
+ path: schema.object_reference,
17
+ message: '`$dynamicAnchor` is a 3.1 addition (from JSON Schema 2020-12); 3.0 has no equivalent',
18
+ )
19
+ end
20
+ violations
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end