Skip to content
Open
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 @@ -2,7 +2,9 @@
* 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
* support `prefixItems` (OpenAPI 3.1) with positional tuple validation
* add `SpecValidator` with `strict_specification_version` config (`:silent` / `:warn` / `:raise`) to detect version mismatches between declared OpenAPI version and actual field usage
* `PrefixItemsIn30`: detect `prefixItems` usage in 3.0 documents (3.1 addition)
* `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)
Expand Down
10 changes: 7 additions & 3 deletions lib/openapi_parser/schema_validator/array_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@ def coerce_and_validate(value, schema, **_keyword_args)
value, err = validate_unique_items(value, schema)
return [nil, err] if err

# array type have an schema in items property
# 3.1: prefixItems positionally validates the leading N elements.
# Elements past the prefix fall back to schema.items, matching how
# JSON Schema 2020-12 layers the two keywords.
prefix_schemas = schema.prefix_items || []
items_schema = schema.items
coerced_values = value.map do |v|
coerced, err = validatable.validate_schema(v, items_schema)
coerced_values = value.each_with_index.map do |v, idx|
sub_schema = prefix_schemas[idx] || items_schema
coerced, err = validatable.validate_schema(v, sub_schema)
return [nil, err] if err

coerced
Expand Down
4 changes: 4 additions & 0 deletions lib/openapi_parser/schemas/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ class Schema < Base
# @return [Schema, nil]
openapi_attr_object :items, Schema, reference: true

# @!attribute [r] prefix_items
# @return [Array<Schema>, nil] tuple-style positional schemas (OpenAPI 3.1+)
openapi_attr_list_object :prefix_items, Schema, reference: true, schema_key: :prefixItems

# @!attribute [r] properties
# @return [Hash{String => Schema}, nil]
openapi_attr_hash_object :properties, Schema, reference: true
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/prefix_items_in_30'
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'
Expand Down Expand Up @@ -59,6 +60,7 @@ def rules
[
Rules::ExclusiveMinimum,
Rules::ExclusiveMaximum,
Rules::PrefixItemsIn30,
Rules::JsonSchemaDialectIn30,
Rules::TypeArrayIn30,
Rules::PathItemsIn30,
Expand Down
25 changes: 25 additions & 0 deletions lib/openapi_parser/spec_validator/rules/prefix_items_in_30.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module OpenAPIParser
class SpecValidator
module Rules
# `prefixItems` is JSON Schema 2020-12's positional tuple keyword.
# 3.1 adopts it; 3.0 has no equivalent and parsing it is a spec
# mismatch the validator should report.
class PrefixItemsIn30 < Rule
def check(root)
return [] unless version == :v3_0

violations = []
each_schema(root) do |schema|
next unless schema.raw_schema.is_a?(Hash) && schema.raw_schema.key?('prefixItems')

violations << violation(
path: schema.object_reference,
message: '`prefixItems` is a 3.1 addition (from JSON Schema 2020-12); 3.0 has no equivalent',
)
end
violations
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 PrefixItemsIn30 < Rule
def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
end

class JsonSchemaDialectIn30 < Rule
def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
end
Expand Down
25 changes: 25 additions & 0 deletions spec/data/openapi_3_1/prefix_items_30.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
openapi: 3.0.3
info:
title: Geo API
version: '1.0'
paths:
/points:
post:
summary: Submit a coordinate
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Coordinate'
responses:
'201':
description: Created
components:
schemas:
Coordinate:
# `prefixItems` is JSON Schema 2020-12's positional tuple keyword,
# adopted by 3.1. 3.0 has no equivalent, so this is a spec violation.
type: array
prefixItems:
- type: number
- type: number
25 changes: 25 additions & 0 deletions spec/data/openapi_3_1/prefix_items_31.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
openapi: 3.1.0
info:
title: Geo API
version: '1.0'
paths:
/points:
post:
summary: Submit a coordinate
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Coordinate'
responses:
'201':
description: Created
components:
schemas:
Coordinate:
# `prefixItems` is legitimate under 3.1 (JSON Schema 2020-12), so no
# violation is expected here.
type: array
prefixItems:
- type: number
- type: number
51 changes: 51 additions & 0 deletions spec/openapi_parser/schema_validator/array_validator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,55 @@
end
end
end

describe 'prefixItems tuple validation (3.1)' do
let(:tuple_schema) do
raw = {
'openapi' => '3.1.0',
'info' => { 'title' => 'test', 'version' => '1.0' },
'paths' => {},
'components' => {
'schemas' => {
'Tuple' => {
'type' => 'array',
'prefixItems' => [
{ 'type' => 'string' },
{ 'type' => 'integer' },
],
'items' => { 'type' => 'boolean' },
},
},
},
}
OpenAPIParser.parse(raw, strict_reference_validation: false).components.schemas['Tuple']
end

context 'when the array obeys prefixItems exactly' do
it 'passes validation' do
expect(OpenAPIParser::SchemaValidator.validate(['a', 1], tuple_schema, options)).to eq(['a', 1])
end
end

context 'when an element fails its prefixItems schema' do
it 'raises a validation error' do
expect do
OpenAPIParser::SchemaValidator.validate(['a', 'not_an_integer'], tuple_schema, options)
end.to raise_error(OpenAPIParser::ValidateError)
end
end

context 'when extra elements are validated against items' do
it 'passes when extras conform to items' do
expect(OpenAPIParser::SchemaValidator.validate(['a', 1, true, false], tuple_schema, options)).to eq(['a', 1, true, false])
end
end

context 'when extra elements violate items' do
it 'raises a validation error' do
expect do
OpenAPIParser::SchemaValidator.validate(['a', 1, 'not_boolean'], tuple_schema, options)
end.to raise_error(OpenAPIParser::ValidateError)
end
end
end
end
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 'prefixItems (JSON Schema tuple keyword new in 3.1)' do
it 'warns on the version-mismatched document under :warn' do
expect_mismatch_warns('prefix_items_30.yaml', [:prefix_items_in30])
end

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

it 'stays clean on the correctly-versioned document' do
expect_clean('prefix_items_31.yaml')
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])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
require_relative '../../../spec_helper'

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

def doc_with_prefix_items(openapi_version_string)
raw = base_doc(
openapi_version_string,
{
'type' => 'array',
'prefixItems' => [
{ 'type' => 'string' },
{ 'type' => 'integer' },
],
},
)
OpenAPIParser.parse(raw, strict_reference_validation: false)
end

def doc_without_prefix_items(openapi_version_string)
raw = base_doc(openapi_version_string, { 'type' => 'array', 'items' => { 'type' => 'integer' } })
OpenAPIParser.parse(raw, strict_reference_validation: false)
end

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

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

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

context 'with a 3.0 document using prefixItems' do
it 'reports one violation pointing at the offending schema' do
root = doc_with_prefix_items('3.0.0')
violations = run_rule_for(root)
expect(violations.size).to eq 1
expect(violations.first.path).to eq '#/components/schemas/Tuple'
expect(violations.first.rule_name).to eq :prefix_items_in30
end
end

context 'with a 3.0 document without prefixItems' do
it 'reports no violation' do
root = doc_without_prefix_items('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_prefix_items('4.0.0')
expect(run_rule_for(root)).to eq []
end
end
end