Permalink
Browse files

Add RequestBodyValidator.

  • Loading branch information...
1 parent a638000 commit 2fccf77d5e280da166bdb9d100c4d783c1af57a5 @myronmarston myronmarston committed Oct 16, 2012
View
@@ -25,6 +25,8 @@ definitions:
* `Interpol::Sinatra::RequestParamsParser` validates and parses
a sinatra `params` hash based on your endpoint params schema
definitions.
+* `Interpol::RequestBodyValidator` is a rack middleware that validates
+ and parses request bodies based on your schema definitions.
You can use any of these tools individually or some combination of all
of them.
@@ -167,7 +169,7 @@ Interpol.default_configuration do |config|
# available. This block will be eval'd in the context of a
# sinatra application, so you can use sinatra helpers like `halt` here.
#
- # Needed by Interpol::StubApp and Interpol::Sinatra::RequestParamsParser.
+ # Used by Interpol::StubApp and Interpol::Sinatra::RequestParamsParser.
config.on_unavailable_sinatra_request_version do |requested_version, available_versions|
message = JSON.dump(
"message" => "Not Acceptable",
@@ -178,6 +180,13 @@ Interpol.default_configuration do |config|
halt 406, message
end
+ # Determines the response when the requested version is not available.
+ #
+ # Used by Interpol::RequestBodyValidator.
+ config.on_unavailable_request_version do |env, requested_version, available_versions|
+ [406, { 'Content-Type' => 'text/plain' }, ['Wrong Version!']]
+ end
+
# Determines which responses will be validated against the endpoint
# definition when you use Interpol::ResponseSchemaValidator. The
# validation is meant to run against the "happy path" response.
@@ -190,6 +199,14 @@ Interpol.default_configuration do |config|
headers['Content-Type'] == my_custom_mime_type
end
+ # Determines which request bodies to validate.
+ #
+ # Used by Interpol::RequestBodyValidator.
+ config.validate_request_if do |env|
+ env.fetch('CONTENT_TYPE').to_s.include?('json') &&
+ %w[ POST PUT ].include?(env.fetch('REQUEST_METHOD'))
+ end
+
# Determines how Interpol::ResponseSchemaValidator handles
# invalid data. By default it will raise an error, but you can
# make it print a warning instead.
@@ -224,6 +241,14 @@ Interpol.default_configuration do |config|
config.on_invalid_sinatra_request_params do |error|
halt 400, JSON.dump(:error => error.message)
end
+
+ # Determines how to respond when the request body is invalid
+ # based on your schema definition.
+ #
+ # Used by Interpol::RequestBodyValidator.
+ config.on_invalid_request_body do |env, error|
+ [400, { 'Content-Type' => 'text/plain' }, [error.message]]
+ end
end
```
@@ -392,6 +417,38 @@ class MySinatraApp < Sinatra::Base
end
```
+### Interpol::RequestBodyValidator
+
+This rack middleware validates request body (e.g. for POST or PUT
+requests) based on your endpoint request schema definitions.
+It also makes the parsed request body available as
+`interpol.parsed_body` in the rack env hash.
+
+``` ruby
+require 'sinatra/base'
+require 'interpol/request_body_validator'
+
+class MySinatraApp < Sinatra::Base
+ # The block is only necessary if you want to override the
+ # default config or have not set a default config.
+ use Interpol::RequestBodyValidator do |config|
+ config.on_invalid_request_body do |error|
+ [400, { 'Content-Type' => 'text/plain' }, [error.message]]
+ end
+ end
+
+ helpers do
+ def parsed_body
+ env.fetch('interpol.parsed_body')
+ end
+ end
+
+ put '/users/:user_id' do
+ User.create_or_replace(parsed_body.user_id, parsed_body.attributes)
+ end
+end
+```
+
## Contributing
1. Fork it
@@ -97,6 +97,14 @@ def validate_if(&block)
validate_response_if(&block)
end
+ def validate_request?(env)
+ @validate_request_if_block.call(env)
+ end
+
+ def validate_request_if(&block)
+ @validate_request_if_block = block
+ end
+
def on_unavailable_sinatra_request_version(&block)
@unavailable_sinatra_request_version_block = block
end
@@ -105,6 +113,14 @@ def sinatra_request_version_unavailable(execution_context, *args)
execution_context.instance_exec(*args, &@unavailable_sinatra_request_version_block)
end
+ def on_unavailable_request_version(&block)
+ @unavailable_request_version_block = block
+ end
+
+ def request_version_unavailable(*args)
+ @unavailable_request_version_block.call(*args)
+ end
+
def on_invalid_sinatra_request_params(&block)
@invalid_sinatra_request_params_block = block
end
@@ -113,6 +129,14 @@ def sinatra_request_params_invalid(execution_context, *args)
execution_context.instance_exec(*args, &@invalid_sinatra_request_params_block)
end
+ def on_invalid_request_body(&block)
+ @invalid_request_body_block = block
+ end
+
+ def request_body_invalid(*args)
+ @invalid_request_body_block.call(*args)
+ end
+
def filter_example_data(&block)
filter_example_data_blocks << block
end
@@ -156,6 +180,13 @@ def endpoint_merge_keys
}.join("\n\n")
end
+ def rack_json_response(status, hash)
+ json = JSON.dump(hash)
+
+ [status, { 'Content-Type' => 'application/json',
+ 'Content-Length' => json.bytesize }, [json]]
+ end
+
def register_default_callbacks
request_version do
raise ConfigurationError, "request_version has not been configured"
@@ -170,13 +201,31 @@ def register_default_callbacks
status >= 200 && status <= 299 && status != 204 # No Content
end
+ validate_request_if do |env|
+ env.fetch('CONTENT_TYPE').to_s.include?('json') &&
+ %w[ POST PUT ].include?(env.fetch('REQUEST_METHOD'))
+ end
+
+ on_unavailable_request_version do |env, requested, available|
+ message = "The requested API version is invalid. " +
+ "Requested: #{requested}. " +
+ "Available: #{available}"
+
+ rack_json_response(406, :error => message)
+ end
+
on_unavailable_sinatra_request_version do |requested, available|
message = "The requested API version is invalid. " +
"Requested: #{requested}. " +
"Available: #{available}"
+
halt 406, JSON.dump(:error => message)
end
+ on_invalid_request_body do |env, error|
+ rack_json_response(400, :error => error.message)
+ end
+
on_invalid_sinatra_request_params do |error|
halt 400, JSON.dump(:error => error.message)
end
@@ -0,0 +1,82 @@
+require 'interpol'
+require 'interpol/dynamic_struct'
+
+module Interpol
+ # Validates and parses a request body according to the endpoint
+ # schema definitions.
+ class RequestBodyValidator
+ def initialize(app, &block)
+ @config = Configuration.default.customized_duplicate(&block)
+ @app = app
+ end
+
+ def call(env)
+ if @config.validate_request?(env)
+ handler = Handler.new(env, @config)
+
+ handler.validate do |error_response|
+ return error_response
+ end
+
+ env['interpol.parsed_body'] = handler.parse
+ end
+
+ @app.call(env)
+ end
+
+ # Handles request body validation for a single request.
+ class Handler
+ attr_reader :env, :config
+
+ def initialize(env, config)
+ @env = env
+ @config = config
+ end
+
+ def parse
+ DynamicStruct.new(parsed_body)
+ end
+
+ def validate(&block)
+ endpoint_definition(&block).validate_data!(parsed_body)
+ rescue Interpol::ValidationError => e
+ yield @config.request_body_invalid(env, e)
+ end
+
+ private
+
+ def request_method
+ env.fetch('REQUEST_METHOD')
+ end
+
+ def path
+ env.fetch('PATH_INFO')
+ end
+
+ def parsed_body
+ @parsed_body ||= JSON.parse(unparsed_body)
+ end
+
+ def unparsed_body
+ @unparsed_body ||= begin
+ input = env.fetch('rack.input')
+ input.read.tap { input.rewind }
+ end
+ end
+
+ def endpoint_definition(&block)
+ config.endpoints.find_definition(request_method, path, 'request', nil) do |endpoint|
+ available = endpoint.available_versions
+
+ @config.request_version_for(env, endpoint).tap do |requested|
+ unless available.include?(requested)
+ yield @config.request_version_unavailable(env, requested, available)
+ end
+ end
+ end
+ end
+
+ end
+ end
+end
+
Oops, something went wrong. Retry.

0 comments on commit 2fccf77

Please sign in to comment.