diff --git a/CHANGELOG.md b/CHANGELOG.md index 94fd9ca7..1033f67c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ * `AdditionalOperationsBefore32`: detect Path Item `additionalOperations` usage in pre-3.2 documents (3.2 addition) * `QuerystringBefore32`: detect Parameter `in: querystring` usage in pre-3.2 documents (3.2 addition; not validated at runtime) * `CookieStyleBefore32`: detect Parameter `style: cookie` usage in pre-3.2 documents (3.2 addition) + * `ResponseSummaryBefore32`: detect Response Object `summary` usage in pre-3.2 documents (3.2 addition) * validate a value against a schema without `type`: keywords such as `properties`, `required`, `items`, `maxLength`, `enum` and `minimum` now apply when the value has the type the keyword is for, as in JSON Schema. They were all skipped before, so a value that breaks them may now fail validation * add `allow_3_2_features` config to apply OpenAPI 3.2 runtime behavior to documents that declare an earlier version (default `false`: pre-3.2 documents behave as before) * support `components.mediaTypes` (OpenAPI 3.2), resolving `$ref`s in request body and response `content` in 3.2 documents; an unresolved one accepts any body unless `strict_reference_validation` is set @@ -20,6 +21,7 @@ * support Discriminator `defaultMapping` (OpenAPI 3.2): fallback schema when the discriminator property is absent or its value has no explicit or implicit mapping; honored in 3.2 documents * support the `query` HTTP method and `additionalOperations` (OpenAPI 3.2) on Path Items, including request-operation lookup, in 3.2 documents * match HTTP methods case-insensitively in request-operation lookup in 3.2 documents (`request_operation("GET", path)` still returns nil before 3.2) +* support Response Object `summary` (OpenAPI 3.2) in the parse layer ## 2.5.0 (2026-10-21) * add OpenAPI 3.2 `SpecValidator` rules diff --git a/lib/openapi_parser/schemas/response.rb b/lib/openapi_parser/schemas/response.rb index c203c995..e17c9f99 100644 --- a/lib/openapi_parser/schemas/response.rb +++ b/lib/openapi_parser/schemas/response.rb @@ -6,6 +6,10 @@ class Response < Base openapi_attr_values :description + # @!attribute [r] summary + # @return [String, nil] short summary of the response (OpenAPI 3.2+) + openapi_attr_value :summary + # @!attribute [r] content # @return [Hash{String => MediaType}, nil] content_type to MediaType hash openapi_attr_hash_object :content, MediaType, reference: OpenAPIParser::MediaTypeSelectable::CONTENT_REFERENCE diff --git a/lib/openapi_parser/spec_validator.rb b/lib/openapi_parser/spec_validator.rb index 129b9624..72ca0310 100644 --- a/lib/openapi_parser/spec_validator.rb +++ b/lib/openapi_parser/spec_validator.rb @@ -32,6 +32,7 @@ require_relative 'spec_validator/rules/additional_operations_before_32' require_relative 'spec_validator/rules/querystring_before_32' require_relative 'spec_validator/rules/cookie_style_before_32' +require_relative 'spec_validator/rules/response_summary_before_32' module OpenAPIParser class SpecViolationError < OpenAPIError @@ -111,6 +112,7 @@ def rules Rules::AdditionalOperationsBefore32, Rules::QuerystringBefore32, Rules::CookieStyleBefore32, + Rules::ResponseSummaryBefore32, ] end end diff --git a/lib/openapi_parser/spec_validator/rules/response_summary_before_32.rb b/lib/openapi_parser/spec_validator/rules/response_summary_before_32.rb new file mode 100644 index 00000000..f3573f6a --- /dev/null +++ b/lib/openapi_parser/spec_validator/rules/response_summary_before_32.rb @@ -0,0 +1,25 @@ +module OpenAPIParser + class SpecValidator + module Rules + # 3.2 adds `summary` to the Response Object. The parse layer accepts it + # permissively; this rule reports the version mismatch. + class ResponseSummaryBefore32 < Rule + def check(root) + return [] unless version_before?('3.2') + + violations = [] + each_node(root, OpenAPIParser::Schemas::Response) do |node| + raw = node.raw_schema + next unless raw.is_a?(Hash) && raw.key?('summary') + + violations << violation( + path: "#{node.object_reference}/summary", + message: '`summary` on a Response Object is a 3.2 addition; earlier documents have no such field', + ) + end + violations + end + end + end + end +end diff --git a/sig/openapi_parser/spec_validator.rbs b/sig/openapi_parser/spec_validator.rbs index 2c635479..23ac6376 100644 --- a/sig/openapi_parser/spec_validator.rbs +++ b/sig/openapi_parser/spec_validator.rbs @@ -175,6 +175,10 @@ module OpenAPIParser class CookieStyleBefore32 < Rule def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation] end + + class ResponseSummaryBefore32 < Rule + def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation] + end end end diff --git a/spec/data/openapi_3_2/response_summary_31.yaml b/spec/data/openapi_3_2/response_summary_31.yaml new file mode 100644 index 00000000..19e32932 --- /dev/null +++ b/spec/data/openapi_3_2/response_summary_31.yaml @@ -0,0 +1,14 @@ +openapi: 3.1.0 +info: + title: Pet API + version: '1.0' +paths: + /pets: + get: + summary: List pets + responses: + '200': + # `summary` on a Response Object is a 3.2 addition; a 3.1 document + # has no such field, so its use here is a spec violation. + summary: Pets + description: OK diff --git a/spec/data/openapi_3_2/response_summary_32.yaml b/spec/data/openapi_3_2/response_summary_32.yaml new file mode 100644 index 00000000..7792516a --- /dev/null +++ b/spec/data/openapi_3_2/response_summary_32.yaml @@ -0,0 +1,12 @@ +openapi: 3.2.0 +info: + title: Pet API + version: '1.0' +paths: + /pets: + get: + summary: List pets + responses: + '200': + summary: Pets + description: OK diff --git a/spec/openapi_parser/schemas/response_spec.rb b/spec/openapi_parser/schemas/response_spec.rb index 1c4f1165..74a21985 100644 --- a/spec/openapi_parser/schemas/response_spec.rb +++ b/spec/openapi_parser/schemas/response_spec.rb @@ -23,4 +23,13 @@ expect(subject.content['application/json'].object_reference).to eq '#/paths/~1pets/get/responses/default/content/application~1json' end end + + describe 'summary (OpenAPI 3.2)' do + let(:root) { OpenAPIParser.parse(load_yaml_file('./spec/data/openapi_3_2/response_summary_32.yaml'), {}) } + + it 'is parsed onto the Response object' do + response = root.paths.path['/pets'].get.responses.response['200'] + expect(response.summary).to eq 'Pets' + end + end end diff --git a/spec/openapi_parser/spec_validator/integration_3_2_spec.rb b/spec/openapi_parser/spec_validator/integration_3_2_spec.rb index 33c3d754..39b771a6 100644 --- a/spec/openapi_parser/spec_validator/integration_3_2_spec.rb +++ b/spec/openapi_parser/spec_validator/integration_3_2_spec.rb @@ -234,4 +234,18 @@ def expect_clean(file) expect_clean('cookie_style_32.yaml') end end + + describe 'Response Object summary (3.2 addition)' do + it 'warns on the version-mismatched document under :warn' do + expect_mismatch_warns('response_summary_31.yaml', [:response_summary_before32]) + end + + it 'raises SpecViolationError on the version-mismatched document under :raise' do + expect_mismatch_raises('response_summary_31.yaml', [:response_summary_before32]) + end + + it 'stays clean on the correctly-versioned document' do + expect_clean('response_summary_32.yaml') + end + end end diff --git a/spec/openapi_parser/spec_validator/rules/response_summary_before_32_spec.rb b/spec/openapi_parser/spec_validator/rules/response_summary_before_32_spec.rb new file mode 100644 index 00000000..7e34748b --- /dev/null +++ b/spec/openapi_parser/spec_validator/rules/response_summary_before_32_spec.rb @@ -0,0 +1,68 @@ +require_relative '../../../spec_helper' + +RSpec.describe 'OpenAPIParser::SpecValidator::Rules::ResponseSummaryBefore32' do + def base_doc(openapi_version_string, resp) + { + 'openapi' => openapi_version_string, + 'info' => { 'title' => 'test', 'version' => '1.0' }, + 'paths' => { + '/pets' => { + 'get' => { + 'responses' => { '200' => resp }, + }, + }, + }, + } + end + + def doc_with(openapi_version_string) + OpenAPIParser.parse(base_doc(openapi_version_string, { 'summary' => 'Pets', 'description' => 'OK' }), strict_reference_validation: false) + end + + def doc_without(openapi_version_string) + OpenAPIParser.parse(base_doc(openapi_version_string, { 'description' => 'OK' }), strict_reference_validation: false) + end + + def run_rule_for(root) + OpenAPIParser::SpecValidator::Rules::ResponseSummaryBefore32.new(root.openapi_version).check(root) + end + + context 'with a 3.2 document using a response summary' do + it 'reports no violation' do + expect(run_rule_for(doc_with('3.2.0'))).to eq [] + end + end + + context 'with a 3.2 document without a response summary' do + it 'reports no violation' do + expect(run_rule_for(doc_without('3.2.0'))).to eq [] + end + end + + context 'with a 3.1 document using a response summary' do + it 'reports one violation pointing at the field' do + violations = run_rule_for(doc_with('3.1.0')) + expect(violations.size).to eq 1 + expect(violations.first.path).to eq '#/paths/~1pets/get/responses/200/summary' + expect(violations.first.rule_name).to eq :response_summary_before32 + end + end + + context 'with a 3.1 document without a response summary' do + it 'reports no violation' do + expect(run_rule_for(doc_without('3.1.0'))).to eq [] + end + end + + context 'with a 3.0 document using a response summary' do + it 'reports one violation' do + expect(run_rule_for(doc_with('3.0.0')).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 + expect(run_rule_for(doc_with('not-a-version'))).to eq [] + end + end +end