Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support simple form-data #152

Merged
merged 4 commits into from
Feb 15, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased

- Update to hanami-router 2.0.0 stable. Use it's BodyParser middleware to parse the body.
- Support simple form data requests. Still have to figure out file uploads.

## 0.20.0
- You can pass a filepath to `spec:` now so you no longer have to call `OpenapiFirst.load` anymore.
Expand Down
7 changes: 7 additions & 0 deletions lib/openapi_first/operation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ def name
"#{method.upcase} #{path} (#{operation_id})"
end

def valid_request_content_type?(request_content_type)
content = operation_object.dig('requestBody', 'content')
return unless content

!!find_content_for_content_type(content, request_content_type)
end

private

def response_by_code(status)
Expand Down
7 changes: 3 additions & 4 deletions lib/openapi_first/request_validation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ def call(env) # rubocop:disable Metrics/AbcSize

private

def parse_and_validate_request_body!(operation, request)
def parse_and_validate_request_body!(operation, request) # rubocop:disable Metrics/CyclomaticComplexity
env = request.env

body = env.delete(Hanami::Router::ROUTER_PARSED_BODY) if env.key?(Hanami::Router::ROUTER_PARSED_BODY)
body ||= request.POST if request.form_data?

validate_request_body_presence!(body, operation)
return if body.nil?
Expand All @@ -58,9 +59,7 @@ def parse_and_validate_request_body!(operation, request)
end

def validate_request_content_type!(operation, content_type)
return if operation.request_body.dig('content', content_type)

throw_error(415)
operation.valid_request_content_type?(content_type) || throw_error(415)
end

def validate_request_body_presence!(body, operation)
Expand Down
19 changes: 16 additions & 3 deletions lib/openapi_first/router.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
require 'multi_json'
require 'hanami/router'
require 'hanami/middleware/body_parser'
require_relative 'body_parser_middleware'

module OpenapiFirst
class Router
Expand Down Expand Up @@ -57,10 +56,25 @@ def call_router(env)
env[ORIGINAL_PATH] = env[Rack::PATH_INFO]
env[Rack::PATH_INFO] = Rack::Request.new(env).path
@router.call(env)
rescue Hanami::Middleware::BodyParser::BodyParsingError => e
handle_body_parsing_error(e)
ensure
env[Rack::PATH_INFO] = env.delete(ORIGINAL_PATH) if env[ORIGINAL_PATH]
end

def handle_body_parsing_error(exception)
err = { title: 'Failed to parse body as JSON', status: '400' }
err[:detail] = exception.cause unless ENV['RACK_ENV'] == 'production'
errors = [err]
raise RequestInvalidError, errors if @raise

Rack::Response.new(
MultiJson.dump(errors: errors),
400,
Rack::CONTENT_TYPE => 'application/vnd.api+json'
).finish
end

def build_router(operations)
router = Hanami::Router.new.tap do |r|
operations.each do |operation|
Expand All @@ -72,9 +86,8 @@ def build_router(operations)
)
end
end
raise_error = @raise
Rack::Builder.app do
use BodyParserMiddleware, { raise_error: raise_error }
use Hanami::Middleware::BodyParser, %i[json form]
run router
end
end
Expand Down
1 change: 1 addition & 0 deletions spec/data/foo.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bar
66 changes: 66 additions & 0 deletions spec/data/request-body-validation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,34 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Error"
/json_api:
post:
description: Creates a new pet in the store. Duplicates are allowed
operationId: create_pet_json_api
requestBody:
description: Pet to add to the store
required: true
content:
application/vnd.api+json:
schema:
$ref: "#/components/schemas/NewPet"
responses:
"200":
description: ok
/custom-json-type:
post:
description: Creates a new pet in the store. Duplicates are allowed
operationId: create_pet_custom-json-type
requestBody:
description: Pet to add to the store
required: true
content:
application/prs.custom-json-type+json:
schema:
$ref: "#/components/schemas/NewPet"
responses:
"200":
description: ok
/pets/{id}:
parameters:
- name: id
Expand Down Expand Up @@ -89,6 +117,44 @@ paths:
responses:
200:
description: Ok
/with-form-data:
post:
requestBody:
description: Pet to add to the store
required: true
content:
multipart/form-data:
schema:
$ref: "#/components/schemas/NewPet"
responses:
"200":
description: ok
/with-form-urlencoded:
post:
requestBody:
description: Pet to add to the store
required: true
content:
application/x-www-form-urlencoded:
schema:
$ref: "#/components/schemas/NewPet"
responses:
"200":
description: ok
/with-multipart-file:
post:
requestBody:
description: Pet to add to the store
required: true
content:
multipart/form-data:
schema:
properties:
file:
description: The file
responses:
"200":
description: pet response
components:
schemas:
Pet:
Expand Down
28 changes: 28 additions & 0 deletions spec/operation_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -364,4 +364,32 @@
end
end
end

describe '#valid_request_content_type?' do
it 'returns true for an exact match' do
operation = described_class.new('/', 'get',
{ 'get' => { 'requestBody' => { 'content' => { 'application/json' => {} } } } })
valid = operation.valid_request_content_type?('application/json')
expect(valid).to be true
end

it 'ignores content type parameters' do
operation = described_class.new('/', 'get',
{ 'get' => { 'requestBody' => { 'content' => { 'application/json' => {} } } } })
valid = operation.valid_request_content_type?('application/json; charset=UTF8')
expect(valid).to be true
end

it 'matches type/*' do
operation = described_class.new('/', 'get', { 'get' => { 'requestBody' => { 'content' => { 'text/*' => {} } } } })
valid = operation.valid_request_content_type?('text/plain')
expect(valid).to be true
end

it 'matches */*' do
operation = described_class.new('/', 'get', { 'get' => { 'requestBody' => { 'content' => { '*/*' => {} } } } })
valid = operation.valid_request_content_type?('application/json')
expect(valid).to be true
end
end
end
43 changes: 43 additions & 0 deletions spec/request_validation/request_body_validation_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,23 @@
expect(last_response.status).to be 200
end

it 'works with json:api media type' do
header Rack::CONTENT_TYPE, 'application/vnd.api+json'
post '/json_api', json_dump(request_body)

expect(last_response.status).to be 200
expect(last_request.env[OpenapiFirst::REQUEST_BODY]).to eq request_body
end

pending 'works with a custom json media type' do
header Rack::CONTENT_TYPE, 'application/vnd.my-custom+json'
post '/custom-json-type', json_dump(request_body)

expect(last_response.status).to be 200
expect(last_request.env[OpenapiFirst::REQUEST_BODY]).to eq request_body
end


it 'adds parsed request body to env' do
header Rack::CONTENT_TYPE, 'application/json'
post path, json_dump(request_body)
Expand Down Expand Up @@ -166,6 +183,32 @@
end
end

it 'ignores content type parameters' do
header Rack::CONTENT_TYPE, 'application/json; encoding=utf-8'
post '/pets', json_dump(request_body)

expect(last_response.status).to be 200
end

it 'succeeds with simple multipart form data' do
header Rack::CONTENT_TYPE, 'multipart/form-data'
post '/with-form-data', request_body

expect(last_response.status).to be(200), last_response.body
expect(last_request.env[OpenapiFirst::REQUEST_BODY]).to eq request_body
end


it 'succeeds with form-urlencoded data' do
header Rack::CONTENT_TYPE, 'application/x-www-form-urlencoded'
post '/with-form-urlencoded', request_body

expect(last_response.status).to be(200), last_response.body
expect(last_request.env[OpenapiFirst::REQUEST_BODY]).to eq request_body
end

it "handles file uploads"

it 'returns 415 if required request body is missing' do
header Rack::CONTENT_TYPE, 'application/json'
post path
Expand Down