Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
4 changes: 4 additions & 0 deletions lib/openapi_parser/schemas/openapi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions lib/openapi_parser/spec_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -57,6 +58,7 @@ def rules
[
Rules::ExclusiveMinimum,
Rules::ExclusiveMaximum,
Rules::JsonSchemaDialectIn30,
Rules::TypeArrayIn30,
Rules::PathItemsIn30,
Rules::NullableDeprecation,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions sig/openapi_parser/spec_validator.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions spec/data/openapi_3_1/json_schema_dialect_30.yaml
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions spec/data/openapi_3_1/json_schema_dialect_31.yaml
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions spec/openapi_parser/spec_validator/integration_3_1_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
Original file line number Diff line number Diff line change
@@ -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