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 .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Metrics/BlockLength:
Exclude:
- 'spec/**/*.rb'
- '*.gemspec'
Metrics/MethodLength:
Max: 20
Layout/EmptyLinesAroundAttributeAccessor:
Enabled: true
Layout/SpaceAroundMethodCallOperator:
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Changelog

## Unreleased
- Support nullable: true
- Request validation fails if request includes a property with `readOnly: true`
- Response validation fails if response body includes a property with `writeOnly: true`

Expand Down
2 changes: 1 addition & 1 deletion lib/openapi_first/operation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def build_parameters_json_schema
end
end

def generate_schema(schema, params, parameter) # rubocop:disable Metrics/MethodLength
def generate_schema(schema, params, parameter)
required = Set.new(schema['required'])
params.each do |key, value|
required << key if parameter.required
Expand Down
2 changes: 1 addition & 1 deletion lib/openapi_first/request_validation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def initialize(app, raise_error: false)
@raise = raise_error
end

def call(env) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
def call(env) # rubocop:disable Metrics/AbcSize
operation = env[OpenapiFirst::OPERATION]
return @app.call(env) unless operation

Expand Down
2 changes: 1 addition & 1 deletion lib/openapi_first/router.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def call_router(env)
env[Rack::PATH_INFO] = env.delete(ORIGINAL_PATH) if env[ORIGINAL_PATH]
end

def build_router(operations) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
def build_router(operations) # rubocop:disable Metrics/AbcSize
router = Hanami::Router.new {}
operations.each do |operation|
normalized_path = operation.path.gsub('{', ':').gsub('}', '')
Expand Down
15 changes: 13 additions & 2 deletions lib/openapi_first/schema_validation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
require 'json_schemer'

module OpenapiFirst
# Wraps JSONSchemer, mostly to add raw_schema method to test things :|
class SchemaValidation
attr_reader :raw_schema

Expand All @@ -14,12 +13,24 @@ def initialize(schema, write: true)
custom_keywords['readOnly'] = proc { |data| !data } if write
@schemer = JSONSchemer.schema(
schema,
keywords: custom_keywords
keywords: custom_keywords,
before_property_validation: proc do |data, property, property_schema, parent|
convert_nullable(data, property, property_schema, parent)
end
)
end

def validate(input)
@schemer.validate(input)
end

private

def convert_nullable(_data, _property, property_schema, _parent)
return unless property_schema.is_a?(Hash) && property_schema['nullable'] && property_schema['type']

property_schema['type'] = [*property_schema['type'], 'null']
property_schema.delete('nullable')
end
end
end
35 changes: 35 additions & 0 deletions spec/data/nullable.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
openapi: '3.0.2'
paths:
/test:
post:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Test'
responses:
'201':
description: Created
content:
application/json:
schema:
$ref: '#/components/schemas/Test'
get:
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/Test'
components:
schemas:
Test:
type: object
required:
- name
properties:
name:
type: string
nullable: true

29 changes: 29 additions & 0 deletions spec/request_validation/request_body_validation_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,35 @@
end
end

describe 'with a required nullable field' do
let(:app) do
Rack::Builder.new do
spec = OpenapiFirst.load('./spec/data/nullable.yaml')
use OpenapiFirst::Router, spec: spec, raise_error: true
use OpenapiFirst::RequestValidation, raise_error: true
run lambda { |_env|
Rack::Response.new('hello', 201).finish
}
end
end

it 'fails if field is missing' do
header Rack::CONTENT_TYPE, 'application/json'
expect do
post '/test', json_dump({})
end.to raise_error OpenapiFirst::RequestInvalidError, 'Request body invalid: is missing required properties: name' # rubocop:disable Layout/LineLength
end

it 'succeeds if field is nil' do
header Rack::CONTENT_TYPE, 'application/json'
request_body = {
name: nil
}
post '/test', json_dump(request_body)
expect(last_response.status).to eq 201
end
end

describe 'raise_error: true' do
let(:raise_error_option) { true }

Expand Down
27 changes: 26 additions & 1 deletion spec/response_validation_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,31 @@
end
end

describe 'unknown content-type' do
describe 'with a required nullable field' do
let(:spec) { OpenapiFirst.load('./spec/data/nullable.yaml') }

describe 'when the field is missing' do
let(:response_body) do
json_dump({})
end

it 'raises an error' do
message = 'is missing required properties: name '
expect do
get '/test'
end.to raise_error OpenapiFirst::ResponseBodyInvalidError, message
end
end

describe 'when the field is nil' do
let(:response_body) do
json_dump({ name: nil })
end

it 'does not raise an error' do
get '/test'
expect(last_response.status).to eq 200
end
end
end
end