diff --git a/CHANGELOG.md b/CHANGELOG.md index 67e0171..ee4b3c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,9 @@ * support array-form `type` (3.1) in value validation * support root-level `jsonSchemaDialect` (OpenAPI 3.1) in the parse layer * support `prefixItems` (OpenAPI 3.1) with positional tuple validation +* support `contentMediaType` (OpenAPI 3.1) in the parse layer * add `SpecValidator` with `strict_specification_version` config (`:silent` / `:warn` / `:raise`) to detect version mismatches between declared OpenAPI version and actual field usage + * `ContentMediaTypeIn30`: detect `contentMediaType` usage in 3.0 documents (3.1 addition) * `PrefixItemsIn30`: detect `prefixItems` usage in 3.0 documents (3.1 addition) * `JsonSchemaDialectIn30`: detect root-level `jsonSchemaDialect` usage in 3.0 documents (3.1 addition) * `TypeArrayIn30`: detect array-form `type` usage in 3.0 documents (3.1 form) diff --git a/lib/openapi_parser/schemas/schema.rb b/lib/openapi_parser/schemas/schema.rb index 5436a3f..2a438f2 100644 --- a/lib/openapi_parser/schemas/schema.rb +++ b/lib/openapi_parser/schemas/schema.rb @@ -72,7 +72,8 @@ class Schema < Base :nullable, :example, :deprecated, - :const + :const, + :contentMediaType # @!attribute [r] read_only # @return [Boolean, nil] diff --git a/lib/openapi_parser/spec_validator.rb b/lib/openapi_parser/spec_validator.rb index f239dec..edc4963 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/content_media_type_in_30' require_relative 'spec_validator/rules/prefix_items_in_30' require_relative 'spec_validator/rules/json_schema_dialect_in_30' require_relative 'spec_validator/rules/type_array_in_30' @@ -61,6 +62,7 @@ def rules [ Rules::ExclusiveMinimum, Rules::ExclusiveMaximum, + Rules::ContentMediaTypeIn30, Rules::PrefixItemsIn30, Rules::JsonSchemaDialectIn30, Rules::TypeArrayIn30, 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 new file mode 100644 index 0000000..ba6908f --- /dev/null +++ b/lib/openapi_parser/spec_validator/rules/content_media_type_in_30.rb @@ -0,0 +1,25 @@ +module OpenAPIParser + class SpecValidator + module Rules + # `contentMediaType` is a JSON Schema 2020-12 annotation adopted by + # 3.1. It is metadata, so it has no runtime side-effects; we only + # report version mismatches. + class ContentMediaTypeIn30 < Rule + def check(root) + return [] unless version == :v3_0 + + violations = [] + each_schema(root) do |schema| + next unless schema.raw_schema.is_a?(Hash) && schema.raw_schema.key?('contentMediaType') + + violations << violation( + path: schema.object_reference, + message: '`contentMediaType` is a 3.1 addition (from JSON Schema 2020-12); 3.0 has no equivalent', + ) + end + violations + end + end + end + end +end diff --git a/sig/openapi_parser/spec_validator.rbs b/sig/openapi_parser/spec_validator.rbs index c634973..da58edd 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 ContentMediaTypeIn30 < Rule + def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation] + end + class PrefixItemsIn30 < Rule def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation] end diff --git a/spec/data/openapi_3_1/content_media_type_30.yaml b/spec/data/openapi_3_1/content_media_type_30.yaml new file mode 100644 index 0000000..dc53d1e --- /dev/null +++ b/spec/data/openapi_3_1/content_media_type_30.yaml @@ -0,0 +1,24 @@ +openapi: 3.0.3 +info: + title: Document Upload API + version: '1.0' +paths: + /documents: + post: + summary: Upload a document + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Attachment' + responses: + '201': + description: Created +components: + schemas: + Attachment: + type: string + # `contentMediaType` is a JSON Schema 2020-12 annotation adopted by 3.1; + # 3.0 has no equivalent, so its use on a 3.0 document is a spec + # violation. + contentMediaType: application/json diff --git a/spec/data/openapi_3_1/content_media_type_31.yaml b/spec/data/openapi_3_1/content_media_type_31.yaml new file mode 100644 index 0000000..ed683b5 --- /dev/null +++ b/spec/data/openapi_3_1/content_media_type_31.yaml @@ -0,0 +1,23 @@ +openapi: 3.1.0 +info: + title: Document Upload API + version: '1.0' +paths: + /documents: + post: + summary: Upload a document + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Attachment' + responses: + '201': + description: Created +components: + schemas: + Attachment: + type: string + # `contentMediaType` is legitimate under 3.1 (JSON Schema 2020-12), so + # no violation is expected here. + contentMediaType: application/json 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 53fb8f4..6800dff 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 'contentMediaType (JSON Schema 2020-12 annotation new in 3.1)' do + it 'warns on the version-mismatched document under :warn' do + expect_mismatch_warns('content_media_type_30.yaml', [:content_media_type_in30]) + end + + it 'raises SpecViolationError on the version-mismatched document under :raise' do + expect_mismatch_raises('content_media_type_30.yaml', [:content_media_type_in30]) + end + + it 'stays clean on the correctly-versioned document' do + expect_clean('content_media_type_31.yaml') + end + end + describe 'prefixItems (JSON Schema tuple keyword new in 3.1)' do it 'warns on the version-mismatched document under :warn' do expect_mismatch_warns('prefix_items_30.yaml', [:prefix_items_in30]) 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 new file mode 100644 index 0000000..6f4526b --- /dev/null +++ b/spec/openapi_parser/spec_validator/rules/content_media_type_in_30_spec.rb @@ -0,0 +1,64 @@ +require_relative '../../../spec_helper' + +RSpec.describe 'OpenAPIParser::SpecValidator::Rules::ContentMediaTypeIn30' do + def base_doc(openapi_version_string, sample_schema) + { + 'openapi' => openapi_version_string, + 'info' => { 'title' => 'test', 'version' => '1.0' }, + 'paths' => {}, + 'components' => { 'schemas' => { 'Sample' => sample_schema } }, + } + end + + def doc_with_content_media_type(openapi_version_string) + raw = base_doc(openapi_version_string, { 'type' => 'string', 'contentMediaType' => 'image/png' }) + OpenAPIParser.parse(raw, strict_reference_validation: false) + end + + def doc_without_content_media_type(openapi_version_string) + raw = base_doc(openapi_version_string, { 'type' => 'string' }) + OpenAPIParser.parse(raw, strict_reference_validation: false) + end + + def run_rule_for(root) + OpenAPIParser::SpecValidator::Rules::ContentMediaTypeIn30.new(root.openapi_version).check(root) + end + + context 'with a 3.1 document using contentMediaType' do + it 'reports no violation' do + root = doc_with_content_media_type('3.1.0') + expect(run_rule_for(root)).to eq [] + end + end + + context 'with a 3.1 document without contentMediaType' do + it 'reports no violation' do + root = doc_without_content_media_type('3.1.0') + expect(run_rule_for(root)).to eq [] + end + end + + context 'with a 3.0 document using contentMediaType' do + it 'reports one violation pointing at the offending schema' do + root = doc_with_content_media_type('3.0.0') + violations = run_rule_for(root) + expect(violations.size).to eq 1 + expect(violations.first.path).to eq '#/components/schemas/Sample' + expect(violations.first.rule_name).to eq :content_media_type_in30 + end + end + + context 'with a 3.0 document without contentMediaType' do + it 'reports no violation' do + root = doc_without_content_media_type('3.0.0') + expect(run_rule_for(root)).to eq [] + end + end + + context 'with an :unknown version document' do + it 'reports no violation (rule skipped)' do + root = doc_with_content_media_type('4.0.0') + expect(run_rule_for(root)).to eq [] + end + end +end