Skip to content

Commit

Permalink
Type validation specs.
Browse files Browse the repository at this point in the history
  • Loading branch information
AlphaHydrae committed Jan 26, 2015
1 parent ca351ae commit f73a788
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 40 deletions.
46 changes: 33 additions & 13 deletions lib/errapi/validations/type.rb
Original file line number Diff line number Diff line change
@@ -1,34 +1,54 @@
module Errapi::Validations
class Type
class Type < Base

def initialize options = {}
unless key = exactly_one_option?(OPTIONS, options)
raise ArgumentError, "One option among :instance_of, :kind_of, :is_a or :is_an must be supplied (but only one)."
end

keys = options.keys.select{ |k,v| OPTIONS.include? k }
raise ArgumentError, "One option among :instance_of, :kind_of, :is_a or :is_an must be given (but only one)." if keys.length != 1

if options.key? :instance_of
@instance_of = check_type! options[:instance_of]
if key == :instance_of
@instance_of = check_types! options[key]
raise ArgumentError, "Type aliases cannot be used with the :instance_of option. Use :kind_of, :is_a or :is_an." if options[key].kind_of? Symbol
else
@kind_of = check_type! options[keys.first]
@kind_of = check_types! options[key]
end
end

def validate value, context, options = {}
if @instance_of && !value.instance_of?(@instance_of)
if @instance_of && @instance_of.none?{ |type| value.instance_of? type }
context.add_error reason: :wrong_type, check_value: @instance_of, checked_value: value.class
elsif @kind_of && !value.kind_of?(@kind_of)
elsif @kind_of && @kind_of.none?{ |type| value.kind_of? type }
context.add_error reason: :wrong_type, check_value: @kind_of, checked_value: value.class
end
end

private

def check_type! type
raise ArgumentError, "A class or module is required, but a #{type.class} was given." unless TYPE_CLASSES.include? type.class
type
def check_types! types
if !types.kind_of?(Array)
types = [ types ]
elsif types.empty?
raise ArgumentError, "At least one class or module is required, but an empty array was given."
end

types.each do |type|
unless TYPE_ALIASES.key?(type) || type.class == Class || type.class == Module
raise ArgumentError, "A class or module (or an array of classes or modules, or a type alias) is required, but a #{type.class} was given."
end
end

types.collect{ |type| TYPE_ALIASES[type] || type }.flatten.uniq
end

OPTIONS = %i(instance_of kind_of is_a is_an)
TYPE_CLASSES = [ Class, Module ]
TYPE_ALIASES = {
string: [ String ],
number: [ Numeric ],
integer: [ Integer ],
boolean: [ TrueClass, FalseClass ],
object: [ Hash ],
array: [ Array ],
null: [ NilClass ]
}
end
end
4 changes: 4 additions & 0 deletions spec/validations/exclusion_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
expect{ described_class.new }.to raise_error(/either :from or :in or :within/i)
end

it "should not allow multiple options to be set" do
expect{ described_class.new from: %w(foo bar baz), in: %w(qux corge grault) }.to raise_error(/only one/i)
end

it "should not accept an object without the #include? method as an option" do
%i(in within).each do |option|
[ nil, true, false, Object.new ].each do |invalid_option|
Expand Down
4 changes: 4 additions & 0 deletions spec/validations/inclusion_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
expect{ described_class.new }.to raise_error(/either :in or :within/i)
end

it "should not allow multiple options to be set" do
expect{ described_class.new in: %w(foo bar baz), within: %w(qux corge grault) }.to raise_error(/not both/i)
end

it "should not accept an object without the #include? method as an option" do
%i(in within).each do |option|
[ nil, true, false, Object.new ].each do |invalid_option|
Expand Down
158 changes: 131 additions & 27 deletions spec/validations/type_spec.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
require 'helper'

RSpec.describe Errapi::Validations::Type do
SPEC_TYPE_VALIDATION_OPTIONS = %i(instance_of kind_of is_a is_an)
SPEC_TYPE_VALIDATION_ALIASES = %i(string number integer boolean object array null)
let(:context){ double add_error: nil }
let(:validation_options){ {} }
let(:type){ Array }
Expand All @@ -12,73 +14,175 @@
end

it "should not allow two options to be set" do
%i(instance_of kind_of is_a is_an instance_of is_a is_an kind_of).each_slice(2).with_index do |slice,i|
SPEC_TYPE_VALIDATION_OPTIONS.permutation(2).to_a.collect(&:sort).uniq.each do |slice|
expect{ described_class.new slice.inject({}){ |memo,option| memo[option] = type; memo } }.to raise_error(ArgumentError, /only one/i)
end
end

it "should not allow something other than a class or module to be given as an option" do
[ nil, true, 'abc', [] ].each do |bad_type|
it "should not accept something other than a class, module or type alias as an option" do
[ nil, true, 'abc', {} ].each do |bad_type|
%i(instance_of kind_of is_a is_an).each do |option|
expect{ described_class.new({ option => bad_type }) }.to raise_error(ArgumentError, /class or module/i)
end
end
end

it "should not accept an empty array as an option" do
%i(instance_of kind_of is_a is_an).each do |option|
expect{ described_class.new({ option => [] }) }.to raise_error(ArgumentError, /at least one class or module is required/i)
end
end

it "should not accept a type alias for the :instance_of option" do
SPEC_TYPE_VALIDATION_ALIASES.each do |type_alias|
expect{ described_class.new instance_of: type_alias }.to raise_error(ArgumentError, /type aliases cannot be used/i)
end
end

describe "with the :instance_of option" do
let(:validation_options){ { instance_of: type } }
let(:types_wrapper){ [*types] }
let(:invalid_values){ [ nil, true, 'abc' ] }
let(:validation_options){ { instance_of: types } }

shared_examples_for "an exact type match" do

it "should not accept another type" do

it "should not accept another type" do
invalid_values.each.with_index do |value,i|
validate value
expect(context).to have_received(:add_error).with(reason: :wrong_type, check_value: types_wrapper, checked_value: value.class)
end

[ nil, true, 'abc', {} ].each.with_index do |value,i|
validate value
expect(context).to have_received(:add_error).with(reason: :wrong_type, check_value: type, checked_value: value.class)
expect(context).to have_received(:add_error).exactly(3).times
end

expect(context).to have_received(:add_error).exactly(4).times
it "should not accept a subtype" do
types_wrapper.each do |type|
subtype = Class.new type
validate subtype.new
expect(context).to have_received(:add_error).with(reason: :wrong_type, check_value: types_wrapper, checked_value: subtype)
end
end

it "should accept the type" do
types_wrapper.each do |type|
validate type.new
expect(context).not_to have_received(:add_error)
end
end
end

it "should not accept a subtype" do
validate subtype.new
expect(context).to have_received(:add_error)
describe "with one type" do
let(:types){ Array }
it_should_behave_like "an exact type match"
end

it "should accept the type" do
validate type.new
expect(context).not_to have_received(:add_error)
describe "with multiple types" do
let(:types){ [ Array, Hash ] }
it_should_behave_like "an exact type match"
end
end

shared_examples_for "a comparison that allows subtypes" do
let(:types_wrapper){ [*types] }
let(:invalid_values){ [ nil, true, 'abc' ] }
let(:validation_options){ { type_option => types } }

shared_examples_for "a lenient type match" do

it "should not accept another type" do
it "should not accept another type" do

[ nil, true, 'abc', {} ].each.with_index do |value,i|
validate value
expect(context).to have_received(:add_error).with(reason: :wrong_type, check_value: type, checked_value: value.class)
invalid_values.each.with_index do |value,i|
validate value
expect(context).to have_received(:add_error).with(reason: :wrong_type, check_value: types_wrapper, checked_value: value.class)
end

expect(context).to have_received(:add_error).exactly(3).times
end

it "should accept a subtype" do
types_wrapper.each do |type|
subtype = Class.new type
validate subtype.new
expect(context).not_to have_received(:add_error)
end
end

expect(context).to have_received(:add_error).exactly(4).times
it "should accept the type" do
types_wrapper.each do |type|
validate type.new
expect(context).not_to have_received(:add_error)
end
end
end

it "should accept a subtype" do
validate subtype.new
expect(context).not_to have_received(:add_error)
describe "with one type" do
let(:types){ Array }
it_should_behave_like "a lenient type match"
end

it "should accept the type" do
validate type.new
expect(context).not_to have_received(:add_error)
describe "with multiple types" do
let(:types){ [ Array, Hash ] }
it_should_behave_like "a lenient type match"
end

describe "with a type alias" do
let(:types){ :object }
let(:types_wrapper){ [ Hash ] }
it_should_behave_like "a lenient type match"
end

describe "with a mix" do
let(:types){ [ :object, Array, Set, :array ] }
let(:types_wrapper){ [ Hash, Array, Set ] }
it_should_behave_like "a lenient type match"
end
end

%i(kind_of is_a is_an).each do |option|
describe "with the #{option} option" do
let(:validation_options){ { option => type } }
let(:type_option){ option }
it_should_behave_like "a comparison that allows subtypes"
end
end

describe "type aliases" do
let(:aliases){ %i(string number integer boolean object array null) }
let(:sample_values){ { string: 'abc', number: 4.5, integer: 3, boolean: false, object: { foo: 'bar' }, array: [ 1, 2, 3 ], null: nil } }
let(:special_cases){ { number: [ :integer ] } } # number is a superset of integer
let(:corresponding_types){ { string: [ String ], number: [ Numeric ], integer: [ Integer ], boolean: [ TrueClass, FalseClass ], object: [ Hash ], array: [ Array ], null: [ NilClass ] } }

SPEC_TYPE_VALIDATION_ALIASES.each do |type_alias|
describe type_alias.to_s do
let(:validation_options){ { kind_of: type_alias } }

it "should not accept other types" do

invalid_values = sample_values.reject{ |k,v| k == type_alias }
invalid_values.reject!{ |k,v| special_cases[type_alias] && special_cases[type_alias].include?(k) }

invalid_values.each_pair do |type,invalid_value|
validate invalid_value
expect(context).to have_received(:add_error).with(reason: :wrong_type, check_value: corresponding_types[type_alias], checked_value: invalid_value.class)
end
end

it "should accept the type" do

validate sample_values[type_alias]
expect(context).not_to have_received(:add_error)

if special_cases[type_alias]
special_cases[type_alias].each do |special_case|
validate sample_values[special_case]
expect(context).not_to have_received(:add_error)
end
end
end
end
end
end

def validate value, options = {}
subject.validate value, context, options
end
Expand Down

0 comments on commit f73a788

Please sign in to comment.