Permalink
Browse files

Merge pull request #15 from seomoz/parse_param_config

Refactor request param parsing into configurable ParamParser objects.
  • Loading branch information...
2 parents 3c296c1 + a6f9ffa commit e6b32bfee3ce1f660bb1aaf5cd3285330d18f55e @myronmarston myronmarston committed Oct 23, 2012
View
@@ -17,7 +17,6 @@ if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'ruby' # MRI only
cane.style_measure = 100
cane.abc_exclude = %w[
- Interpol::Configuration#register_default_callbacks
Interpol::StubApp::Builder#initialize
]
@@ -2,6 +2,7 @@
require 'interpol/errors'
require 'yaml'
require 'interpol/configuration_ruby_18_extensions' if RUBY_VERSION.to_f < 1.9
+require 'uri'
module Interpol
module DefinitionFinder
@@ -40,6 +41,7 @@ def initialize
self.endpoint_definition_merge_key_files = []
self.documentation_title = "API Documentation Provided by Interpol"
register_default_callbacks
+ register_built_in_param_parsers
@filter_example_data_blocks = []
yield self if block_given?
@@ -163,6 +165,23 @@ def customized_duplicate(&block)
dup.tap(&block)
end
+ def define_request_param_parser(type, options = {}, &block)
+ ParamParser.new(type, options, &block).tap do |parser|
+ # Use unshift so that new parsers take precedence over older ones.
+ param_parsers[type].unshift parser
+ end
+ end
+
+ def param_parser_for(type, options)
+ match = param_parsers[type].find do |parser|
+ parser.matches_options?(options)
+ end
+
+ return match if match
+
+ raise UnsupportedTypeError.new(type, options)
+ end
+
private
# 1.9 version
@@ -204,51 +223,70 @@ def named_example_selectors
@named_example_selectors ||= {}
end
- def register_default_callbacks
- request_version do
- raise ConfigurationError, "request_version has not been configured"
- end
+ def param_parsers
+ @param_parsers ||= Hash.new { |h, k| h[k] = [] }
+ end
- response_version do
- raise ConfigurationError, "response_version has not been configured"
- end
+ def self.instance_eval_args_for(file)
+ filename = File.expand_path("../configuration/#{file}.rb", __FILE__)
+ contents = File.read(filename)
+ [contents, filename, 1]
+ end
- validate_response_if do |env, status, headers, body|
- headers['Content-Type'].to_s.include?('json') &&
- status >= 200 && status <= 299 && status != 204 # No Content
- end
+ BUILT_IN_PARSER_EVAL_ARGS = instance_eval_args_for("built_in_param_parsers")
- validate_request_if do |env|
- env['CONTENT_TYPE'].to_s.include?('json') &&
- %w[ POST PUT PATCH ].include?(env.fetch('REQUEST_METHOD'))
- end
+ def register_built_in_param_parsers
+ instance_eval(*BUILT_IN_PARSER_EVAL_ARGS)
+ end
- on_unavailable_request_version do |env, requested, available|
- message = "The requested request version is invalid. " +
- "Requested: #{requested}. " +
- "Available: #{available}"
+ DEFAULT_CALLBACK_EVAL_ARGS = instance_eval_args_for("default_callbacks")
+ def register_default_callbacks
+ instance_eval(*DEFAULT_CALLBACK_EVAL_ARGS)
+ end
+ end
- rack_json_response(406, :error => message)
- end
+ # Holds the validation/parsing logic for a particular parameter
+ # type (w/ additional options).
+ class ParamParser
+ def initialize(type, options = {})
+ @type = type
+ @options = options
+ yield self
+ end
- on_unavailable_sinatra_request_version do |requested, available|
- message = "The requested request version is invalid. " +
- "Requested: #{requested}. " +
- "Available: #{available}"
+ def string_validation_options(options = nil, &block)
+ @string_validation_options_block = block || Proc.new { options }
+ end
- halt 406, JSON.dump(:error => message)
- end
+ def parse(&block)
+ @parse_block = block
+ end
- on_invalid_request_body do |env, error|
- rack_json_response(400, :error => error.message)
+ def matches_options?(options)
+ @options.all? do |key, value|
+ options.has_key?(key) && options[key] == value
end
+ end
+
+ def type_validation_options_for(type, options)
+ return type unless @string_validation_options_block
+ string_options = @string_validation_options_block.call(options)
+ Array(type) + [string_options.merge('type' => 'string')]
+ end
- on_invalid_sinatra_request_params do |error|
- halt 400, JSON.dump(:error => error.message)
+ def parse_value(value)
+ unless @parse_block
+ raise "No parse callback has been set for param type definition: #{description}"
end
- select_example_response do |endpoint_def, _|
- endpoint_def.examples.first
+ @parse_block.call(value)
+ end
+
+ def description
+ @description ||= @type.inspect.tap do |desc|
+ if @options.any?
+ desc << " (with options: #{@options.inspect})"
+ end
end
end
end
@@ -0,0 +1,86 @@
+define_request_param_parser('integer') do |param|
+ param.string_validation_options 'pattern' => '^\-?\d+$'
+
+ param.parse do |value|
+ begin
+ raise TypeError unless value # On 1.8.7 Integer(nil) does not raise an error
+ Integer(value)
+ rescue TypeError
+ raise ArgumentError, "Could not convert #{value.inspect} to an integer"
+ end
+ end
+end
+
+define_request_param_parser('number') do |param|
+ param.string_validation_options 'pattern' => '^\-?\d+(\.\d+)?$'
+
+ param.parse do |value|
+ begin
+ Float(value)
+ rescue TypeError
+ raise ArgumentError, "Could not convert #{value.inspect} to a float"
+ end
+ end
+end
+
+define_request_param_parser('boolean') do |param|
+ param.string_validation_options 'enum' => %w[ true false ]
+
+ booleans = { 'true' => true, true => true,
+ 'false' => false, false => false }
+ param.parse do |value|
+ booleans.fetch(value) do
+ raise ArgumentError, "Could not convert #{value.inspect} to a boolean"
+ end
+ end
+end
+
+define_request_param_parser('null') do |param|
+ param.string_validation_options 'enum' => ['']
+
+ nulls = { '' => nil, nil => nil }
+ param.parse do |value|
+ nulls.fetch(value) do
+ raise ArgumentError, "Could not convert #{value.inspect} to a null"
+ end
+ end
+end
+
+define_request_param_parser('string') do |param|
+ param.parse do |value|
+ unless value.is_a?(String)
+ raise ArgumentError, "#{value.inspect} is not a string"
+ end
+
+ value
+ end
+end
+
+define_request_param_parser('string', 'format' => 'date') do |param|
+ param.parse do |value|
+ unless value =~ /\A\d{4}\-\d{2}\-\d{2}\z/
+ raise ArgumentError, "#{value.inspect} is not in iso8601 format"
+ end
+
+ Date.new(*value.split('-').map(&:to_i))
+ end
+end
+
+define_request_param_parser('string', 'format' => 'date-time') do |param|
+ param.parse &Time.method(:iso8601)
+end
+
+define_request_param_parser('string', 'format' => 'uri') do |param|
+ param.parse do |value|
+ begin
+ URI(value).tap do |uri|
+ unless uri.scheme && uri.host
+ raise ArgumentError, "#{uri.inspect} is not a valid full URI"
+ end
+ end
+ rescue URI::InvalidURIError => e
+ raise ArgumentError, e.message, e.backtrace
+ end
+ end
+end
+
@@ -0,0 +1,46 @@
+request_version do
+ raise ConfigurationError, "request_version has not been configured"
+end
+
+response_version do
+ raise ConfigurationError, "response_version has not been configured"
+end
+
+validate_response_if do |env, status, headers, body|
+ headers['Content-Type'].to_s.include?('json') &&
+ status >= 200 && status <= 299 && status != 204 # No Content
+end
+
+validate_request_if do |env|
+ env['CONTENT_TYPE'].to_s.include?('json') &&
+ %w[ POST PUT PATCH ].include?(env.fetch('REQUEST_METHOD'))
+end
+
+on_unavailable_request_version do |env, requested, available|
+ message = "The requested request 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 request 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
+
+select_example_response do |endpoint_def, _|
+ endpoint_def.examples.first
+end
+
View
@@ -201,14 +201,6 @@ def example_status_code
@example_status_code ||= @status_codes.example_status_code
end
- def parse_request_params(request_params)
- request_params_parser.parse(request_params)
- end
-
- def request_params_parser
- @request_params_parser ||= RequestParamsParser.new(self)
- end
-
private
def make_schema_strict!(raw_schema, modify_object=true)
View
@@ -38,5 +38,35 @@ class MultipleEndpointDefinitionsFoundError < Error; end
# Raised when an invalid status code is found during validate_codes!
class StatusCodeMatcherArgumentError < ArgumentError; end
+
+ # Raised when an unsupported parameter type is defined.
+ class UnsupportedTypeError < ArgumentError
+ attr_reader :type, :options
+
+ def initialize(type, options = {})
+ @type = type
+ @options = options
+
+ description = type.inspect
+ description << " (#{options.inspect})" if options.any?
+ super("No param parser can be found for #{description}")
+ end
+ end
+
+ # Raised when the path_params are not part of the endpoint route.
+ class InvalidPathParamsError < ArgumentError
+ attr_reader :invalid_params
+
+ def initialize(*invalid_params)
+ @invalid_params = invalid_params
+ super("The path params #{invalid_params.join(', ')} are not in the route")
+ end
+ end
+
+ # Raised when a parameter value cannot be parsed.
+ CannotBeParsedError = Class.new(ArgumentError)
+
+ # Raised when a params definition is invalid.
+ InvalidParamsDefinitionError = Class.new(ArgumentError)
end
Oops, something went wrong.

0 comments on commit e6b32bf

Please sign in to comment.