From 86b7d5dfdd28d874ff9f36814717067d3616ea00 Mon Sep 17 00:00:00 2001 From: fusagiko / takayamaki Date: Wed, 20 May 2026 01:37:43 +0900 Subject: [PATCH 1/4] Add pending specs for PathItemsIn30 rule (3.1 strategy PR7) --- .../rules/path_items_in_30_spec.rb | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 spec/openapi_parser/spec_validator/rules/path_items_in_30_spec.rb 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 new file mode 100644 index 0000000..99fe07d --- /dev/null +++ b/spec/openapi_parser/spec_validator/rules/path_items_in_30_spec.rb @@ -0,0 +1,39 @@ +require_relative '../../../spec_helper' + +RSpec.describe 'OpenAPIParser::SpecValidator::Rules::PathItemsIn30' do + def doc_with_path_items(openapi_version_string, include_path_items: true) + components = { 'schemas' => {} } + if include_path_items + components['pathItems'] = { + 'sample' => { 'get' => { 'responses' => { '200' => { 'description' => 'ok' } } } }, + } + end + raw = { + 'openapi' => openapi_version_string, + 'info' => { 'title' => 'test', 'version' => '1.0' }, + 'paths' => {}, + 'components' => components, + } + OpenAPIParser.parse(raw, strict_reference_validation: false) + end + + def run_rule_for(root) + OpenAPIParser::SpecValidator::Rules::PathItemsIn30.new(root.openapi_version).check(root) + end + + context 'with a 3.1 document using components.pathItems' do + it 'reports no violation' + end + + context 'with a 3.0 document using components.pathItems' do + it 'reports one violation pointing at #/components/pathItems' + end + + context 'with a 3.0 document without components.pathItems' do + it 'reports no violation' + end + + context 'with an :unknown version document using components.pathItems' do + it 'reports no violation (rule skipped)' + end +end From b3cb0b0a798120101ca3c9b9974c9d1e6355c69d Mon Sep 17 00:00:00 2001 From: fusagiko / takayamaki Date: Wed, 20 May 2026 01:38:33 +0900 Subject: [PATCH 2/4] PathItemsIn30 rule for components.pathItems on 3.0 docs (3.1 strategy PR7) The parse layer keeps accepting Components.pathItems in any version (geemus suggested permissive parsing); this rule reports the 3.0 mismatch so callers on :warn / :raise can surface it. Detection inspects components.raw_schema directly to catch an explicitly empty `pathItems: {}` literal too. --- lib/openapi_parser/spec_validator.rb | 2 ++ .../spec_validator/rules/path_items_in_30.rb | 25 +++++++++++++++++++ sig/openapi_parser/spec_validator.rbs | 4 +++ .../rules/path_items_in_30_spec.rb | 23 ++++++++++++++--- 4 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 lib/openapi_parser/spec_validator/rules/path_items_in_30.rb diff --git a/lib/openapi_parser/spec_validator.rb b/lib/openapi_parser/spec_validator.rb index 2d92cf4..cf98cf8 100644 --- a/lib/openapi_parser/spec_validator.rb +++ b/lib/openapi_parser/spec_validator.rb @@ -2,6 +2,7 @@ require_relative 'spec_validator/rule' require_relative 'spec_validator/rules/exclusive_minimum' require_relative 'spec_validator/rules/exclusive_maximum' +require_relative 'spec_validator/rules/path_items_in_30' require_relative 'spec_validator/rules/nullable_deprecation' require_relative 'spec_validator/rules/example_singular_deprecation' @@ -53,6 +54,7 @@ def rules [ Rules::ExclusiveMinimum, Rules::ExclusiveMaximum, + Rules::PathItemsIn30, Rules::NullableDeprecation, Rules::ExampleSingularDeprecation, ] 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 new file mode 100644 index 0000000..8537449 --- /dev/null +++ b/lib/openapi_parser/spec_validator/rules/path_items_in_30.rb @@ -0,0 +1,25 @@ +module OpenAPIParser + class SpecValidator + module Rules + # `components.pathItems` is a 3.1 addition. The parse layer accepts it + # in 3.0 documents permissively; this rule reports the version + # mismatch. + class PathItemsIn30 < Rule + def check(root) + return [] unless version == :v3_0 + + components = root.components + return [] unless components + + raw = components.raw_schema + return [] unless raw.is_a?(Hash) && raw.key?('pathItems') + + [violation( + path: "#{components.object_reference}/pathItems", + message: '`components.pathItems` is a 3.1 addition; 3.0 documents should not declare it', + )] + end + end + end + end +end diff --git a/sig/openapi_parser/spec_validator.rbs b/sig/openapi_parser/spec_validator.rbs index 1c7fcb6..e094ebd 100644 --- a/sig/openapi_parser/spec_validator.rbs +++ b/sig/openapi_parser/spec_validator.rbs @@ -45,6 +45,10 @@ module OpenAPIParser def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation] end + class PathItemsIn30 < Rule + def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation] + end + class NullableDeprecation < Rule def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation] 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 99fe07d..6bc5037 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 @@ -22,18 +22,33 @@ def run_rule_for(root) end context 'with a 3.1 document using components.pathItems' do - it 'reports no violation' + it 'reports no violation' do + root = doc_with_path_items('3.1.0') + expect(run_rule_for(root)).to eq [] + end end context 'with a 3.0 document using components.pathItems' do - it 'reports one violation pointing at #/components/pathItems' + it 'reports one violation pointing at #/components/pathItems' do + root = doc_with_path_items('3.0.0') + violations = run_rule_for(root) + expect(violations.size).to eq 1 + expect(violations.first.path).to eq '#/components/pathItems' + expect(violations.first.rule_name).to eq :path_items_in30 + end end context 'with a 3.0 document without components.pathItems' do - it 'reports no violation' + it 'reports no violation' do + root = doc_with_path_items('3.0.0', include_path_items: false) + expect(run_rule_for(root)).to eq [] + end end context 'with an :unknown version document using components.pathItems' do - it 'reports no violation (rule skipped)' + it 'reports no violation (rule skipped)' do + root = doc_with_path_items('4.0.0') + expect(run_rule_for(root)).to eq [] + end end end From 993920e9bdadfc9317b0758c6d788207c57578d4 Mon Sep 17 00:00:00 2001 From: fusagiko / takayamaki Date: Sat, 30 May 2026 16:43:18 +0900 Subject: [PATCH 3/4] Integration test: components.pathItems new in 3.1 `components.pathItems` on a 3.0 document warns and raises (3.1 addition); the same field on a 3.1 document stays clean. --- spec/data/openapi_3_1/path_items_30.yaml | 21 +++++++++++++++++++ spec/data/openapi_3_1/path_items_31.yaml | 21 +++++++++++++++++++ .../spec_validator/integration_3_1_spec.rb | 14 +++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 spec/data/openapi_3_1/path_items_30.yaml create mode 100644 spec/data/openapi_3_1/path_items_31.yaml diff --git a/spec/data/openapi_3_1/path_items_30.yaml b/spec/data/openapi_3_1/path_items_30.yaml new file mode 100644 index 0000000..559af4f --- /dev/null +++ b/spec/data/openapi_3_1/path_items_30.yaml @@ -0,0 +1,21 @@ +openapi: 3.0.3 +info: + title: Reusable Path Items API + version: '1.0' +paths: + /ping: + get: + summary: Health check + responses: + '200': + description: OK +components: + # `components.pathItems` is a 3.1 addition; a 3.0 document should not + # declare it, so this is a spec violation. + pathItems: + SharedItem: + get: + summary: A reusable operation + responses: + '200': + description: OK diff --git a/spec/data/openapi_3_1/path_items_31.yaml b/spec/data/openapi_3_1/path_items_31.yaml new file mode 100644 index 0000000..631fcd7 --- /dev/null +++ b/spec/data/openapi_3_1/path_items_31.yaml @@ -0,0 +1,21 @@ +openapi: 3.1.0 +info: + title: Reusable Path Items API + version: '1.0' +paths: + /ping: + get: + summary: Health check + responses: + '200': + description: OK +components: + # `components.pathItems` is legitimate under 3.1, so no violation is + # expected here. + pathItems: + SharedItem: + get: + summary: A reusable operation + responses: + '200': + description: OK diff --git a/spec/openapi_parser/spec_validator/integration_3_1_spec.rb b/spec/openapi_parser/spec_validator/integration_3_1_spec.rb index 1d8ac7c..c1646d9 100644 --- a/spec/openapi_parser/spec_validator/integration_3_1_spec.rb +++ b/spec/openapi_parser/spec_validator/integration_3_1_spec.rb @@ -74,6 +74,20 @@ def expect_clean(file) end end + describe 'components.pathItems (3.1 addition)' do + it 'warns on the version-mismatched document under :warn' do + expect_mismatch_warns('path_items_30.yaml', [:path_items_in30]) + end + + it 'raises SpecViolationError on the version-mismatched document under :raise' do + expect_mismatch_raises('path_items_30.yaml', [:path_items_in30]) + end + + it 'stays clean on the correctly-versioned document' do + expect_clean('path_items_31.yaml') + end + end + describe 'nullable (3.0 keyword removed in 3.1)' do it 'warns on the version-mismatched document under :warn' do expect_mismatch_warns('nullable_31.yaml', [:nullable_deprecation]) From 3f592bef4777813a5ae794ca7999013e54ce5e56 Mon Sep 17 00:00:00 2001 From: fusagiko / takayamaki Date: Sat, 4 Jul 2026 10:41:05 +0900 Subject: [PATCH 4/4] Add CHANGELOG entries for PathItemsIn30 + rules from #193 / #195 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0481a52..d9c3666 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ ## Unreleased * support `components.pathItems` so `$ref`s into it resolve, unblocking OpenAPI 3.1 documents that use reusable path items * add `SpecValidator` with `strict_specification_version` config (`:silent` / `:warn` / `:raise`) to detect version mismatches between declared OpenAPI version and actual field usage + * `NullableDeprecation`: detect `nullable` usage in 3.1 documents (removed in 3.1) + * `ExampleSingularDeprecation`: detect singular `example` on schemas in 3.1 documents (deprecated in 3.1) + * `PathItemsIn30`: detect `components.pathItems` usage in 3.0 documents (3.1 addition) * `ExclusiveMinimum` / `ExclusiveMaximum`: detect 3.0 Boolean vs 3.1 numeric form mismatch * support 3.1-style numeric `exclusiveMinimum` / `exclusiveMaximum` in value validation (standalone bound, not a Boolean modifier on `minimum` / `maximum`)