Skip to content

Commit

Permalink
Merge pull request #152 from ahx/form-data
Browse files Browse the repository at this point in the history
Support simple form-data
  • Loading branch information
ahx committed Feb 15, 2023
2 parents 8141da3 + fc4893d commit 17ea5e7
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 7 deletions.
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

0 comments on commit 17ea5e7

Please sign in to comment.