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
Expand Up @@ -6,8 +6,10 @@
* `PathItemsIn30`: detect `components.pathItems` usage in 3.0 documents (3.1 addition)
* `ExclusiveMinimum` / `ExclusiveMaximum`: detect 3.0 Boolean vs 3.1 numeric form mismatch
* `TypeNullIn30`: detect `type: "null"` usage in 3.0 documents (3.1 primitive)
* `WebhooksIn30`: detect root-level `webhooks` usage in 3.0 documents (3.1 addition)
* support 3.1-style numeric `exclusiveMinimum` / `exclusiveMaximum` in value validation (standalone bound, not a Boolean modifier on `minimum` / `maximum`)
* support `type: "null"` (3.1 primitive) in value validation
* support root-level `webhooks` (OpenAPI 3.1) in the parse layer

## 2.3.1 (2025-11-14)
* add optional date coercion with behavior matching existing datetime coercion
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 @@ -46,6 +46,10 @@ def openapi_version
# @return [Info, nil]
openapi_attr_object :info, Info, reference: false

# @!attribute [r] webhooks
# @return [Hash{String => PathItem}, nil] webhook path items (OpenAPI 3.1+)
openapi_attr_hash_object :webhooks, PathItem, reference: true

# @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 @@ -6,6 +6,7 @@
require_relative 'spec_validator/rules/nullable_deprecation'
require_relative 'spec_validator/rules/example_singular_deprecation'
require_relative 'spec_validator/rules/type_null_in_30'
require_relative 'spec_validator/rules/webhooks_in_30'

module OpenAPIParser
class SpecViolationError < OpenAPIError
Expand Down Expand Up @@ -59,6 +60,7 @@ def rules
Rules::NullableDeprecation,
Rules::ExampleSingularDeprecation,
Rules::TypeNullIn30,
Rules::WebhooksIn30,
]
end
end
Expand Down
18 changes: 18 additions & 0 deletions lib/openapi_parser/spec_validator/rules/webhooks_in_30.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module OpenAPIParser
class SpecValidator
module Rules
# `webhooks` is a 3.1 root-level addition; 3.0 has no equivalent.
class WebhooksIn30 < Rule
def check(root)
return [] unless version == :v3_0
return [] unless root.raw_schema.is_a?(Hash) && root.raw_schema.key?('webhooks')

[violation(
path: '#/webhooks',
message: '`webhooks` 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 @@ -60,6 +60,10 @@ module OpenAPIParser
class TypeNullIn30 < Rule
def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
end

class WebhooksIn30 < Rule
def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
end
end
end

Expand Down
20 changes: 20 additions & 0 deletions spec/data/openapi_3_1/webhooks_30.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
openapi: 3.0.3
info:
title: Billing API
version: '1.0'
paths:
/invoices:
get:
summary: List invoices
responses:
'200':
description: OK
# `webhooks` is a 3.1 root-level addition; 3.0 has no such field, so its
# presence on a 3.0 document is a spec violation.
webhooks:
invoicePaid:
post:
summary: Notify that an invoice was paid
responses:
'200':
description: Acknowledged
20 changes: 20 additions & 0 deletions spec/data/openapi_3_1/webhooks_31.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
openapi: 3.1.0
info:
title: Billing API
version: '1.0'
paths:
/invoices:
get:
summary: List invoices
responses:
'200':
description: OK
# `webhooks` is a legitimate root-level field in 3.1, so no violation is
# expected here.
webhooks:
invoicePaid:
post:
summary: Notify that an invoice was paid
responses:
'200':
description: Acknowledged
13 changes: 13 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 @@ -130,4 +130,17 @@ def expect_clean(file)
end
end

describe 'webhooks (3.1 root-level addition)' do
it 'warns on the version-mismatched document under :warn' do
expect_mismatch_warns('webhooks_30.yaml', [:webhooks_in30])
end

it 'raises SpecViolationError on the version-mismatched document under :raise' do
expect_mismatch_raises('webhooks_30.yaml', [:webhooks_in30])
end

it 'stays clean on the correctly-versioned document' do
expect_clean('webhooks_31.yaml')
end
end
end
84 changes: 84 additions & 0 deletions spec/openapi_parser/spec_validator/rules/webhooks_in_30_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
require_relative '../../../spec_helper'

RSpec.describe 'OpenAPIParser::SpecValidator::Rules::WebhooksIn30' do
def base_doc(openapi_version_string)
{
'openapi' => openapi_version_string,
'info' => { 'title' => 'test', 'version' => '1.0' },
'paths' => {},
}
end

def doc_with_webhooks(openapi_version_string)
raw = base_doc(openapi_version_string)
raw['webhooks'] = {
'newPet' => { 'post' => { 'responses' => { '200' => { 'description' => 'ok' } } } },
}
OpenAPIParser.parse(raw, strict_reference_validation: false)
end

def doc_without_webhooks(openapi_version_string)
OpenAPIParser.parse(base_doc(openapi_version_string), strict_reference_validation: false)
end

def run_rule_for(root)
OpenAPIParser::SpecValidator::Rules::WebhooksIn30.new(root.openapi_version).check(root)
end

context 'with a 3.1 document declaring webhooks' do
it 'reports no violation' do
root = doc_with_webhooks('3.1.0')
expect(run_rule_for(root)).to eq []
end
end

context 'with a 3.1 document without webhooks' do
it 'reports no violation' do
root = doc_without_webhooks('3.1.0')
expect(run_rule_for(root)).to eq []
end
end

context 'with a 3.0 document declaring webhooks' do
it 'reports one violation pointing at #/webhooks' do
root = doc_with_webhooks('3.0.0')
violations = run_rule_for(root)
expect(violations.size).to eq 1
expect(violations.first.path).to eq '#/webhooks'
expect(violations.first.rule_name).to eq :webhooks_in30
end
end

context 'with a 3.0 document without webhooks' do
it 'reports no violation' do
root = doc_without_webhooks('3.0.0')
expect(run_rule_for(root)).to eq []
end
end

context 'with an :unknown version document declaring webhooks' do
it 'reports no violation (rule skipped)' do
root = doc_with_webhooks('4.0.0')
expect(run_rule_for(root)).to eq []
end
end
end

RSpec.describe 'OpenAPI#webhooks parse layer' do
let(:root) do
raw = {
'openapi' => '3.1.0',
'info' => { 'title' => 'test', 'version' => '1.0' },
'paths' => {},
'webhooks' => {
'newPet' => { 'post' => { 'responses' => { '200' => { 'description' => 'ok' } } } },
},
}
OpenAPIParser.parse(raw, strict_reference_validation: false)
end

it 'exposes webhooks as a Hash of PathItem' do
expect(root.webhooks).to be_a(Hash)
expect(root.webhooks['newPet']).to be_a(OpenAPIParser::Schemas::PathItem)
end
end