Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge pull request #15 from seomoz/parse_param_config

Refactor request param parsing into configurable ParamParser objects.
  • Loading branch information...
commit e6b32bfee3ce1f660bb1aaf5cd3285330d18f55e 2 parents 3c296c1 + a6f9ffa
@myronmarston myronmarston authored
View
1  Rakefile
@@ -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
]
View
104 lib/interpol/configuration.rb
@@ -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
View
86 lib/interpol/configuration/built_in_param_parsers.rb
@@ -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
+
View
46 lib/interpol/configuration/default_callbacks.rb
@@ -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
8 lib/interpol/endpoint.rb
@@ -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
30 lib/interpol/errors.rb
@@ -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
View
134 lib/interpol/request_params_parser.rb
@@ -1,6 +1,5 @@
require 'interpol'
require 'interpol/dynamic_struct'
-require 'uri'
require 'interpol/each_with_object' unless Enumerable.method_defined?(:each_with_object)
module Interpol
@@ -20,10 +19,10 @@ module Interpol
# The parsed params object supports dot-syntax for accessing parameters
# and will convert values where feasible (e.g. '3' = 3, 'true' => true, etc).
class RequestParamsParser
- def initialize(endpoint_definition)
- @validator = ParamValidator.new(endpoint_definition)
+ def initialize(endpoint_definition, configuration)
+ @validator = ParamValidator.new(endpoint_definition, configuration)
@validator.validate_path_params_valid_for_route!
- @converter = ParamConverter.new(@validator.param_definitions)
+ @converter = ParamConverter.new(@validator.param_definitions, configuration)
end
def parse(params)
@@ -37,8 +36,9 @@ def validate!(params)
# Private: This takes care of the validation.
class ParamValidator
- def initialize(endpoint_definition)
+ def initialize(endpoint_definition, configuration)
@endpoint_definition = endpoint_definition
+ @configuration = configuration
@params_schema = build_params_schema
end
@@ -107,26 +107,19 @@ def no_additional_properties?
].none? { |params| params['additionalProperties'] }
end
- STRING_EQUIVALENTS = {
- 'string' => nil,
- 'integer' => { 'type' => 'string', 'pattern' => '^\-?\d+$' },
- 'number' => { 'type' => 'string', 'pattern' => '^\-?\d+(\.\d+)?$' },
- 'boolean' => { 'type' => 'string', 'enum' => %w[ true false ] },
- 'null' => { 'type' => 'string', 'enum' => [''] }
- }
-
def adjusted_schema(schema)
- types = Array(schema['type'])
-
- string_equivalents = types.map do |type|
- STRING_EQUIVALENTS.fetch(type) do
- unless type.is_a?(Hash) # a nested union type
- raise UnsupportedTypeError.new(type)
- end
+ adjusted_types = Array(schema['type']).inject([]) do |type_list, type|
+ type_string, options = if type.is_a?(Hash)
+ [type.fetch('type'), type]
+ else
+ [type, schema]
end
- end.compact
- schema.merge('type' => (types + string_equivalents)).tap do |adjusted|
+ @configuration.param_parser_for(type_string, options).
+ type_validation_options_for([type] + type_list, options)
+ end
+
+ schema.merge('type' => adjusted_types).tap do |adjusted|
adjusted['required'] = true unless adjusted['optional']
end
end
@@ -136,8 +129,9 @@ def adjusted_schema(schema)
class ParamConverter
attr_reader :param_definitions
- def initialize(param_definitions)
+ def initialize(param_definitions, configuration)
@param_definitions = param_definitions
+ @configuration = configuration
end
def convert(params)
@@ -156,10 +150,10 @@ def convert_param(name, value)
definition = param_definitions.fetch(name)
Array(definition['type']).each do |type|
- converter = converter_for(type, definition)
+ parser = parser_for(type, definition)
begin
- return converter.call(value)
+ return parser.parse_value(value)
rescue ArgumentError => e
# Try the next unioned type...
end
@@ -168,96 +162,14 @@ def convert_param(name, value)
raise CannotBeParsedError, "The #{name} #{value.inspect} cannot be parsed"
end
- BOOLEANS = { 'true' => true, true => true,
- 'false' => false, false => false }
- def self.convert_boolean(value)
- BOOLEANS.fetch(value) do
- raise ArgumentError, "#{value} is not convertable to a boolean"
- end
- end
-
- NULLS = { '' => nil, nil => nil }
- def self.convert_null(value)
- NULLS.fetch(value) do
- raise ArgumentError, "#{value} is not convertable to null"
- end
- end
-
- def self.convert_date(value)
- unless value =~ /\A\d{4}\-\d{2}\-\d{2}\z/
- raise ArgumentError, "Not in iso8601 format"
+ def parser_for(type, options)
+ if type.is_a?(Hash)
+ return parser_for(type.fetch('type'), type)
end
- Date.new(*value.split('-').map(&:to_i))
- end
-
- def self.convert_uri(value)
- URI(value).tap do |uri|
- unless uri.scheme && uri.host
- raise ArgumentError, "Not a valid full URI"
- end
- end
- rescue URI::InvalidURIError => e
- raise ArgumentError, e.message, e.backtrace
- end
-
- CONVERTERS = {
- 'integer' => method(:Integer),
- 'number' => method(:Float),
- 'boolean' => method(:convert_boolean),
- 'null' => method(:convert_null)
- }
-
- IDENTITY_CONVERTER = lambda { |v| v }
-
- def converter_for(type, definition)
- CONVERTERS.fetch(type) do
- if Hash === type && type['type']
- converter_for(type['type'], type)
- elsif type == 'string'
- string_converter_for(definition)
- else
- raise CannotBeParsedError, "#{type} cannot be parsed"
- end
- end
- end
-
- STRING_CONVERTERS = {
- 'date' => method(:convert_date),
- 'date-time' => Time.method(:iso8601),
- 'uri' => method(:convert_uri)
- }
-
- def string_converter_for(definition)
- STRING_CONVERTERS.fetch(definition['format'], IDENTITY_CONVERTER)
+ @configuration.param_parser_for(type, options)
end
end
-
- # Raised when an unsupported parameter type is defined.
- class UnsupportedTypeError < ArgumentError
- attr_reader :type
-
- def initialize(type)
- @type = type
- super("#{type} params are not supported")
- 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
end
View
17 lib/interpol/sinatra/request_params_parser.rb
@@ -27,7 +27,13 @@ def call(env)
# receive the app instance as an argument here.
def validate_and_parse_params(app)
return unless app.settings.parse_params?
- SingleRequestParamsParser.parse_params(config, app)
+ SingleRequestParamsParser.parse_params(config, app, endpoint_parsers)
+ end
+
+ def endpoint_parsers
+ @endpoint_parsers ||= Hash.new do |hash, endpoint|
+ hash[endpoint] = Interpol::RequestParamsParser.new(endpoint, @config)
+ end
end
private
@@ -57,17 +63,18 @@ def check_configuration_validity
# Handles finding parsing request params for a single request.
class SingleRequestParamsParser
- def self.parse_params(config, app)
- new(config, app).parse_params
+ def self.parse_params(config, app, endpoint_parsers)
+ new(config, app, endpoint_parsers).parse_params
end
- def initialize(config, app)
+ def initialize(config, app, endpoint_parsers)
@config = config
@app = app
+ @endpoint_parsers = endpoint_parsers
end
def parse_params
- endpoint_definition.parse_request_params(params_to_parse)
+ @endpoint_parsers[endpoint_definition].parse(params_to_parse)
rescue Interpol::ValidationError => error
request_params_invalid(error)
end
View
6 lib/interpol/test_helper.rb
@@ -9,7 +9,7 @@ def define_interpol_example_tests(&block)
config = Configuration.default.customized_duplicate(&block)
each_definition_from(config.endpoints) do |endpoint, definition|
- define_definition_test(endpoint, definition)
+ define_definition_test(config, endpoint, definition)
each_example_from(definition) do |example, example_index|
define_example_test(config, endpoint, definition, example, example_index)
@@ -40,13 +40,13 @@ def define_example_test(config, endpoint, definition, example, example_index)
define_test(description) { example.validate! }
end
- def define_definition_test(endpoint, definition)
+ def define_definition_test(config, endpoint, definition)
return unless definition.request?
description = "#{endpoint.name} (v #{definition.version}) request " +
"definition has valid params schema"
define_test description do
- definition.request_params_parser # it will raise an error if it is invalid
+ RequestParamsParser.new(definition, config) # it will raise an error if it is invalid
end
end
View
362 spec/unit/interpol/configuration_spec.rb
@@ -335,6 +335,368 @@ def assert_expected_endpoint
cd.validation_mode.should be(:error)
end
end
+
+ describe "#param_parser_for" do
+ let!(:simple1) { config.define_request_param_parser('simple1') { } }
+ let!(:simple2) { config.define_request_param_parser('simple2') { } }
+
+ let!(:complex1_2) { config.define_request_param_parser('complex1', 'foo' => 2) { } }
+ let!(:complex1_3) { config.define_request_param_parser('complex1', 'foo' => 3) { } }
+ let!(:complex1_nil) { config.define_request_param_parser('complex1', 'foo' => nil) { } }
+
+ let!(:complex2_4) { config.define_request_param_parser('complex2', 'foo' => 4) { } }
+ let!(:complex2_5) { config.define_request_param_parser('complex2', 'foo' => 5,
+ 'bar' => 'a') { } }
+
+ it 'raises an error if no matching definition can be found' do
+ expect {
+ config.param_parser_for('blah', {})
+ }.to raise_error(UnsupportedTypeError)
+ end
+
+ it 'returns the last matching definition (to allow user overrides)' do
+ new_def = config.define_request_param_parser('simple1') { }
+ config.param_parser_for('simple1', {}).should be(new_def)
+ end
+
+ context 'when only a type is given' do
+ it 'returns the matching definition' do
+ config.param_parser_for('simple1', {}).should be(simple1)
+ config.param_parser_for('simple2', {}).should be(simple2)
+ end
+ end
+
+ context 'when options are given' do
+ it 'returns the matching definition' do
+ config.param_parser_for('complex1', 'foo' => 2).should eq(complex1_2)
+ config.param_parser_for('complex1', 'foo' => 3).should eq(complex1_3)
+ end
+
+ it 'ignores extra options that do not apply' do
+ config.param_parser_for('complex1', 'foo' => 2, 'a' => 1).should eq(complex1_2)
+ config.param_parser_for('complex1', 'foo' => 3, 'b' => 2).should eq(complex1_3)
+ config.param_parser_for('simple1', 'a' => 2).should be(simple1)
+ end
+
+ it 'only matches nil values if the matching key is included in the provided hash' do
+ config.param_parser_for('complex1', 'foo' => nil).should eq(complex1_nil)
+ expect {
+ config.param_parser_for('complex1', 'bar' => 4)
+ }.to raise_error(UnsupportedTypeError)
+ end
+ end
+ end
+ end
+
+ describe ParamParser do
+ let(:config) { Configuration.new }
+
+ describe "#parse_value" do
+ it 'raises a useful error if no parse callback has been set' do
+ definition = ParamParser.new("foo", "bar" => 3) { }
+ expect {
+ definition.parse_value("blah")
+ }.to raise_error(/parse/)
+ end
+ end
+
+ it 'allows a block to be passed for string_validation_options' do
+ parser = ParamParser.new("foo", "bar" => 3) do |p|
+ p.string_validation_options do |opts|
+ opts.merge("a" => 2)
+ end
+ end
+
+ options = parser.type_validation_options_for('foo', 'b' => 3)
+ options.last.should eq("type" => "string", "b" => 3, "a" => 2)
+ end
+
+ RSpec::Matchers.define :have_errors_for do |value|
+ match do |schema|
+ validate(schema)
+ end
+
+ failure_message_for_should_not do |schema|
+ ValidationError.new(@errors, params).message
+ end
+
+ def validate(schema)
+ @errors = ::JSON::Validator.fully_validate(schema, params)
+ @errors.any?
+ end
+
+ define_method :params do
+ { 'some_param' => value }
+ end
+ end
+
+ RSpec::Matchers.define :convert do |old_value|
+ chain :to do |new_value|
+ @new_value = new_value
+ end
+
+ match_for_should do |converter|
+ raise "Must specify the expected value with .to" unless defined?(@new_value)
+ @converter = converter
+ @old_value = old_value
+ converted_value == @new_value
+ end
+
+ match_for_should_not do |converter|
+ @converter = converter
+ @old_value = old_value
+ raised_argument_error = false
+
+ begin
+ converted_value
+ rescue ArgumentError
+ raised_argument_error = true
+ end
+
+ raised_argument_error
+ end
+
+ failure_message_for_should do |converter|
+ "expected #{old_value.inspect} to convert to #{@new_value.inspect}, " +
+ "but converted to #{converted_value.inspect}"
+ end
+
+ failure_message_for_should_not do |converter|
+ "expected #{old_value.inspect} to trigger an ArgumentError when " +
+ "conversion was attempted, but did not"
+ end
+
+ def converted_value
+ @converted_value ||= @converter.call(@old_value)
+ end
+ end
+
+ def self.for_type(type, options = {}, &block)
+ description = type.inspect
+ description << " (with options: #{options.inspect})" if options.any?
+
+ context "for type: #{description}" do
+ let(:parser) { config.param_parser_for(type, options) }
+ let(:type) { type }
+
+ let(:schema) do {
+ 'type' => 'object',
+ 'properties' => {
+ 'some_param' => options.merge(
+ 'type' => parser.type_validation_options_for(type, options)
+ )
+ }
+ } end
+
+ let(:converter) { parser.method(:parse_value) }
+
+ module_exec(type, &block)
+ end
+ end
+
+ for_type 'integer' do
+ it 'allows a string integer to pass validation' do
+ schema.should_not have_errors_for("23")
+ schema.should_not have_errors_for("-2")
+ end
+
+ it 'allows an integer to pass validation' do
+ schema.should_not have_errors_for(-12)
+ end
+
+ it 'fails a string that is not formatted like an integer' do
+ schema.should have_errors_for("not an int")
+ end
+
+ it 'fails a string that is formatted like a float' do
+ schema.should have_errors_for("0.5")
+ schema.should have_errors_for(0.5)
+ end
+
+ it 'converts string ints to fixnums' do
+ converter.should convert("23").to(23)
+ converter.should convert(17).to(17)
+ end
+
+ it 'does not convert invalid values' do
+ converter.should_not convert("0.5")
+ converter.should_not convert("not a fixnum")
+ converter.should_not convert(nil)
+ end
+ end
+
+ for_type 'number' do
+ it 'allows a string int or float to pass validation' do
+ schema.should_not have_errors_for("23")
+ schema.should_not have_errors_for("-2.5")
+ end
+
+ it 'allows an integer or float to pass validation' do
+ schema.should_not have_errors_for(-12)
+ schema.should_not have_errors_for(2.17)
+ end
+
+ it 'fails a string that is not formatted like an integer or float' do
+ schema.should have_errors_for("not a num")
+ end
+
+ it 'converts string numbers to floats' do
+ converter.should convert("23.3").to(23.3)
+ converter.should convert(-5).to(-5.0)
+ end
+
+ it 'does not convert invalid values' do
+ converter.should_not convert("not a num")
+ converter.should_not convert(nil)
+ end
+ end
+
+ for_type "boolean" do
+ it 'allows "true" or "false" to pass validation' do
+ schema.should_not have_errors_for("true")
+ schema.should_not have_errors_for("false")
+ end
+
+ it 'allows actual boolean values to pass validation' do
+ schema.should_not have_errors_for(true)
+ schema.should_not have_errors_for(false)
+ end
+
+ it 'fails other values' do
+ schema.should have_errors_for("tru")
+ schema.should have_errors_for("flse")
+ schema.should have_errors_for("23")
+ end
+
+ it 'converts boolean strings to boolean values' do
+ converter.should convert("false").to(false)
+ converter.should convert(false).to(false)
+ converter.should convert("true").to(true)
+ converter.should convert(true).to(true)
+ end
+
+ it 'does not convert invalid values' do
+ converter.should_not convert("tru")
+ converter.should_not convert(nil)
+ end
+ end
+
+ for_type "null" do
+ it 'allows nil or "" to pass validation' do
+ schema.should_not have_errors_for(nil)
+ schema.should_not have_errors_for("")
+ end
+
+ it 'fails other values' do
+ schema.should have_errors_for(" ")
+ schema.should have_errors_for(3)
+ end
+
+ it 'converts "" to nil' do
+ converter.should convert("").to(nil)
+ converter.should convert(nil).to(nil)
+ end
+
+ it 'does not convert invalid values' do
+ converter.should_not convert(" ")
+ converter.should_not convert(3)
+ end
+ end
+
+ for_type "string" do
+ it 'allows strings to pass validation' do
+ schema.should_not have_errors_for("a string")
+ schema.should_not have_errors_for("")
+ end
+
+ it 'fails non-string values' do
+ schema.should have_errors_for(nil)
+ schema.should have_errors_for(3)
+ end
+
+ it 'does not change a given string during conversion' do
+ converter.should convert("a").to("a")
+ end
+
+ it "does not convert invalid values" do
+ converter.should_not convert(nil)
+ converter.should_not convert(3)
+ end
+ end
+
+ for_type "string", 'format' => "date" do
+ it 'allows date formatted strings' do
+ schema.should_not have_errors_for("2012-04-28")
+ end
+
+ it 'fails mis-formatted dates' do
+ schema.should have_errors_for("04-28-2012")
+ end
+
+ it 'fails other strings' do
+ schema.should have_errors_for("not a date")
+ end
+
+ it 'converts date strings to date values' do
+ converter.should convert("2012-08-12").to(Date.new(2012, 8, 12))
+ end
+
+ it 'does not convert invalid values' do
+ converter.should_not convert("04-28-2012")
+ converter.should_not convert("not a date")
+ converter.should_not convert(nil)
+ end
+ end
+
+ for_type 'string', 'format' => 'date-time' do
+ let(:time) { Time.utc(2012, 8, 15, 12, 30) }
+
+ it 'allows date-time formatted strings' do
+ schema.should_not have_errors_for(time.iso8601)
+ end
+
+ it 'fails mis-formatted date-times' do
+ schema.should have_errors_for(time.iso8601.gsub('-', '~'))
+ end
+
+ it 'fails other strings' do
+ schema.should have_errors_for("foo")
+ end
+
+ it 'converts date-time strings to time values' do
+ converter.should convert(time.iso8601).to(time)
+ end
+
+ it 'does not convert invalid values' do
+ converter.should_not convert(time.iso8601.gsub('-', '~'))
+ converter.should_not convert(nil)
+ end
+ end
+
+ for_type 'string', 'format' => 'uri' do
+ let(:uri) { URI('http://foo.com/bar') }
+
+ it 'allows URI strings' do
+ schema.should_not have_errors_for(uri.to_s)
+ end
+
+ it 'fails invalid URI strings' do
+ pending "json-schema doesn't validate URIs yet, unfortunately" do
+ schema.should have_errors_for('not a URI')
+ end
+ end
+
+ it 'converts URI strings to a URI object' do
+ converter.should convert(uri.to_s).to (uri)
+ end
+
+ it 'does not convert invalid URIs' do
+ converter.should_not convert('2012-08-12')
+ converter.should_not convert(' ')
+ converter.should_not convert(nil)
+ converter.should_not convert("@*&^^^@")
+ end
+ end
end
end
View
19 spec/unit/interpol/endpoint_spec.rb
@@ -256,25 +256,6 @@ def build_hash(hash = {})
end
end
- describe "#parse_request_params" do
- let(:parser_class) { fire_replaced_class_double("Interpol::RequestParamsParser") }
- let(:parser) { fire_double("Interpol::RequestParamsParser") }
- let(:definition) { EndpointDefinition.new(endpoint, version, 'response', build_hash) }
-
- it 'parses the given params using a RequestParamsParser' do
- parser_class.should_receive(:new).
- with(definition).
- and_return(parser)
-
- parser.should_receive(:parse).
- with("the" => "params").
- and_return("parsed" => "params")
-
- parsed = definition.parse_request_params("the" => "params")
- parsed.should eq("parsed" => "params")
- end
- end
-
describe "#validate_data" do
let(:schema) do {
'type' => 'object',
View
18 spec/unit/interpol/request_params_parser_spec.rb
@@ -10,7 +10,9 @@ def endpoint_definition
Endpoint.new(raw_endpoint_definition).definitions.first
end
- let(:parser) { RequestParamsParser.new(endpoint_definition) }
+ let(:config) { Configuration.new }
+ let(:parser) { RequestParamsParser.new(endpoint_definition, config) }
+
let(:valid_params) do
{ 'user_id' => '11.22', 'project_language' => 'ruby' }
end
@@ -25,7 +27,7 @@ def endpoint_definition
%w[ array object ].each do |type|
it "raises an error for a #{type} param definition since it does not yet support it" do
endpoint_definition_yml.gsub!('boolean', type)
- expect { parser }.to raise_error(/#{type} params are not supported/)
+ expect { parser }.to raise_error(/no param parser/i)
end
end
@@ -50,6 +52,12 @@ def endpoint_definition
end
end
+ # Note: these specs were originally written when RequestParamsParser had explicit
+ # logic to handle each type of a parameter. They are pretty exhaustive.
+ # Now that we have the ParamParser abstraction (and corresponding specs),
+ # we could get by with fewer, simpler specs here, but they helped me prevent
+ # regressions when doing my refactoring. I'm leaving them for now, but feel
+ # free to delete some of these and/or simplify in the future.
describe '#validate!' do
it 'passes when all params are valid' do
parser.validate!(valid_params) # should not raise an error
@@ -153,7 +161,7 @@ def dup_of(entry)
fetch('properties').
fetch('user_id')['optional'] = true
- new_parser = RequestParamsParser.new(endpoint_definition)
+ new_parser = RequestParamsParser.new(endpoint_definition, config)
new_parser.validate!(without_user_id)
end
@@ -258,7 +266,7 @@ def iso8601(date)
parse_with('project_language' => 'ruby').project_language.should eq('ruby')
end
- it 'supports unioned types' do
+ it 'supports unioned types' do
parse_with('union' => '3').union.should eq(3)
parse_with('union' => '2.3').union.should eq(2.3)
parse_with('union' => '').union.should eq(nil)
@@ -307,7 +315,7 @@ def prevent_validation_failure
expect {
parse
- }.to raise_error(/cannot be parsed/)
+ }.to raise_error(/no param parser/i)
end
it 'ensures all defined params are methods on the returned object, ' +
Please sign in to comment.
Something went wrong with that request. Please try again.