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 @@ -24,6 +24,7 @@
* `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)
* `ServerNameBefore32`: detect Server Object `name` usage in pre-3.2 documents (3.2 addition)
* `ExampleValueFieldsBefore32`: detect Example Object `dataValue` / `serializedValue` 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 @@ -19,6 +19,7 @@
require_relative 'spec_validator/rules/self_before_32'
require_relative 'spec_validator/rules/tag_fields_before_32'
require_relative 'spec_validator/rules/server_name_before_32'
require_relative 'spec_validator/rules/example_value_fields_before_32'

module OpenAPIParser
class SpecViolationError < OpenAPIError
Expand Down Expand Up @@ -85,6 +86,7 @@ def rules
Rules::SelfBefore32,
Rules::TagFieldsBefore32,
Rules::ServerNameBefore32,
Rules::ExampleValueFieldsBefore32,
]
end
end
Expand Down
5 changes: 5 additions & 0 deletions lib/openapi_parser/spec_validator/rule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ def each_node(root, *klasses)
end
end

# escapes a map key for use in a violation path, matching object_reference
def escape_reference(key)
key.to_s.gsub('/', '~1')
end

def walk(node, visited, &block)
return unless node.respond_to?(:_openapi_all_child_objects)
return if visited[node.object_id]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
module OpenAPIParser
class SpecValidator
module Rules
# 3.2 adds `dataValue` and `serializedValue` to the Example Object.
# Example Objects are not modeled by the parse layer, so this rule
# inspects the raw `examples` maps on Parameter, Media Type, and
# Header objects, plus `components.examples`. Media Types under a
# Parameter or Header `content` and anything under callbacks are not
# modeled, so examples there are not reached.
class ExampleValueFieldsBefore32 < Rule
NEW_FIELDS = %w[dataValue serializedValue].freeze

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

violations = []
node_classes = [
OpenAPIParser::Schemas::Parameter,
OpenAPIParser::Schemas::MediaType,
OpenAPIParser::Schemas::Header,
OpenAPIParser::Schemas::Components,
]
each_node(root, *node_classes) do |node|
raw = node.raw_schema
next unless raw.is_a?(Hash)

examples = raw['examples']
next unless examples.is_a?(Hash)

examples.each do |name, example|
next unless example.is_a?(Hash)

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

violations << violation(
path: "#{node.object_reference}/examples/#{escape_reference(name)}/#{field}",
message: "`#{field}` on an Example Object is a 3.2 addition; earlier documents have no such field",
)
end
end
end
violations
end
end
end
end
end
6 changes: 6 additions & 0 deletions sig/openapi_parser/spec_validator.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ module OpenAPIParser
private def violation: (path: String, message: String) -> SpecViolation
private def each_schema: (untyped root) ?{ (untyped) -> void } -> untyped
private def each_node: (untyped root, *Class klasses) { (untyped) -> void } -> void
private def escape_reference: (untyped key) -> String
private def walk: (untyped node, Hash[Integer, bool] visited) { (untyped) -> void } -> void
end

Expand Down Expand Up @@ -116,6 +117,11 @@ module OpenAPIParser
class ServerNameBefore32 < Rule
def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
end

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

Expand Down
25 changes: 25 additions & 0 deletions spec/data/openapi_3_2/example_value_fields_31.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
openapi: 3.1.0
info:
title: Pet API
version: '1.0'
paths:
/pets:
post:
summary: Create a pet
requestBody:
content:
application/json:
schema:
type: object
examples:
dog:
summary: A dog
# `dataValue` and `serializedValue` on an Example Object are
# 3.2 additions; a 3.1 document has no such fields, so their
# use here is a spec violation.
dataValue:
name: Rex
serializedValue: '{"name":"Rex"}'
responses:
'201':
description: Created
24 changes: 24 additions & 0 deletions spec/data/openapi_3_2/example_value_fields_32.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
openapi: 3.2.0
info:
title: Pet API
version: '1.0'
paths:
/pets:
post:
summary: Create a pet
requestBody:
content:
application/json:
schema:
type: object
examples:
dog:
summary: A dog
# `dataValue` and `serializedValue` are legitimate Example
# Object fields under 3.2, so no violation is expected here.
dataValue:
name: Rex
serializedValue: '{"name":"Rex"}'
responses:
'201':
description: Created
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 @@ -80,4 +80,18 @@ def expect_clean(file)
expect_clean('server_name_32.yaml')
end
end

describe 'Example Object dataValue/serializedValue (3.2 additions)' do
it 'warns on the version-mismatched document under :warn' do
expect_mismatch_warns('example_value_fields_31.yaml', [:example_value_fields_before32, :example_value_fields_before32])
end

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

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

RSpec.describe 'OpenAPIParser::SpecValidator::Rules::ExampleValueFieldsBefore32' do
def base_doc(openapi_version_string, example)
{
'openapi' => openapi_version_string,
'info' => { 'title' => 'test', 'version' => '1.0' },
'paths' => {
'/pets' => {
'post' => {
'requestBody' => {
'content' => {
'application/json' => {
'schema' => { 'type' => 'object' },
'examples' => { 'dog' => example },
},
},
},
'responses' => { '201' => { 'description' => 'Created' } },
},
},
},
}
end

def doc_with_value_fields(openapi_version_string)
example = {
'summary' => 'A dog',
'dataValue' => { 'name' => 'Rex' },
'serializedValue' => '{"name":"Rex"}',
}
OpenAPIParser.parse(base_doc(openapi_version_string, example), strict_reference_validation: false)
end

def doc_without_value_fields(openapi_version_string)
example = { 'summary' => 'A dog', 'value' => { 'name' => 'Rex' } }
OpenAPIParser.parse(base_doc(openapi_version_string, example), strict_reference_validation: false)
end

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

context 'with a 3.2 document using dataValue and serializedValue' do
it 'reports no violation' do
root = doc_with_value_fields('3.2.0')
expect(run_rule_for(root)).to eq []
end
end

context 'with a 3.2 document using only the classic value field' do
it 'reports no violation' do
root = doc_without_value_fields('3.2.0')
expect(run_rule_for(root)).to eq []
end
end

context 'with a 3.1 document using dataValue and serializedValue' do
it 'reports one violation per offending field pointing at the example' do
root = doc_with_value_fields('3.1.0')
violations = run_rule_for(root)
expect(violations.size).to eq 2
expect(violations.map(&:path)).to match_array [
'#/paths/~1pets/post/requestBody/content/application~1json/examples/dog/dataValue',
'#/paths/~1pets/post/requestBody/content/application~1json/examples/dog/serializedValue',
]
expect(violations.first.rule_name).to eq :example_value_fields_before32
end
end

context 'with a 3.1 document using only the classic value field' do
it 'reports no violation' do
root = doc_without_value_fields('3.1.0')
expect(run_rule_for(root)).to eq []
end
end

context 'with a 3.0 document using dataValue and serializedValue' do
it 'reports violations' do
root = doc_with_value_fields('3.0.0')
expect(run_rule_for(root).size).to eq 2
end
end

context 'with a 3.1 document using the new fields on parameters, headers, and components' do
let(:example) { { 'dataValue' => { 'name' => 'Rex' } } }
let(:root) do
raw = base_doc('3.1.0', { 'value' => {} })
post = raw['paths']['/pets']['post']
post['parameters'] = [{ 'name' => 'q', 'in' => 'query', 'schema' => { 'type' => 'string' }, 'examples' => { 'rex' => example } }]
post['responses']['201']['headers'] = { 'X-Id' => { 'schema' => { 'type' => 'string' }, 'examples' => { 'rex' => example } } }
raw['components'] = { 'examples' => { 'shared/rex' => example } }
OpenAPIParser.parse(raw, strict_reference_validation: false)
end

it 'reports each location, escaping slashes in example names' do
expect(run_rule_for(root).map(&:path)).to match_array [
'#/paths/~1pets/post/parameters/0/examples/rex/dataValue',
'#/paths/~1pets/post/responses/201/headers/X-Id/examples/rex/dataValue',
'#/components/examples/shared~1rex/dataValue',
]
end
end

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