From 19cbd28185be7e66f877ddb13bcbdffe37f22045 Mon Sep 17 00:00:00 2001 From: fusagiko / takayamaki <24884114+takayamaki@users.noreply.github.com> Date: Wed, 23 Sep 2026 22:54:08 +0900 Subject: [PATCH 1/2] Return openapi_version as a Gem::Version so SpecValidator rules can compare ranges (#209) * Enumerate specs for a comparable openapi_version List the cases for returning openapi_version as a Gem::Version, for the version range helpers on SpecValidator::Rule, and for the 3.1-or-later rules applied to 3.2 documents. * Add version range helpers to SpecValidator::Rule Rules compare the declared version with == against a Symbol today, which cannot express "3.1 or later". version_before? and version_at_least? take a boundary string and compare against a Gem::Version, returning false for an unknown version. * Return openapi_version as a Gem::Version and compare ranges in rules openapi_version returned :v3_0 / :v3_1 / :unknown and every rule compared it with ==, so a 3.2 document fell into :unknown and no rule ran on it. It now returns Gem::Version (prerelease dropped) or nil when the field is missing or not a major.minor[.patch] string. Rules use version_before?('3.1') / version_at_least?('3.1'), so the 3.1-or-later rules (nullable, singular example, Boolean exclusiveMinimum / exclusiveMaximum) also cover 3.2. --------- Co-authored-by: fusagiko / takayamaki --- CHANGELOG.md | 1 + lib/openapi_parser/schemas/openapi.rb | 19 +++--- lib/openapi_parser/spec_validator/rule.rb | 13 ++++ .../spec_validator/rules/const_in_30.rb | 2 +- .../rules/content_encoding_in_30.rb | 2 +- .../rules/content_media_type_in_30.rb | 2 +- .../rules/content_schema_in_30.rb | 2 +- .../rules/dynamic_anchor_in_30.rb | 2 +- .../spec_validator/rules/dynamic_ref_in_30.rb | 2 +- .../rules/example_singular_deprecation.rb | 2 +- .../spec_validator/rules/exclusive_maximum.rb | 17 ++--- .../spec_validator/rules/exclusive_minimum.rb | 17 ++--- .../rules/json_schema_dialect_in_30.rb | 2 +- .../rules/nullable_deprecation.rb | 2 +- .../spec_validator/rules/path_items_in_30.rb | 2 +- .../rules/prefix_items_in_30.rb | 2 +- .../spec_validator/rules/type_array_in_30.rb | 2 +- .../spec_validator/rules/type_null_in_30.rb | 2 +- .../spec_validator/rules/webhooks_in_30.rb | 2 +- sig/openapi_parser.rbs | 2 +- sig/openapi_parser/spec_validator.rbs | 10 +-- spec/openapi_parser/schemas/open_api_spec.rb | 50 ++++++++------ .../spec_validator/rule_spec.rb | 66 +++++++++++++++++++ .../spec_validator/rules/const_in_30_spec.rb | 4 +- .../rules/content_encoding_in_30_spec.rb | 4 +- .../rules/content_media_type_in_30_spec.rb | 4 +- .../rules/content_schema_in_30_spec.rb | 4 +- .../rules/dynamic_anchor_in_30_spec.rb | 4 +- .../rules/dynamic_ref_in_30_spec.rb | 4 +- .../example_singular_deprecation_spec.rb | 11 +++- .../rules/exclusive_maximum_spec.rb | 20 +++++- .../rules/exclusive_minimum_spec.rb | 20 +++++- .../rules/json_schema_dialect_in_30_spec.rb | 4 +- .../rules/nullable_deprecation_spec.rb | 11 +++- .../rules/path_items_in_30_spec.rb | 4 +- .../rules/prefix_items_in_30_spec.rb | 4 +- .../rules/type_array_in_30_spec.rb | 4 +- .../rules/type_null_in_30_spec.rb | 4 +- .../rules/webhooks_in_30_spec.rb | 4 +- 39 files changed, 233 insertions(+), 100 deletions(-) create mode 100644 spec/openapi_parser/spec_validator/rule_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index c02a9b05..e6785f8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ * `DynamicRefIn30`: detect `$dynamicRef` usage in 3.0 documents (3.1 addition) * `DynamicAnchorIn30`: detect `$dynamicAnchor` usage in 3.0 documents (3.1 addition) * `ContentSchemaIn30`: detect `contentSchema` usage in 3.0 documents (3.1 addition) +* 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 * support 3.1-style numeric `exclusiveMinimum` / `exclusiveMaximum` in value validation (standalone bound, not a Boolean modifier on `minimum` / `maximum`) * support `type: "null"` (3.1 primitive) in value validation * support root-level `webhooks` (OpenAPI 3.1) in the parse layer diff --git a/lib/openapi_parser/schemas/openapi.rb b/lib/openapi_parser/schemas/openapi.rb index f80cc367..5e916888 100644 --- a/lib/openapi_parser/schemas/openapi.rb +++ b/lib/openapi_parser/schemas/openapi.rb @@ -21,17 +21,16 @@ def initialize(raw_schema, config, uri: nil, schema_registry: {}) # @return [String, nil] openapi_attr_values :openapi - # @return [Symbol] :v3_0 / :v3_1 / :unknown + # Declared OpenAPI version as a comparable value. + # A prerelease tag is dropped ("3.1.0-rc1" => 3.1.0) because it does not + # change which version's rules the document is written against. + # @return [Gem::Version, nil] nil when the field is missing or not a + # major.minor[.patch] version string def openapi_version - return :unknown unless openapi.is_a?(String) - - if openapi.start_with?('3.0') - :v3_0 - elsif openapi.start_with?('3.1') - :v3_1 - else - :unknown - end + return nil unless openapi.is_a?(String) + return nil unless openapi.match?(/\A\d+\.\d+/) && Gem::Version.correct?(openapi) + + Gem::Version.new(openapi).release end # @!attribute [r] paths diff --git a/lib/openapi_parser/spec_validator/rule.rb b/lib/openapi_parser/spec_validator/rule.rb index 733f1283..56eae76f 100644 --- a/lib/openapi_parser/spec_validator/rule.rb +++ b/lib/openapi_parser/spec_validator/rule.rb @@ -14,12 +14,25 @@ def initialize(version) @version = version end + # @return [Gem::Version, nil] declared OpenAPI version, nil when unknown attr_reader :version def check(_root) raise NotImplementedError end + # @param [String] boundary like '3.1' + # @return [Boolean] false when the version is unknown + def version_before?(boundary) + !version.nil? && version < Gem::Version.new(boundary) + end + + # @param [String] boundary like '3.1' + # @return [Boolean] false when the version is unknown + def version_at_least?(boundary) + !version.nil? && version >= Gem::Version.new(boundary) + end + private def violation(path:, message:) diff --git a/lib/openapi_parser/spec_validator/rules/const_in_30.rb b/lib/openapi_parser/spec_validator/rules/const_in_30.rb index 5d68883b..68c100d3 100644 --- a/lib/openapi_parser/spec_validator/rules/const_in_30.rb +++ b/lib/openapi_parser/spec_validator/rules/const_in_30.rb @@ -6,7 +6,7 @@ module Rules # literal `const: null` (deliberate) still flags. class ConstIn30 < Rule def check(root) - return [] unless version == :v3_0 + return [] unless version_before?('3.1') violations = [] each_schema(root) do |schema| diff --git a/lib/openapi_parser/spec_validator/rules/content_encoding_in_30.rb b/lib/openapi_parser/spec_validator/rules/content_encoding_in_30.rb index 95d9fc47..fe63c54f 100644 --- a/lib/openapi_parser/spec_validator/rules/content_encoding_in_30.rb +++ b/lib/openapi_parser/spec_validator/rules/content_encoding_in_30.rb @@ -5,7 +5,7 @@ module Rules # 3.1. Metadata only, no runtime side-effects. class ContentEncodingIn30 < Rule def check(root) - return [] unless version == :v3_0 + return [] unless version_before?('3.1') violations = [] each_schema(root) do |schema| diff --git a/lib/openapi_parser/spec_validator/rules/content_media_type_in_30.rb b/lib/openapi_parser/spec_validator/rules/content_media_type_in_30.rb index ba6908f7..3ef08b2e 100644 --- a/lib/openapi_parser/spec_validator/rules/content_media_type_in_30.rb +++ b/lib/openapi_parser/spec_validator/rules/content_media_type_in_30.rb @@ -6,7 +6,7 @@ module Rules # report version mismatches. class ContentMediaTypeIn30 < Rule def check(root) - return [] unless version == :v3_0 + return [] unless version_before?('3.1') violations = [] each_schema(root) do |schema| diff --git a/lib/openapi_parser/spec_validator/rules/content_schema_in_30.rb b/lib/openapi_parser/spec_validator/rules/content_schema_in_30.rb index 60f221f6..a94d27f3 100644 --- a/lib/openapi_parser/spec_validator/rules/content_schema_in_30.rb +++ b/lib/openapi_parser/spec_validator/rules/content_schema_in_30.rb @@ -6,7 +6,7 @@ module Rules # version mismatch on 3.0 documents. class ContentSchemaIn30 < Rule def check(root) - return [] unless version == :v3_0 + return [] unless version_before?('3.1') violations = [] each_schema(root) do |schema| diff --git a/lib/openapi_parser/spec_validator/rules/dynamic_anchor_in_30.rb b/lib/openapi_parser/spec_validator/rules/dynamic_anchor_in_30.rb index a42ce5f7..196cf168 100644 --- a/lib/openapi_parser/spec_validator/rules/dynamic_anchor_in_30.rb +++ b/lib/openapi_parser/spec_validator/rules/dynamic_anchor_in_30.rb @@ -6,7 +6,7 @@ module Rules # equivalent; the rule flags it on 3.0 documents. class DynamicAnchorIn30 < Rule def check(root) - return [] unless version == :v3_0 + return [] unless version_before?('3.1') violations = [] each_schema(root) do |schema| diff --git a/lib/openapi_parser/spec_validator/rules/dynamic_ref_in_30.rb b/lib/openapi_parser/spec_validator/rules/dynamic_ref_in_30.rb index 34b7e62c..76123dd1 100644 --- a/lib/openapi_parser/spec_validator/rules/dynamic_ref_in_30.rb +++ b/lib/openapi_parser/spec_validator/rules/dynamic_ref_in_30.rb @@ -6,7 +6,7 @@ module Rules # value as raw schema data; this rule reports the version mismatch. class DynamicRefIn30 < Rule def check(root) - return [] unless version == :v3_0 + return [] unless version_before?('3.1') violations = [] each_schema(root) do |schema| diff --git a/lib/openapi_parser/spec_validator/rules/example_singular_deprecation.rb b/lib/openapi_parser/spec_validator/rules/example_singular_deprecation.rb index 0cb8c69e..6351b3bf 100644 --- a/lib/openapi_parser/spec_validator/rules/example_singular_deprecation.rb +++ b/lib/openapi_parser/spec_validator/rules/example_singular_deprecation.rb @@ -6,7 +6,7 @@ module Rules # allowed but discouraged, so we report it as a violation. class ExampleSingularDeprecation < Rule def check(root) - return [] unless version == :v3_1 + return [] unless version_at_least?('3.1') violations = [] each_schema(root) do |schema| diff --git a/lib/openapi_parser/spec_validator/rules/exclusive_maximum.rb b/lib/openapi_parser/spec_validator/rules/exclusive_maximum.rb index c2b4cdf4..6e097488 100644 --- a/lib/openapi_parser/spec_validator/rules/exclusive_maximum.rb +++ b/lib/openapi_parser/spec_validator/rules/exclusive_maximum.rb @@ -3,28 +3,25 @@ class SpecValidator module Rules class ExclusiveMaximum < Rule def check(root) - return [] if version == :unknown + return [] if version.nil? violations = [] each_schema(root) do |schema| value = schema.exclusiveMaximum next if value.nil? - case version - when :v3_0 + if version_before?('3.1') if value.is_a?(Numeric) violations << violation( path: schema.object_reference, message: 'numeric exclusiveMaximum is a 3.1-only form; in 3.0 use a Boolean modifier paired with `maximum`', ) end - when :v3_1 - if value == true || value == false - violations << violation( - path: schema.object_reference, - message: 'Boolean exclusiveMaximum is a 3.0-only form; in 3.1 use a standalone numeric bound', - ) - end + elsif value == true || value == false + violations << violation( + path: schema.object_reference, + message: 'Boolean exclusiveMaximum is a 3.0-only form; in 3.1 use a standalone numeric bound', + ) end end violations diff --git a/lib/openapi_parser/spec_validator/rules/exclusive_minimum.rb b/lib/openapi_parser/spec_validator/rules/exclusive_minimum.rb index b1894dbb..5b54ad00 100644 --- a/lib/openapi_parser/spec_validator/rules/exclusive_minimum.rb +++ b/lib/openapi_parser/spec_validator/rules/exclusive_minimum.rb @@ -3,28 +3,25 @@ class SpecValidator module Rules class ExclusiveMinimum < Rule def check(root) - return [] if version == :unknown + return [] if version.nil? violations = [] each_schema(root) do |schema| value = schema.exclusiveMinimum next if value.nil? - case version - when :v3_0 + if version_before?('3.1') if value.is_a?(Numeric) violations << violation( path: schema.object_reference, message: 'numeric exclusiveMinimum is a 3.1-only form; in 3.0 use a Boolean modifier paired with `minimum`', ) end - when :v3_1 - if value == true || value == false - violations << violation( - path: schema.object_reference, - message: 'Boolean exclusiveMinimum is a 3.0-only form; in 3.1 use a standalone numeric bound', - ) - end + elsif value == true || value == false + violations << violation( + path: schema.object_reference, + message: 'Boolean exclusiveMinimum is a 3.0-only form; in 3.1 use a standalone numeric bound', + ) end end violations diff --git a/lib/openapi_parser/spec_validator/rules/json_schema_dialect_in_30.rb b/lib/openapi_parser/spec_validator/rules/json_schema_dialect_in_30.rb index c33c3c26..98cf5454 100644 --- a/lib/openapi_parser/spec_validator/rules/json_schema_dialect_in_30.rb +++ b/lib/openapi_parser/spec_validator/rules/json_schema_dialect_in_30.rb @@ -5,7 +5,7 @@ module Rules # equivalent. class JsonSchemaDialectIn30 < Rule def check(root) - return [] unless version == :v3_0 + return [] unless version_before?('3.1') return [] unless root.raw_schema.is_a?(Hash) && root.raw_schema.key?('jsonSchemaDialect') [violation( diff --git a/lib/openapi_parser/spec_validator/rules/nullable_deprecation.rb b/lib/openapi_parser/spec_validator/rules/nullable_deprecation.rb index 29f7fb3a..7900f31e 100644 --- a/lib/openapi_parser/spec_validator/rules/nullable_deprecation.rb +++ b/lib/openapi_parser/spec_validator/rules/nullable_deprecation.rb @@ -6,7 +6,7 @@ module Rules # is a spec violation, regardless of true/false. class NullableDeprecation < Rule def check(root) - return [] unless version == :v3_1 + return [] unless version_at_least?('3.1') violations = [] each_schema(root) do |schema| diff --git a/lib/openapi_parser/spec_validator/rules/path_items_in_30.rb b/lib/openapi_parser/spec_validator/rules/path_items_in_30.rb index 85374497..e8392649 100644 --- a/lib/openapi_parser/spec_validator/rules/path_items_in_30.rb +++ b/lib/openapi_parser/spec_validator/rules/path_items_in_30.rb @@ -6,7 +6,7 @@ module Rules # mismatch. class PathItemsIn30 < Rule def check(root) - return [] unless version == :v3_0 + return [] unless version_before?('3.1') components = root.components return [] unless components diff --git a/lib/openapi_parser/spec_validator/rules/prefix_items_in_30.rb b/lib/openapi_parser/spec_validator/rules/prefix_items_in_30.rb index f0791c47..6896efca 100644 --- a/lib/openapi_parser/spec_validator/rules/prefix_items_in_30.rb +++ b/lib/openapi_parser/spec_validator/rules/prefix_items_in_30.rb @@ -6,7 +6,7 @@ module Rules # mismatch the validator should report. class PrefixItemsIn30 < Rule def check(root) - return [] unless version == :v3_0 + return [] unless version_before?('3.1') violations = [] each_schema(root) do |schema| diff --git a/lib/openapi_parser/spec_validator/rules/type_array_in_30.rb b/lib/openapi_parser/spec_validator/rules/type_array_in_30.rb index 23dab065..d274532e 100644 --- a/lib/openapi_parser/spec_validator/rules/type_array_in_30.rb +++ b/lib/openapi_parser/spec_validator/rules/type_array_in_30.rb @@ -6,7 +6,7 @@ module Rules # array form on a 3.0 document is a spec violation. class TypeArrayIn30 < Rule def check(root) - return [] unless version == :v3_0 + return [] unless version_before?('3.1') violations = [] each_schema(root) do |schema| diff --git a/lib/openapi_parser/spec_validator/rules/type_null_in_30.rb b/lib/openapi_parser/spec_validator/rules/type_null_in_30.rb index 43b86317..7ac4cb34 100644 --- a/lib/openapi_parser/spec_validator/rules/type_null_in_30.rb +++ b/lib/openapi_parser/spec_validator/rules/type_null_in_30.rb @@ -7,7 +7,7 @@ module Rules # `["string", "null"]` are covered by a separate rule. class TypeNullIn30 < Rule def check(root) - return [] unless version == :v3_0 + return [] unless version_before?('3.1') violations = [] each_schema(root) do |schema| diff --git a/lib/openapi_parser/spec_validator/rules/webhooks_in_30.rb b/lib/openapi_parser/spec_validator/rules/webhooks_in_30.rb index c4c03508..a91b8e59 100644 --- a/lib/openapi_parser/spec_validator/rules/webhooks_in_30.rb +++ b/lib/openapi_parser/spec_validator/rules/webhooks_in_30.rb @@ -4,7 +4,7 @@ module Rules # `webhooks` is a 3.1 root-level addition; 3.0 has no equivalent. class WebhooksIn30 < Rule def check(root) - return [] unless version == :v3_0 + return [] unless version_before?('3.1') return [] unless root.raw_schema.is_a?(Hash) && root.raw_schema.key?('webhooks') [violation( diff --git a/sig/openapi_parser.rbs b/sig/openapi_parser.rbs index b422c58b..e93db499 100644 --- a/sig/openapi_parser.rbs +++ b/sig/openapi_parser.rbs @@ -15,7 +15,7 @@ module OpenAPIParser class OpenAPI def initialize: (Hash[bot, bot] hash, untyped config, uri: OpenAPIParser::readable_uri?, schema_registry: Hash[bot, bot]) -> OpenAPIParser::Schemas::OpenAPI def openapi: () -> String? - def openapi_version: () -> (:v3_0 | :v3_1 | :unknown) + def openapi_version: () -> Gem::Version? end end end diff --git a/sig/openapi_parser/spec_validator.rbs b/sig/openapi_parser/spec_validator.rbs index 2f62b13a..9f20df44 100644 --- a/sig/openapi_parser/spec_validator.rbs +++ b/sig/openapi_parser/spec_validator.rbs @@ -9,7 +9,7 @@ module OpenAPIParser class SpecValidator @root: OpenAPIParser::Schemas::OpenAPI - @version: (:v3_0 | :v3_1 | :unknown) + @version: Gem::Version? def self.run: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecViolation] def self.run!: (OpenAPIParser::Schemas::OpenAPI root, policy: (:silent | :warn | :raise)) -> void @@ -25,12 +25,14 @@ module OpenAPIParser end class Rule - @version: (:v3_0 | :v3_1 | :unknown) - attr_reader version: (:v3_0 | :v3_1 | :unknown) + @version: Gem::Version? + attr_reader version: Gem::Version? def self.rule_name: () -> Symbol - def initialize: ((:v3_0 | :v3_1 | :unknown) version) -> void + def initialize: (Gem::Version? version) -> void def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecViolation] + def version_before?: (String boundary) -> bool + def version_at_least?: (String boundary) -> bool private def violation: (path: String, message: String) -> SpecViolation private def each_schema: (untyped root) ?{ (untyped) -> void } -> untyped private def walk: (untyped node, Hash[Integer, bool] visited) { (untyped) -> void } -> void diff --git a/spec/openapi_parser/schemas/open_api_spec.rb b/spec/openapi_parser/schemas/open_api_spec.rb index 83e806d0..4360e44a 100644 --- a/spec/openapi_parser/schemas/open_api_spec.rb +++ b/spec/openapi_parser/schemas/open_api_spec.rb @@ -30,50 +30,62 @@ def parse_with_openapi_field(value, present: true) end context 'with a typical 3.0.x version like "3.0.0"' do - it 'returns :v3_0' do - expect(parse_with_openapi_field('3.0.0').openapi_version).to eq :v3_0 + it 'returns Gem::Version 3.0.0' do + expect(parse_with_openapi_field('3.0.0').openapi_version).to eq Gem::Version.new('3.0.0') end end context 'with a typical 3.1.x version like "3.1.0"' do - it 'returns :v3_1' do - expect(parse_with_openapi_field('3.1.0').openapi_version).to eq :v3_1 + it 'returns Gem::Version 3.1.0' do + expect(parse_with_openapi_field('3.1.0').openapi_version).to eq Gem::Version.new('3.1.0') end end - context 'with a minor-only version "3.0"' do - it 'returns :v3_0' do - expect(parse_with_openapi_field('3.0').openapi_version).to eq :v3_0 + context 'with a 3.2.x version like "3.2.0"' do + it 'returns Gem::Version 3.2.0' do + expect(parse_with_openapi_field('3.2.0').openapi_version).to eq Gem::Version.new('3.2.0') end end context 'with a minor-only version "3.1"' do - it 'returns :v3_1' do - expect(parse_with_openapi_field('3.1').openapi_version).to eq :v3_1 + it 'returns a version equal to 3.1.0' do + expect(parse_with_openapi_field('3.1').openapi_version).to eq Gem::Version.new('3.1.0') end end - context 'with a prerelease tag like "3.0.0-rc1"' do - it 'returns :v3_0 by prefix match' do - expect(parse_with_openapi_field('3.0.0-rc1').openapi_version).to eq :v3_0 + context 'with a prerelease tag like "3.1.0-rc1"' do + it 'returns the release version 3.1.0' do + expect(parse_with_openapi_field('3.1.0-rc1').openapi_version).to eq Gem::Version.new('3.1.0') end end - context 'with an unknown major version like "4.0.0"' do - it 'returns :unknown' do - expect(parse_with_openapi_field('4.0.0').openapi_version).to eq :unknown + context 'with a major version beyond 3 like "4.0.0"' do + it 'returns Gem::Version 4.0.0 without special-casing it' do + expect(parse_with_openapi_field('4.0.0').openapi_version).to eq Gem::Version.new('4.0.0') end end context 'when the openapi field is missing' do - it 'returns :unknown' do - expect(parse_with_openapi_field(nil, present: false).openapi_version).to eq :unknown + it 'returns nil' do + expect(parse_with_openapi_field(nil, present: false).openapi_version).to be_nil end end context 'with a non-string openapi field' do - it 'returns :unknown' do - expect(parse_with_openapi_field(31).openapi_version).to eq :unknown + it 'returns nil' do + expect(parse_with_openapi_field(31).openapi_version).to be_nil + end + end + + context 'with a string that is not a version like "three"' do + it 'returns nil' do + expect(parse_with_openapi_field('three').openapi_version).to be_nil + end + end + + context 'with a major-only version "3"' do + it 'returns nil (OpenAPI versions are at least major.minor)' do + expect(parse_with_openapi_field('3').openapi_version).to be_nil end end end diff --git a/spec/openapi_parser/spec_validator/rule_spec.rb b/spec/openapi_parser/spec_validator/rule_spec.rb new file mode 100644 index 00000000..90f7302c --- /dev/null +++ b/spec/openapi_parser/spec_validator/rule_spec.rb @@ -0,0 +1,66 @@ +require_relative '../../spec_helper' + +RSpec.describe OpenAPIParser::SpecValidator::Rule do + def rule_for(version_string) + version = version_string && Gem::Version.new(version_string) + OpenAPIParser::SpecValidator::Rule.new(version) + end + + describe '#version_before?' do + context 'with a 3.0.0 document and a 3.1 boundary' do + it 'returns true' do + expect(rule_for('3.0.0').version_before?('3.1')).to be true + end + end + + context 'with a 3.1.0 document and a 3.1 boundary' do + it 'returns false (the boundary itself is not before)' do + expect(rule_for('3.1.0').version_before?('3.1')).to be false + end + end + + context 'with a 3.2.0 document and a 3.1 boundary' do + it 'returns false' do + expect(rule_for('3.2.0').version_before?('3.1')).to be false + end + end + + context 'with a 3.0.3 patch version and a 3.1 boundary' do + it 'returns true' do + expect(rule_for('3.0.3').version_before?('3.1')).to be true + end + end + + context 'with an unknown (nil) version' do + it 'returns false' do + expect(rule_for(nil).version_before?('3.1')).to be false + end + end + end + + describe '#version_at_least?' do + context 'with a 3.1.0 document and a 3.1 boundary' do + it 'returns true (the boundary itself counts)' do + expect(rule_for('3.1.0').version_at_least?('3.1')).to be true + end + end + + context 'with a 3.2.0 document and a 3.1 boundary' do + it 'returns true' do + expect(rule_for('3.2.0').version_at_least?('3.1')).to be true + end + end + + context 'with a 3.0.0 document and a 3.1 boundary' do + it 'returns false' do + expect(rule_for('3.0.0').version_at_least?('3.1')).to be false + end + end + + context 'with an unknown (nil) version' do + it 'returns false' do + expect(rule_for(nil).version_at_least?('3.1')).to be false + end + end + end +end diff --git a/spec/openapi_parser/spec_validator/rules/const_in_30_spec.rb b/spec/openapi_parser/spec_validator/rules/const_in_30_spec.rb index fb0ad906..5992dffc 100644 --- a/spec/openapi_parser/spec_validator/rules/const_in_30_spec.rb +++ b/spec/openapi_parser/spec_validator/rules/const_in_30_spec.rb @@ -55,9 +55,9 @@ def run_rule_for(root) end end - context 'with an :unknown version document' do + context 'with a document whose openapi field is not a version' do it 'reports no violation (rule skipped)' do - root = doc_with_const('4.0.0') + root = doc_with_const('not-a-version') expect(run_rule_for(root)).to eq [] end end diff --git a/spec/openapi_parser/spec_validator/rules/content_encoding_in_30_spec.rb b/spec/openapi_parser/spec_validator/rules/content_encoding_in_30_spec.rb index ba675511..217bdaa2 100644 --- a/spec/openapi_parser/spec_validator/rules/content_encoding_in_30_spec.rb +++ b/spec/openapi_parser/spec_validator/rules/content_encoding_in_30_spec.rb @@ -55,9 +55,9 @@ def run_rule_for(root) end end - context 'with an :unknown version document' do + context 'with a document whose openapi field is not a version' do it 'reports no violation (rule skipped)' do - root = doc_with_content_encoding('4.0.0') + root = doc_with_content_encoding('not-a-version') expect(run_rule_for(root)).to eq [] end end diff --git a/spec/openapi_parser/spec_validator/rules/content_media_type_in_30_spec.rb b/spec/openapi_parser/spec_validator/rules/content_media_type_in_30_spec.rb index 6f4526bc..d2dd8da3 100644 --- a/spec/openapi_parser/spec_validator/rules/content_media_type_in_30_spec.rb +++ b/spec/openapi_parser/spec_validator/rules/content_media_type_in_30_spec.rb @@ -55,9 +55,9 @@ def run_rule_for(root) end end - context 'with an :unknown version document' do + context 'with a document whose openapi field is not a version' do it 'reports no violation (rule skipped)' do - root = doc_with_content_media_type('4.0.0') + root = doc_with_content_media_type('not-a-version') expect(run_rule_for(root)).to eq [] end end diff --git a/spec/openapi_parser/spec_validator/rules/content_schema_in_30_spec.rb b/spec/openapi_parser/spec_validator/rules/content_schema_in_30_spec.rb index 7435b230..7b32ff55 100644 --- a/spec/openapi_parser/spec_validator/rules/content_schema_in_30_spec.rb +++ b/spec/openapi_parser/spec_validator/rules/content_schema_in_30_spec.rb @@ -61,9 +61,9 @@ def run_rule_for(root) end end - context 'with an :unknown version document' do + context 'with a document whose openapi field is not a version' do it 'reports no violation (rule skipped)' do - root = doc_with_content_schema('4.0.0') + root = doc_with_content_schema('not-a-version') expect(run_rule_for(root)).to eq [] end end diff --git a/spec/openapi_parser/spec_validator/rules/dynamic_anchor_in_30_spec.rb b/spec/openapi_parser/spec_validator/rules/dynamic_anchor_in_30_spec.rb index 10c6707a..9d7411c3 100644 --- a/spec/openapi_parser/spec_validator/rules/dynamic_anchor_in_30_spec.rb +++ b/spec/openapi_parser/spec_validator/rules/dynamic_anchor_in_30_spec.rb @@ -55,9 +55,9 @@ def run_rule_for(root) end end - context 'with an :unknown version document' do + context 'with a document whose openapi field is not a version' do it 'reports no violation (rule skipped)' do - root = doc_with_dynamic_anchor('4.0.0') + root = doc_with_dynamic_anchor('not-a-version') expect(run_rule_for(root)).to eq [] end end diff --git a/spec/openapi_parser/spec_validator/rules/dynamic_ref_in_30_spec.rb b/spec/openapi_parser/spec_validator/rules/dynamic_ref_in_30_spec.rb index c9949b4e..08d8e3d4 100644 --- a/spec/openapi_parser/spec_validator/rules/dynamic_ref_in_30_spec.rb +++ b/spec/openapi_parser/spec_validator/rules/dynamic_ref_in_30_spec.rb @@ -55,9 +55,9 @@ def run_rule_for(root) end end - context 'with an :unknown version document' do + context 'with a document whose openapi field is not a version' do it 'reports no violation (rule skipped)' do - root = doc_with_dynamic_ref('4.0.0') + root = doc_with_dynamic_ref('not-a-version') expect(run_rule_for(root)).to eq [] end end diff --git a/spec/openapi_parser/spec_validator/rules/example_singular_deprecation_spec.rb b/spec/openapi_parser/spec_validator/rules/example_singular_deprecation_spec.rb index 0c9c7742..2d82ae3f 100644 --- a/spec/openapi_parser/spec_validator/rules/example_singular_deprecation_spec.rb +++ b/spec/openapi_parser/spec_validator/rules/example_singular_deprecation_spec.rb @@ -68,9 +68,16 @@ def run_rule_for(root) end end - context 'with an :unknown version document' do + context 'with a 3.2 document using singular example on a Schema' do + it 'reports one violation (the deprecation carries over from 3.1)' do + root = doc_with_example('3.2.0') + expect(run_rule_for(root).size).to eq 1 + end + end + + context 'with a document whose openapi field is not a version' do it 'reports no violation (rule skipped)' do - root = doc_with_example('4.0.0') + root = doc_with_example('not-a-version') expect(run_rule_for(root)).to eq [] end end diff --git a/spec/openapi_parser/spec_validator/rules/exclusive_maximum_spec.rb b/spec/openapi_parser/spec_validator/rules/exclusive_maximum_spec.rb index 5b35124c..88034783 100644 --- a/spec/openapi_parser/spec_validator/rules/exclusive_maximum_spec.rb +++ b/spec/openapi_parser/spec_validator/rules/exclusive_maximum_spec.rb @@ -51,9 +51,25 @@ def run_rule_for(root) end end - context 'with an :unknown version document containing exclusiveMaximum' do + context 'with a 3.2 document using a 3.1-style numeric exclusiveMaximum' do + it 'reports no violation' do + root = schema_with_exclusive_maximum('3.2.0', 5) + expect(run_rule_for(root)).to eq [] + end + end + + context 'with a 3.2 document using a 3.0-style boolean exclusiveMaximum' do + it 'reports one violation (3.2 keeps the 3.1 form)' do + root = schema_with_exclusive_maximum('3.2.0', true, maximum_value: 5) + violations = run_rule_for(root) + expect(violations.size).to eq 1 + expect(violations.first.message).to include('Boolean exclusiveMaximum') + end + end + + context 'with a document whose openapi field is not a version' do it 'reports no violation (rule skipped)' do - root = schema_with_exclusive_maximum('4.0.0', 5) + root = schema_with_exclusive_maximum('not-a-version', 5) expect(run_rule_for(root)).to eq [] end end diff --git a/spec/openapi_parser/spec_validator/rules/exclusive_minimum_spec.rb b/spec/openapi_parser/spec_validator/rules/exclusive_minimum_spec.rb index 7e44b455..f1e985d2 100644 --- a/spec/openapi_parser/spec_validator/rules/exclusive_minimum_spec.rb +++ b/spec/openapi_parser/spec_validator/rules/exclusive_minimum_spec.rb @@ -52,9 +52,25 @@ def run_rule_for(root) end end - context 'with an :unknown version document containing exclusiveMinimum' do + context 'with a 3.2 document using a 3.1-style numeric exclusiveMinimum' do + it 'reports no violation' do + root = schema_with_exclusive_minimum('3.2.0', 5) + expect(run_rule_for(root)).to eq [] + end + end + + context 'with a 3.2 document using a 3.0-style boolean exclusiveMinimum' do + it 'reports one violation (3.2 keeps the 3.1 form)' do + root = schema_with_exclusive_minimum('3.2.0', true, minimum_value: 5) + violations = run_rule_for(root) + expect(violations.size).to eq 1 + expect(violations.first.message).to include('Boolean exclusiveMinimum') + end + end + + context 'with a document whose openapi field is not a version' do it 'reports no violation (rule skipped)' do - root = schema_with_exclusive_minimum('4.0.0', 5) + root = schema_with_exclusive_minimum('not-a-version', 5) expect(run_rule_for(root)).to eq [] end end diff --git a/spec/openapi_parser/spec_validator/rules/json_schema_dialect_in_30_spec.rb b/spec/openapi_parser/spec_validator/rules/json_schema_dialect_in_30_spec.rb index 47396317..bff8b3b7 100644 --- a/spec/openapi_parser/spec_validator/rules/json_schema_dialect_in_30_spec.rb +++ b/spec/openapi_parser/spec_validator/rules/json_schema_dialect_in_30_spec.rb @@ -54,9 +54,9 @@ def run_rule_for(root) end end - context 'with an :unknown version document' do + context 'with a document whose openapi field is not a version' do it 'reports no violation (rule skipped)' do - root = doc_with_dialect('4.0.0') + root = doc_with_dialect('not-a-version') expect(run_rule_for(root)).to eq [] end end diff --git a/spec/openapi_parser/spec_validator/rules/nullable_deprecation_spec.rb b/spec/openapi_parser/spec_validator/rules/nullable_deprecation_spec.rb index f0d59741..74a27e23 100644 --- a/spec/openapi_parser/spec_validator/rules/nullable_deprecation_spec.rb +++ b/spec/openapi_parser/spec_validator/rules/nullable_deprecation_spec.rb @@ -57,9 +57,16 @@ def run_rule_for(root) end end - context 'with an :unknown version document' do + context 'with a 3.2 document using nullable: true' do + it 'reports one violation (nullable stays removed after 3.1)' do + root = schema_with_nullable('3.2.0', true) + expect(run_rule_for(root).size).to eq 1 + end + end + + context 'with a document whose openapi field is not a version' do it 'reports no violation (rule skipped)' do - root = schema_with_nullable('4.0.0', true) + root = schema_with_nullable('not-a-version', true) expect(run_rule_for(root)).to eq [] end end diff --git a/spec/openapi_parser/spec_validator/rules/path_items_in_30_spec.rb b/spec/openapi_parser/spec_validator/rules/path_items_in_30_spec.rb index 975b2c2d..7497adc7 100644 --- a/spec/openapi_parser/spec_validator/rules/path_items_in_30_spec.rb +++ b/spec/openapi_parser/spec_validator/rules/path_items_in_30_spec.rb @@ -63,9 +63,9 @@ def run_rule_for(root) end end - context 'with an :unknown version document' do + context 'with a document whose openapi field is not a version' do it 'reports no violation (rule skipped)' do - root = doc_with_path_items('4.0.0') + root = doc_with_path_items('not-a-version') expect(run_rule_for(root)).to eq [] end end diff --git a/spec/openapi_parser/spec_validator/rules/prefix_items_in_30_spec.rb b/spec/openapi_parser/spec_validator/rules/prefix_items_in_30_spec.rb index 98d23f53..f7540c02 100644 --- a/spec/openapi_parser/spec_validator/rules/prefix_items_in_30_spec.rb +++ b/spec/openapi_parser/spec_validator/rules/prefix_items_in_30_spec.rb @@ -64,9 +64,9 @@ def run_rule_for(root) end end - context 'with an :unknown version document' do + context 'with a document whose openapi field is not a version' do it 'reports no violation (rule skipped)' do - root = doc_with_prefix_items('4.0.0') + root = doc_with_prefix_items('not-a-version') expect(run_rule_for(root)).to eq [] end end diff --git a/spec/openapi_parser/spec_validator/rules/type_array_in_30_spec.rb b/spec/openapi_parser/spec_validator/rules/type_array_in_30_spec.rb index 61acccbc..5420993f 100644 --- a/spec/openapi_parser/spec_validator/rules/type_array_in_30_spec.rb +++ b/spec/openapi_parser/spec_validator/rules/type_array_in_30_spec.rb @@ -55,9 +55,9 @@ def run_rule_for(root) end end - context 'with an :unknown version document' do + context 'with a document whose openapi field is not a version' do it 'reports no violation (rule skipped)' do - root = doc_with_type_array('4.0.0') + root = doc_with_type_array('not-a-version') expect(run_rule_for(root)).to eq [] end end diff --git a/spec/openapi_parser/spec_validator/rules/type_null_in_30_spec.rb b/spec/openapi_parser/spec_validator/rules/type_null_in_30_spec.rb index e4b8d822..1f41f7e9 100644 --- a/spec/openapi_parser/spec_validator/rules/type_null_in_30_spec.rb +++ b/spec/openapi_parser/spec_validator/rules/type_null_in_30_spec.rb @@ -55,9 +55,9 @@ def run_rule_for(root) end end - context 'with an :unknown version document' do + context 'with a document whose openapi field is not a version' do it 'reports no violation (rule skipped)' do - root = doc_with_type_null('4.0.0') + root = doc_with_type_null('not-a-version') expect(run_rule_for(root)).to eq [] end end diff --git a/spec/openapi_parser/spec_validator/rules/webhooks_in_30_spec.rb b/spec/openapi_parser/spec_validator/rules/webhooks_in_30_spec.rb index a8365861..b6b25607 100644 --- a/spec/openapi_parser/spec_validator/rules/webhooks_in_30_spec.rb +++ b/spec/openapi_parser/spec_validator/rules/webhooks_in_30_spec.rb @@ -56,9 +56,9 @@ def run_rule_for(root) end end - context 'with an :unknown version document declaring webhooks' do + context 'with a document whose openapi field is not a version declaring webhooks' do it 'reports no violation (rule skipped)' do - root = doc_with_webhooks('4.0.0') + root = doc_with_webhooks('not-a-version') expect(run_rule_for(root)).to eq [] end end From 27935698ef6a1d0386f8cc4d735ab9738cac1b1d Mon Sep 17 00:00:00 2001 From: jonathan schatz Date: Thu, 17 Sep 2026 18:29:54 -0700 Subject: [PATCH 2/2] Bump ruby to lowest supported version (3.3.12) --- .ruby-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ruby-version b/.ruby-version index fd2a0186..4d541eb2 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.1.0 +3.3.12