Skip to content
Draft
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
* `DynamicAnchorIn30`: detect `$dynamicAnchor` usage in 3.0 documents (3.1 addition)
* `ContentSchemaIn30`: detect `contentSchema` usage in 3.0 documents (3.1 addition)
* `SelfBefore32`: detect `$self` usage in pre-3.2 documents (3.2 addition)
* `TagFieldsBefore32`: detect Tag Object `summary` / `parent` / `kind` usage in pre-3.2 documents (3.2 additions)
* expose the declared version as `OpenAPI#openapi_version` (a `Gem::Version`, or nil when the field is missing or malformed) so `SpecValidator` rules compare version ranges; a 3.2 document is checked by the 3.1-or-later rules
* 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
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 @@ -17,6 +17,7 @@
require_relative 'spec_validator/rules/dynamic_anchor_in_30'
require_relative 'spec_validator/rules/content_schema_in_30'
require_relative 'spec_validator/rules/self_before_32'
require_relative 'spec_validator/rules/tag_fields_before_32'

module OpenAPIParser
class SpecViolationError < OpenAPIError
Expand Down Expand Up @@ -81,6 +82,7 @@ def rules
Rules::DynamicAnchorIn30,
Rules::ContentSchemaIn30,
Rules::SelfBefore32,
Rules::TagFieldsBefore32,
]
end
end
Expand Down
34 changes: 34 additions & 0 deletions lib/openapi_parser/spec_validator/rules/tag_fields_before_32.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
module OpenAPIParser
class SpecValidator
module Rules
# 3.2 adds `summary`, `parent`, and `kind` to the Tag Object.
# Tags are not modeled by the parse layer, so this rule inspects the
# raw root-level `tags` array.
class TagFieldsBefore32 < Rule
NEW_FIELDS = %w[summary parent kind].freeze

def check(root)
return [] unless version_before?('3.2')

tags = root.raw_schema.is_a?(Hash) ? root.raw_schema['tags'] : nil
return [] unless tags.is_a?(Array)

violations = []
tags.each_with_index do |tag, index|
next unless tag.is_a?(Hash)

NEW_FIELDS.each do |field|
next unless tag.key?(field)

violations << violation(
path: "#/tags/#{index}/#{field}",
message: "`#{field}` on a Tag Object is a 3.2 addition; earlier documents have no such field",
)
end
end
violations
end
end
end
end
end
5 changes: 5 additions & 0 deletions sig/openapi_parser/spec_validator.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ module OpenAPIParser
class SelfBefore32 < Rule
def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
end

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

Expand Down
21 changes: 21 additions & 0 deletions spec/data/openapi_3_2/tag_fields_31.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
openapi: 3.1.0
info:
title: Pet API
version: '1.0'
# `summary`, `parent`, and `kind` on a Tag Object are 3.2 additions; a 3.1
# document has no such fields, so their use here is a spec violation.
tags:
- name: pets
summary: Pets
kind: nav
- name: pets/dogs
parent: pets
paths:
/pets:
get:
summary: List pets
tags:
- pets
responses:
'200':
description: OK
21 changes: 21 additions & 0 deletions spec/data/openapi_3_2/tag_fields_32.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
openapi: 3.2.0
info:
title: Pet API
version: '1.0'
# `summary`, `parent`, and `kind` are legitimate Tag Object fields under
# 3.2, so no violation is expected here.
tags:
- name: pets
summary: Pets
kind: nav
- name: pets/dogs
parent: pets
paths:
/pets:
get:
summary: List pets
tags:
- pets
responses:
'200':
description: OK
14 changes: 14 additions & 0 deletions spec/openapi_parser/spec_validator/integration_3_2_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,18 @@ def expect_clean(file)
expect_clean('self_32.yaml')
end
end

describe 'Tag Object summary/parent/kind (3.2 additions)' do
it 'warns on the version-mismatched document under :warn' do
expect_mismatch_warns('tag_fields_31.yaml', [:tag_fields_before32, :tag_fields_before32, :tag_fields_before32])
end

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

it 'stays clean on the correctly-versioned document' do
expect_clean('tag_fields_32.yaml')
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
require_relative '../../../spec_helper'

RSpec.describe 'OpenAPIParser::SpecValidator::Rules::TagFieldsBefore32' do
def base_doc(openapi_version_string, tags)
doc = {
'openapi' => openapi_version_string,
'info' => { 'title' => 'test', 'version' => '1.0' },
'paths' => {},
}
doc['tags'] = tags if tags
doc
end

def doc_with_tag_fields(openapi_version_string)
tags = [
{ 'name' => 'pets', 'summary' => 'Pets', 'kind' => 'nav' },
{ 'name' => 'pets/dogs', 'parent' => 'pets' },
]
OpenAPIParser.parse(base_doc(openapi_version_string, tags), strict_reference_validation: false)
end

def doc_without_tag_fields(openapi_version_string)
tags = [{ 'name' => 'pets', 'description' => 'All pets' }]
OpenAPIParser.parse(base_doc(openapi_version_string, tags), strict_reference_validation: false)
end

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

context 'with a 3.2 document using the new tag fields' do
it 'reports no violation' do
root = doc_with_tag_fields('3.2.0')
expect(run_rule_for(root)).to eq []
end
end

context 'with a 3.2 document without the new tag fields' do
it 'reports no violation' do
root = doc_without_tag_fields('3.2.0')
expect(run_rule_for(root)).to eq []
end
end

context 'with a 3.1 document using the new tag fields' do
it 'reports one violation per offending field pointing at the tag' do
root = doc_with_tag_fields('3.1.0')
violations = run_rule_for(root)
expect(violations.size).to eq 3
expect(violations.map(&:path)).to match_array [
'#/tags/0/summary',
'#/tags/0/kind',
'#/tags/1/parent',
]
expect(violations.first.rule_name).to eq :tag_fields_before32
end
end

context 'with a 3.1 document without the new tag fields' do
it 'reports no violation' do
root = doc_without_tag_fields('3.1.0')
expect(run_rule_for(root)).to eq []
end
end

context 'with a 3.0 document using the new tag fields' do
it 'reports violations' do
root = doc_with_tag_fields('3.0.0')
expect(run_rule_for(root).size).to eq 3
end
end

context 'with a document that has no tags array' do
it 'reports no violation' do
root = OpenAPIParser.parse(base_doc('3.1.0', nil), strict_reference_validation: false)
expect(run_rule_for(root)).to eq []
end
end

context 'with a document whose openapi field is not a version' do
it 'reports no violation (rule skipped)' do
root = doc_with_tag_fields('not-a-version')
expect(run_rule_for(root)).to eq []
end
end
end