diff --git a/lib/openapi_parser/spec_validator.rb b/lib/openapi_parser/spec_validator.rb index 89c6838..2d92cf4 100644 --- a/lib/openapi_parser/spec_validator.rb +++ b/lib/openapi_parser/spec_validator.rb @@ -3,6 +3,7 @@ require_relative 'spec_validator/rules/exclusive_minimum' require_relative 'spec_validator/rules/exclusive_maximum' require_relative 'spec_validator/rules/nullable_deprecation' +require_relative 'spec_validator/rules/example_singular_deprecation' module OpenAPIParser class SpecViolationError < OpenAPIError @@ -53,6 +54,7 @@ def rules Rules::ExclusiveMinimum, Rules::ExclusiveMaximum, Rules::NullableDeprecation, + Rules::ExampleSingularDeprecation, ] end end diff --git a/lib/openapi_parser/spec_validator/rules/example_singular_deprecation.rb b/lib/openapi_parser/spec_validator/rules/example_singular_deprecation.rb new file mode 100644 index 0000000..0cb8c69 --- /dev/null +++ b/lib/openapi_parser/spec_validator/rules/example_singular_deprecation.rb @@ -0,0 +1,25 @@ +module OpenAPIParser + class SpecValidator + module Rules + # In 3.1 the singular `example` keyword on a Schema is deprecated in + # favor of the JSON Schema `examples` array. The field is still + # allowed but discouraged, so we report it as a violation. + class ExampleSingularDeprecation < Rule + def check(root) + return [] unless version == :v3_1 + + violations = [] + each_schema(root) do |schema| + next unless schema.raw_schema.is_a?(Hash) && schema.raw_schema.key?('example') + + violations << violation( + path: schema.object_reference, + message: 'singular `example` on a Schema is deprecated in 3.1; use the `examples` array', + ) + end + violations + end + end + end + end +end diff --git a/sig/openapi_parser/spec_validator.rbs b/sig/openapi_parser/spec_validator.rbs index fea9a7b..1c7fcb6 100644 --- a/sig/openapi_parser/spec_validator.rbs +++ b/sig/openapi_parser/spec_validator.rbs @@ -48,6 +48,10 @@ module OpenAPIParser class NullableDeprecation < Rule def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation] end + + class ExampleSingularDeprecation < Rule + def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation] + end end end diff --git a/spec/data/openapi_3_1/example_keyword_30.yaml b/spec/data/openapi_3_1/example_keyword_30.yaml new file mode 100644 index 0000000..95c36bd --- /dev/null +++ b/spec/data/openapi_3_1/example_keyword_30.yaml @@ -0,0 +1,25 @@ +openapi: 3.0.3 +info: + title: Catalog API + version: '1.0' +paths: + /products: + get: + summary: List products + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Product' +components: + schemas: + Product: + type: object + properties: + # 3.0 form: the singular `example` keyword on a Schema is the standard + # way to attach a sample value in 3.0, so no violation is expected. + sku: + type: string + example: ABC-123 diff --git a/spec/data/openapi_3_1/example_keyword_31.yaml b/spec/data/openapi_3_1/example_keyword_31.yaml new file mode 100644 index 0000000..8f9c765 --- /dev/null +++ b/spec/data/openapi_3_1/example_keyword_31.yaml @@ -0,0 +1,26 @@ +openapi: 3.1.0 +info: + title: Catalog API + version: '1.0' +paths: + /products: + get: + summary: List products + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Product' +components: + schemas: + Product: + type: object + properties: + # 3.0 form leaking into a 3.1 document: the singular `example` is + # deprecated in 3.1 in favor of the `examples` array, so it is + # reported as a violation. + sku: + type: string + example: ABC-123 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 143c3d3..1d8ac7c 100644 --- a/spec/openapi_parser/spec_validator/integration_3_1_spec.rb +++ b/spec/openapi_parser/spec_validator/integration_3_1_spec.rb @@ -87,4 +87,18 @@ def expect_clean(file) expect_clean('nullable_30.yaml') end end + + describe 'singular example on a Schema (deprecated in 3.1)' do + it 'warns on the version-mismatched document under :warn' do + expect_mismatch_warns('example_keyword_31.yaml', [:example_singular_deprecation]) + end + + it 'raises SpecViolationError on the version-mismatched document under :raise' do + expect_mismatch_raises('example_keyword_31.yaml', [:example_singular_deprecation]) + end + + it 'stays clean on the correctly-versioned document' do + expect_clean('example_keyword_30.yaml') + end + 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 new file mode 100644 index 0000000..e4c4515 --- /dev/null +++ b/spec/openapi_parser/spec_validator/rules/example_singular_deprecation_spec.rb @@ -0,0 +1,56 @@ +require_relative '../../../spec_helper' + +RSpec.describe 'OpenAPIParser::SpecValidator::Rules::ExampleSingularDeprecation' do + def schema_with_example(openapi_version_string, schema_payload) + raw = { + 'openapi' => openapi_version_string, + 'info' => { 'title' => 'test', 'version' => '1.0' }, + 'paths' => {}, + 'components' => { 'schemas' => { 'Sample' => schema_payload } }, + } + OpenAPIParser.parse(raw, strict_reference_validation: false) + end + + def run_rule_for(root) + OpenAPIParser::SpecValidator::Rules::ExampleSingularDeprecation.new(root.openapi_version).check(root) + end + + context 'with a 3.0 document using singular example on a Schema' do + it 'reports no violation' do + root = schema_with_example('3.0.0', { 'type' => 'string', 'example' => 'sample' }) + expect(run_rule_for(root)).to eq [] + end + end + + context 'with a 3.1 document using singular example on a Schema' do + it 'reports one violation pointing at the offending schema' do + root = schema_with_example('3.1.0', { 'type' => 'string', 'example' => 'sample' }) + 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 :example_singular_deprecation + expect(violations.first.message).to include('deprecated in 3.1') + end + end + + context 'with a 3.1 document that does not use singular example' do + it 'reports no violation' do + root = schema_with_example('3.1.0', { 'type' => 'string' }) + expect(run_rule_for(root)).to eq [] + end + end + + context 'with a 3.1 document using the examples array (correct 3.1 form)' do + it 'reports no violation' do + root = schema_with_example('3.1.0', { 'type' => 'string', 'examples' => ['sample'] }) + expect(run_rule_for(root)).to eq [] + end + end + + context 'with an :unknown version document' do + it 'reports no violation (rule skipped)' do + root = schema_with_example('4.0.0', { 'type' => 'string', 'example' => 'sample' }) + expect(run_rule_for(root)).to eq [] + end + end +end