diff --git a/CHANGELOG.md b/CHANGELOG.md index 780886ea..a27f6760 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,9 @@ ## Unreleased * support `components.pathItems` so `$ref`s into it resolve, unblocking OpenAPI 3.1 documents that use reusable path items * support array-form `type` (3.1) in value validation +* support root-level `jsonSchemaDialect` (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 + * `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) * `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) diff --git a/lib/openapi_parser/schemas/openapi.rb b/lib/openapi_parser/schemas/openapi.rb index 4fa2b747..f80cc367 100644 --- a/lib/openapi_parser/schemas/openapi.rb +++ b/lib/openapi_parser/schemas/openapi.rb @@ -50,6 +50,10 @@ def openapi_version # @return [Hash{String => PathItem}, nil] webhook path items (OpenAPI 3.1+) openapi_attr_hash_object :webhooks, PathItem, reference: true + # @!attribute [r] json_schema_dialect + # @return [String, nil] dialect URI for embedded JSON Schemas (OpenAPI 3.1+) + openapi_attr_value :json_schema_dialect, schema_key: :jsonSchemaDialect + # @return [OpenAPIParser::RequestOperation, nil] def request_operation(http_method, request_path) OpenAPIParser::RequestOperation.create(http_method, request_path, @path_item_finder, @config) diff --git a/lib/openapi_parser/spec_validator.rb b/lib/openapi_parser/spec_validator.rb index 6c5c4fb6..cb88c59a 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/json_schema_dialect_in_30' require_relative 'spec_validator/rules/type_array_in_30' require_relative 'spec_validator/rules/path_items_in_30' require_relative 'spec_validator/rules/nullable_deprecation' @@ -57,6 +58,7 @@ def rules [ Rules::ExclusiveMinimum, Rules::ExclusiveMaximum, + Rules::JsonSchemaDialectIn30, Rules::TypeArrayIn30, Rules::PathItemsIn30, Rules::NullableDeprecation, 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 new file mode 100644 index 00000000..c33c3c26 --- /dev/null +++ b/lib/openapi_parser/spec_validator/rules/json_schema_dialect_in_30.rb @@ -0,0 +1,19 @@ +module OpenAPIParser + class SpecValidator + module Rules + # `jsonSchemaDialect` is a 3.1 root-level addition; 3.0 has no + # equivalent. + class JsonSchemaDialectIn30 < Rule + def check(root) + return [] unless version == :v3_0 + return [] unless root.raw_schema.is_a?(Hash) && root.raw_schema.key?('jsonSchemaDialect') + + [violation( + path: '#/jsonSchemaDialect', + message: '`jsonSchemaDialect` is a 3.1 root-level addition; 3.0 documents have no such field', + )] + end + end + end + end +end diff --git a/sig/openapi_parser/spec_validator.rbs b/sig/openapi_parser/spec_validator.rbs index 3eec7d64..10876456 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 JsonSchemaDialectIn30 < Rule + def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation] + end + class TypeArrayIn30 < Rule def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation] end diff --git a/spec/data/openapi_3_1/json_schema_dialect_30.yaml b/spec/data/openapi_3_1/json_schema_dialect_30.yaml new file mode 100644 index 00000000..29fcbdb8 --- /dev/null +++ b/spec/data/openapi_3_1/json_schema_dialect_30.yaml @@ -0,0 +1,14 @@ +openapi: 3.0.3 +# `jsonSchemaDialect` is a 3.1 root-level addition; 3.0 documents have no +# such field, so its presence is a spec violation. +jsonSchemaDialect: https://spec.openapis.org/oas/3.1/dialect/base +info: + title: Inventory API + version: '1.0' +paths: + /items: + get: + summary: List items + responses: + '200': + description: OK diff --git a/spec/data/openapi_3_1/json_schema_dialect_31.yaml b/spec/data/openapi_3_1/json_schema_dialect_31.yaml new file mode 100644 index 00000000..ed367c56 --- /dev/null +++ b/spec/data/openapi_3_1/json_schema_dialect_31.yaml @@ -0,0 +1,14 @@ +openapi: 3.1.0 +# `jsonSchemaDialect` is a legitimate root-level field in 3.1, so no +# violation is expected here. +jsonSchemaDialect: https://spec.openapis.org/oas/3.1/dialect/base +info: + title: Inventory API + version: '1.0' +paths: + /items: + get: + summary: List items + 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 001520fa..a49f89b2 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 'jsonSchemaDialect (3.1 root-level addition)' do + it 'warns on the version-mismatched document under :warn' do + expect_mismatch_warns('json_schema_dialect_30.yaml', [:json_schema_dialect_in30]) + end + + it 'raises SpecViolationError on the version-mismatched document under :raise' do + expect_mismatch_raises('json_schema_dialect_30.yaml', [:json_schema_dialect_in30]) + end + + it 'stays clean on the correctly-versioned document' do + expect_clean('json_schema_dialect_31.yaml') + end + end + describe 'type as an Array of names (3.1 form rejected by 3.0)' do it 'warns on the version-mismatched document under :warn' do expect_mismatch_warns('type_array_30.yaml', [:type_array_in30]) 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 new file mode 100644 index 00000000..47396317 --- /dev/null +++ b/spec/openapi_parser/spec_validator/rules/json_schema_dialect_in_30_spec.rb @@ -0,0 +1,79 @@ +require_relative '../../../spec_helper' + +RSpec.describe 'OpenAPIParser::SpecValidator::Rules::JsonSchemaDialectIn30' do + def base_doc(openapi_version_string) + { + 'openapi' => openapi_version_string, + 'info' => { 'title' => 'test', 'version' => '1.0' }, + 'paths' => {}, + } + end + + def doc_with_dialect(openapi_version_string) + raw = base_doc(openapi_version_string) + raw['jsonSchemaDialect'] = 'https://spec.openapis.org/oas/3.1/dialect/base' + OpenAPIParser.parse(raw, strict_reference_validation: false) + end + + def doc_without_dialect(openapi_version_string) + OpenAPIParser.parse(base_doc(openapi_version_string), strict_reference_validation: false) + end + + def run_rule_for(root) + OpenAPIParser::SpecValidator::Rules::JsonSchemaDialectIn30.new(root.openapi_version).check(root) + end + + context 'with a 3.1 document declaring jsonSchemaDialect' do + it 'reports no violation' do + root = doc_with_dialect('3.1.0') + expect(run_rule_for(root)).to eq [] + end + end + + context 'with a 3.1 document without jsonSchemaDialect' do + it 'reports no violation' do + root = doc_without_dialect('3.1.0') + expect(run_rule_for(root)).to eq [] + end + end + + context 'with a 3.0 document declaring jsonSchemaDialect' do + it 'reports one violation pointing at #/jsonSchemaDialect' do + root = doc_with_dialect('3.0.0') + violations = run_rule_for(root) + expect(violations.size).to eq 1 + expect(violations.first.path).to eq '#/jsonSchemaDialect' + expect(violations.first.rule_name).to eq :json_schema_dialect_in30 + end + end + + context 'with a 3.0 document without jsonSchemaDialect' do + it 'reports no violation' do + root = doc_without_dialect('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_dialect('4.0.0') + expect(run_rule_for(root)).to eq [] + end + end +end + +RSpec.describe 'OpenAPI#json_schema_dialect parse layer' do + let(:root) do + raw = { + 'openapi' => '3.1.0', + 'info' => { 'title' => 'test', 'version' => '1.0' }, + 'paths' => {}, + 'jsonSchemaDialect' => 'https://spec.openapis.org/oas/3.1/dialect/base', + } + OpenAPIParser.parse(raw, strict_reference_validation: false) + end + + it 'exposes the dialect URI string' do + expect(root.json_schema_dialect).to eq 'https://spec.openapis.org/oas/3.1/dialect/base' + end +end