type 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.githooks/pre-commit/ruby-appraiser +17 -0
- data/.gitignore +17 -0
- data/CHANGELOG.md +7 -0
- data/CONTRIBUTING.md +41 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +13 -0
- data/README.md +218 -0
- data/Rakefile +10 -0
- data/lib/type.rb +44 -0
- data/lib/type/builtin.rb +111 -0
- data/lib/type/definition.rb +165 -0
- data/lib/type/definition/collection.rb +31 -0
- data/lib/type/definition/collection/constrained.rb +80 -0
- data/lib/type/definition/nilable.rb +55 -0
- data/lib/type/definition/proxy.rb +24 -0
- data/lib/type/definition/scalar.rb +20 -0
- data/lib/type/error.rb +41 -0
- data/lib/type/version.rb +6 -0
- data/spec/type/builtin_spec.rb +242 -0
- data/spec/type/definition_spec.rb +84 -0
- data/type.gemspec +29 -0
- metadata +151 -0
@@ -0,0 +1,165 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'type/definition/proxy'
|
4
|
+
|
5
|
+
module Type
|
6
|
+
# Type::Definition is the interface for all type definitions.
|
7
|
+
#
|
8
|
+
# Standard implementations are:
|
9
|
+
# - Type::Definition::Scalar
|
10
|
+
# - Type::Definition::Collection
|
11
|
+
# Modifier implementations are:
|
12
|
+
# - Type::Definition::Nilable, available as Type::Definition#nilable
|
13
|
+
#
|
14
|
+
module Definition
|
15
|
+
module ClassMethods
|
16
|
+
# @api protected
|
17
|
+
# Public APIs are Type::scalar and Type::collection
|
18
|
+
# @overload generate(name, &block)
|
19
|
+
# The block is called in the context of the definition, and is expected
|
20
|
+
# to call one of `#validate` or `#cast` with appropriate blocks.
|
21
|
+
# @param name [Symbol, nil] (nil)
|
22
|
+
# Capital-letter symbol (e.g., `:Int32`) for which to register this
|
23
|
+
# definition globally.
|
24
|
+
# @return [Type::Definition]
|
25
|
+
# @example
|
26
|
+
# ~~~ ruby
|
27
|
+
# Type::scalar(:Integer) do
|
28
|
+
# validate do |input|
|
29
|
+
# input.kind_of?(::Integer)
|
30
|
+
# end
|
31
|
+
# cast do |input|
|
32
|
+
# Kernel::Integer(input)
|
33
|
+
# end
|
34
|
+
# end
|
35
|
+
# ~~~
|
36
|
+
#
|
37
|
+
# @overload generate(name)
|
38
|
+
# @param name [Symbol, nil] (nil)
|
39
|
+
# Capital-letter symbol (e.g., `:Int32`) for which to register this
|
40
|
+
# definition globally.
|
41
|
+
# @return [Type::Definition::Proxy]
|
42
|
+
# You are expected to call from(type_def, &block) to finish the
|
43
|
+
# definition
|
44
|
+
# @return [Type::Definition, Type::Definition::Proxy]
|
45
|
+
# @example
|
46
|
+
# ~~~ ruby
|
47
|
+
# Type::scalar(:Int32).from(:Integer) do
|
48
|
+
# int32_range = (-1 << 31) ... (1 << 31)
|
49
|
+
# validate do |input|
|
50
|
+
# int32_range.include?(input)
|
51
|
+
# end
|
52
|
+
# end
|
53
|
+
# ~~~
|
54
|
+
def generate(name = nil, &block)
|
55
|
+
return new(name, &block) if block_given?
|
56
|
+
Proxy.new(name, self)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.included(base)
|
61
|
+
base.extend(ClassMethods)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Create a new Type::Definition
|
65
|
+
# You should never have to use Type::Definition#initialize directly;
|
66
|
+
# instead use Type::Definition::generate()
|
67
|
+
#
|
68
|
+
# @param name [Symbol] (nil)
|
69
|
+
# Capital-letter symbol (e.g., `:Int32`) for which to register this
|
70
|
+
# definition globally.
|
71
|
+
# If defining a `Type::Definition` with name `:FooBar`,
|
72
|
+
# the following are registerde:
|
73
|
+
# - `Type::FooBar`: a reference to the `Type::Definition`
|
74
|
+
# - `Type::FooBar()`: an alias to `Type::FooBar::cast!()`
|
75
|
+
# - `Type::FooBar?()`: an alias to `Type::FooBar::validate?()`
|
76
|
+
# @param parent [Symbol, Type::Definition]
|
77
|
+
# A parent Type::Definition whose validation and casting is done *before*
|
78
|
+
# it is done in self. See the builtin Type::Int32 for an example.
|
79
|
+
def initialize(name = nil, parent = nil, &block)
|
80
|
+
@name = name && name.to_sym
|
81
|
+
if parent
|
82
|
+
@parent = Type.find(parent)
|
83
|
+
validators.concat @parent.validators.dup
|
84
|
+
castors.concat @parent.castors.dup
|
85
|
+
end
|
86
|
+
Type.register(self)
|
87
|
+
instance_exec(&block) if block_given?
|
88
|
+
end
|
89
|
+
attr_reader :name
|
90
|
+
|
91
|
+
# @param input [Object]
|
92
|
+
# @return [Boolean]
|
93
|
+
def valid?(input)
|
94
|
+
validators.all? { |proc| proc[input] }
|
95
|
+
rescue
|
96
|
+
false
|
97
|
+
end
|
98
|
+
|
99
|
+
# @param input [Object]
|
100
|
+
# @return [Object] the result of casting, guaranteed to be valid.
|
101
|
+
# @raise [Type::CastError]
|
102
|
+
def cast!(input)
|
103
|
+
input = yield if block_given?
|
104
|
+
raise CastError.new(input, self) if input.nil?
|
105
|
+
castors.reduce(input) do |intermediate, castor|
|
106
|
+
castor[intermediate]
|
107
|
+
end.tap do |output|
|
108
|
+
raise ValidationError.new(output, self) unless valid?(output)
|
109
|
+
end
|
110
|
+
rescue
|
111
|
+
raise CastError.new(input, self)
|
112
|
+
end
|
113
|
+
alias_method :[], :cast!
|
114
|
+
|
115
|
+
def refine(name = nil, &config)
|
116
|
+
self.class.new(name, self, &config)
|
117
|
+
end
|
118
|
+
|
119
|
+
# @return [Proc]
|
120
|
+
def to_proc
|
121
|
+
method(:cast!).to_proc
|
122
|
+
end
|
123
|
+
|
124
|
+
require 'type/definition/scalar'
|
125
|
+
require 'type/definition/collection'
|
126
|
+
require 'type/definition/nilable'
|
127
|
+
|
128
|
+
# @return [String]
|
129
|
+
def to_s
|
130
|
+
name ? "Type::#{name}" : super
|
131
|
+
end
|
132
|
+
|
133
|
+
# @api private
|
134
|
+
# @return [Array<Proc>]
|
135
|
+
# Allows seeding with parent's validators
|
136
|
+
def validators
|
137
|
+
(@validators ||= [])
|
138
|
+
end
|
139
|
+
protected :validators
|
140
|
+
|
141
|
+
# @api private
|
142
|
+
# @return [Array<Proc>]
|
143
|
+
# Allows seeding with parent's validators
|
144
|
+
def castors
|
145
|
+
(@castors ||= [])
|
146
|
+
end
|
147
|
+
protected :castors
|
148
|
+
|
149
|
+
# used for configuring, but not after set up.
|
150
|
+
# TODO: extract to DSL.
|
151
|
+
# @api private
|
152
|
+
def validate(&block)
|
153
|
+
validators << block
|
154
|
+
end
|
155
|
+
private :validate
|
156
|
+
|
157
|
+
# used for configuring, but not after set up.
|
158
|
+
# TODO: extract to DSL.
|
159
|
+
# @api private
|
160
|
+
def cast(&block)
|
161
|
+
castors << block
|
162
|
+
end
|
163
|
+
private :cast
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'type/definition/collection/constrained'
|
4
|
+
|
5
|
+
module Type
|
6
|
+
class << self
|
7
|
+
# see Definition::Collection#generate
|
8
|
+
def collection(name = nil, &block)
|
9
|
+
Definition::Collection.generate(name, &block)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module Definition
|
14
|
+
# Type::Definition::Collection validate and cast enumerables.
|
15
|
+
# For a more interesting implementation, see the constrained
|
16
|
+
# implementation of Type::Definition::Collection::Constrained
|
17
|
+
class Collection
|
18
|
+
include Definition
|
19
|
+
|
20
|
+
def valid?(input, &block)
|
21
|
+
return false unless input.kind_of?(Enumerable)
|
22
|
+
super
|
23
|
+
end
|
24
|
+
|
25
|
+
def cast!(input, &block)
|
26
|
+
raise CastError.new(input, self) unless input.kind_of?(Enumerable)
|
27
|
+
super
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Type
|
4
|
+
module Definition
|
5
|
+
# Re-open Collection to add constraint methods
|
6
|
+
class Collection
|
7
|
+
# @overload constrain(constraint)
|
8
|
+
# @param constraint [Type::Definition]
|
9
|
+
# @overload constrain(constraint)
|
10
|
+
# @param constraint [Hash{Type::Defintion=>Type::Defintion}]
|
11
|
+
# a single-element hash whose key is the constraint for keys,
|
12
|
+
# and whose value is a constraint for values
|
13
|
+
# @return [Type::Defintion::Collection::Constrained]
|
14
|
+
def constrain(constraint)
|
15
|
+
Constrained.new(self, constraint)
|
16
|
+
end
|
17
|
+
alias_method :of, :constrain
|
18
|
+
|
19
|
+
# @return [False]
|
20
|
+
def constrained?
|
21
|
+
false
|
22
|
+
end
|
23
|
+
|
24
|
+
# A Constrained collection also validates and casts the contents
|
25
|
+
# of the collection.
|
26
|
+
class Constrained < Collection
|
27
|
+
# @api private (See Type::Defintion::Collection#constrain)
|
28
|
+
def initialize(parent, constraint)
|
29
|
+
@constraints = Array(constraint).flatten.map { |c| Type.find(c) }
|
30
|
+
|
31
|
+
validators << method(:validate_each?)
|
32
|
+
castors << method(:cast_each!)
|
33
|
+
|
34
|
+
super(nil, parent)
|
35
|
+
|
36
|
+
@name = "#{parent.name}(#{@constraints.join('=>')})"
|
37
|
+
end
|
38
|
+
attr_reader :constraints
|
39
|
+
|
40
|
+
# @return [True]
|
41
|
+
def constrained?
|
42
|
+
true
|
43
|
+
end
|
44
|
+
|
45
|
+
# @api private
|
46
|
+
def to_s
|
47
|
+
parent_name = @parent && @parent.name
|
48
|
+
return super unless parent_name
|
49
|
+
"Type::#{parent_name}(#{@constraints.join('=>')})"
|
50
|
+
end
|
51
|
+
|
52
|
+
protected
|
53
|
+
|
54
|
+
# @api private
|
55
|
+
# @param enum [Enumerable]
|
56
|
+
# @return [Boolean]
|
57
|
+
def validate_each?(enum)
|
58
|
+
enum.all? do |item|
|
59
|
+
next @constraints.first.valid?(item) if @constraints.size == 1
|
60
|
+
@constraints.zip(item).all? do |constraint, value|
|
61
|
+
constraint.valid?(value)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# @api private
|
67
|
+
# @param enum [Enumerable]
|
68
|
+
# @return [Enumerable]
|
69
|
+
def cast_each!(enum)
|
70
|
+
enum.map do |item|
|
71
|
+
next @constraints.first.cast!(item) if @constraints.size == 1
|
72
|
+
@constraints.zip(item).map do |constraint, value|
|
73
|
+
constraint.cast!(value)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Type
|
4
|
+
# Re-open Definition to add nilable methods
|
5
|
+
module Definition
|
6
|
+
# Return a nilable representation of this type definition
|
7
|
+
# @return [Type::Definition::Nilable]
|
8
|
+
def nilable
|
9
|
+
Nilable.new(self)
|
10
|
+
end
|
11
|
+
|
12
|
+
# @return [False]
|
13
|
+
def nilable?
|
14
|
+
false
|
15
|
+
end
|
16
|
+
|
17
|
+
# Nilable Type::Definitions are the same as their non-nilable
|
18
|
+
# counterparts with the following exceptions:
|
19
|
+
# - a `nil` value is considered valid
|
20
|
+
# - a `nil` value is returned without casting
|
21
|
+
class Nilable
|
22
|
+
include Definition
|
23
|
+
def initialize(parent)
|
24
|
+
super(nil, parent)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns true if input is nil *or* the input is valid
|
28
|
+
def valid?(input)
|
29
|
+
input.nil? || super
|
30
|
+
end
|
31
|
+
|
32
|
+
# Casts the input unless it is nil
|
33
|
+
def cast!(input)
|
34
|
+
return nil if input.nil?
|
35
|
+
super
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [True]
|
39
|
+
def nilable?
|
40
|
+
true
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [self]
|
44
|
+
def nilable
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [String]
|
49
|
+
def to_s
|
50
|
+
parent_name = @parent && @parent.name
|
51
|
+
parent_name ? "Type::#{parent_name}(nilable)" : super
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Type
|
4
|
+
module Definition
|
5
|
+
# @api private
|
6
|
+
# The Proxy is an in-progress definition, a convenience object to support
|
7
|
+
# the declaration syntax.
|
8
|
+
class Proxy
|
9
|
+
def initialize(name, klass)
|
10
|
+
@name = name
|
11
|
+
@klass = klass
|
12
|
+
end
|
13
|
+
|
14
|
+
# @see Type::Definition::generate() for usage
|
15
|
+
def from(parent, &config)
|
16
|
+
raise ArgumentError, 'Block Required!' unless block_given?
|
17
|
+
|
18
|
+
Type[parent].tap do |resolved_parent|
|
19
|
+
raise ArgumentError unless resolved_parent.kind_of?(@klass)
|
20
|
+
end.refine(@name, &config)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Type
|
4
|
+
class << self
|
5
|
+
# @see Definition::Scalar#generate
|
6
|
+
def scalar(name = nil, &block)
|
7
|
+
Definition::Scalar.generate(name, &block)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module Definition
|
12
|
+
# The Scalar Class is an implementation of Definition interface
|
13
|
+
# that takes 100% of implementation from the base. This is
|
14
|
+
# to differentiate it from Collection, which has additional
|
15
|
+
# constraints.
|
16
|
+
class Scalar
|
17
|
+
include Definition
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/type/error.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Type
|
4
|
+
# An Error class for exceptions raised while validating or casting
|
5
|
+
class Error < ::TypeError
|
6
|
+
def initialize(input, type_definition)
|
7
|
+
@input = input
|
8
|
+
@type_definition = type_definition
|
9
|
+
@cause = $! # aka $ERROR_INFO
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
"<#{self.class.name}: #{message}#{caused_by_clause}>"
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :input, :type_definition, :cause
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def caused_by_clause
|
21
|
+
return '' unless @cause
|
22
|
+
", caused by #{@cause}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Type::CastError is the raised class of Type::Definition#cast!
|
27
|
+
class CastError < Error
|
28
|
+
def message
|
29
|
+
"Could not cast #{input.inspect} with #{type_definition}."
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Type::ValidationError is raised internally in Type::Definition#cast!
|
34
|
+
# when, after casting, an element fails to validate.
|
35
|
+
# @api private
|
36
|
+
class ValidationError < Error
|
37
|
+
def message
|
38
|
+
"#{input.inspect} is not valid #{type_definition}."
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/type/version.rb
ADDED
@@ -0,0 +1,242 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'type'
|
3
|
+
|
4
|
+
RSpec::Matchers.define(:cast) do |input|
|
5
|
+
match do |definition|
|
6
|
+
begin
|
7
|
+
@actual = definition.cast!(input)
|
8
|
+
if @chained
|
9
|
+
failure_message_for_should do
|
10
|
+
"Expected result to be #{@expected.inspect}(#{@expected.class}) " +
|
11
|
+
"but got #{@actual.inspect}(#{@actual.class}) instead"
|
12
|
+
end
|
13
|
+
@expected == @actual
|
14
|
+
else
|
15
|
+
true
|
16
|
+
end
|
17
|
+
rescue Type::CastError => cast_error
|
18
|
+
failure_message_for_should do
|
19
|
+
"#{definition} failed to cast #{input.inspect}(#{input.class}) " +
|
20
|
+
"by raising #{cast_error}(#{cast_error.cause})."
|
21
|
+
end
|
22
|
+
false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
description do
|
27
|
+
"cast #{input.inspect}(#{input.class})"
|
28
|
+
end
|
29
|
+
|
30
|
+
chain(:to) do |expected|
|
31
|
+
description do
|
32
|
+
"cast #{input.inspect}(#{input.class}) to #{expected.inspect}(#{expected.class})"
|
33
|
+
end
|
34
|
+
@chained = true
|
35
|
+
@expected = expected
|
36
|
+
end
|
37
|
+
|
38
|
+
chain(:unchanged) do
|
39
|
+
description do
|
40
|
+
"cast #{input.inspect}(#{input.class}) unchanged"
|
41
|
+
end
|
42
|
+
@chained = true
|
43
|
+
@expected = input
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
RSpec::Matchers.define :validate do |input|
|
48
|
+
match do |definition|
|
49
|
+
definition.valid?(input)
|
50
|
+
end
|
51
|
+
description do
|
52
|
+
"validate #{input.inspect}(#{input.class})"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
shared_examples_for 'Type::Definition::Nilable compatibility' do
|
57
|
+
context 'when nilable' do
|
58
|
+
subject { described_class.nilable }
|
59
|
+
it { should be_a_kind_of Type::Definition::Nilable }
|
60
|
+
it { should be_nilable }
|
61
|
+
it { should cast(nil).to(nil) }
|
62
|
+
it { should validate(nil) }
|
63
|
+
it { should_not cast(Object.new) unless described_class == Type::String }
|
64
|
+
end
|
65
|
+
it { should_not be_a_kind_of Type::Definition::Nilable }
|
66
|
+
it { should_not be_nilable }
|
67
|
+
it { should_not cast(nil) }
|
68
|
+
it { should_not validate(nil) }
|
69
|
+
it { should_not cast(Object.new) unless described_class == Type::String }
|
70
|
+
end
|
71
|
+
|
72
|
+
shared_examples_for 'Type::Definition::Scalar' do
|
73
|
+
include_examples 'Type::Definition::Nilable compatibility'
|
74
|
+
it { should be_a_kind_of Type::Definition }
|
75
|
+
it { should be_a_kind_of Type::Definition::Scalar }
|
76
|
+
end
|
77
|
+
|
78
|
+
shared_examples_for 'Type::Integer' do
|
79
|
+
it_should_behave_like 'Type::Definition::Scalar'
|
80
|
+
|
81
|
+
it { should cast(414).unchanged }
|
82
|
+
it { should cast('123').to(123) }
|
83
|
+
it { should cast(456).to(456) }
|
84
|
+
it { should cast(Math::PI).to(3) } # alabama ftw
|
85
|
+
|
86
|
+
it { should_not cast('not a number') }
|
87
|
+
it { should_not cast(Hash.new) }
|
88
|
+
|
89
|
+
it { should validate(123) }
|
90
|
+
it { should_not validate('123') }
|
91
|
+
end
|
92
|
+
|
93
|
+
shared_examples_for 'bounded Type::Integer' do
|
94
|
+
it_should_behave_like 'Type::Integer'
|
95
|
+
|
96
|
+
let(:range_max) { valid_range.end - (valid_range.exclude_end? ? 1 : 0) }
|
97
|
+
let(:range_min) { valid_range.begin }
|
98
|
+
|
99
|
+
it { should cast(range_max).unchanged }
|
100
|
+
it { should cast(range_min).unchanged }
|
101
|
+
|
102
|
+
it { should_not cast(range_max.next) }
|
103
|
+
it { should_not cast(range_min.pred) }
|
104
|
+
|
105
|
+
it { should validate(range_max) }
|
106
|
+
it { should_not validate(range_max.next) }
|
107
|
+
end
|
108
|
+
|
109
|
+
describe Type::Integer do
|
110
|
+
it_should_behave_like 'Type::Integer'
|
111
|
+
end
|
112
|
+
|
113
|
+
describe Type::Int32 do
|
114
|
+
let(:valid_range) { (-1 << 31)...(1 << 31) }
|
115
|
+
it_should_behave_like 'bounded Type::Integer'
|
116
|
+
end
|
117
|
+
|
118
|
+
describe Type::Int64 do
|
119
|
+
let(:valid_range) { (-1 << 63)...(1 << 63) }
|
120
|
+
it_should_behave_like 'bounded Type::Integer'
|
121
|
+
end
|
122
|
+
|
123
|
+
describe Type::UInt32 do
|
124
|
+
let(:valid_range) { 0...(1 << 32) }
|
125
|
+
it_should_behave_like 'bounded Type::Integer'
|
126
|
+
end
|
127
|
+
|
128
|
+
describe Type::UInt64 do
|
129
|
+
let(:valid_range) { 0...(1 << 64) }
|
130
|
+
it_should_behave_like 'bounded Type::Integer'
|
131
|
+
end
|
132
|
+
|
133
|
+
describe Type::Boolean do
|
134
|
+
it_should_behave_like 'Type::Definition::Scalar'
|
135
|
+
it { should validate true }
|
136
|
+
it { should validate false }
|
137
|
+
it { should_not validate nil }
|
138
|
+
it { should_not validate 'true' }
|
139
|
+
it { should_not validate 'false' }
|
140
|
+
it { should cast(true).unchanged }
|
141
|
+
it { should cast(false).unchanged }
|
142
|
+
end
|
143
|
+
|
144
|
+
shared_examples_for 'Type::Float' do
|
145
|
+
it_should_behave_like 'Type::Definition::Scalar'
|
146
|
+
it { should cast(10).to(10.0) }
|
147
|
+
it { should cast(12.3).unchanged }
|
148
|
+
it { should cast('12.3').to(12.3) }
|
149
|
+
it { should cast('123e-1').to(12.3) }
|
150
|
+
it { should cast('12.3e10').to(123000000000.0) }
|
151
|
+
it { should cast('123e10').to(1230000000000.0) }
|
152
|
+
it { should_not cast('a string') }
|
153
|
+
it { should_not cast(Hash.new) }
|
154
|
+
it { should validate(12.3) }
|
155
|
+
it { should_not validate(12) }
|
156
|
+
end
|
157
|
+
|
158
|
+
describe Type::Float do
|
159
|
+
include_examples 'Type::Float'
|
160
|
+
it { should validate(Float::INFINITY) }
|
161
|
+
it { should validate(-Float::INFINITY) }
|
162
|
+
end
|
163
|
+
|
164
|
+
describe Type::Float32 do
|
165
|
+
include_examples 'Type::Float'
|
166
|
+
it { should_not validate(Float::INFINITY) }
|
167
|
+
it { should_not validate(-Float::INFINITY) }
|
168
|
+
end
|
169
|
+
|
170
|
+
describe Type::Float64 do
|
171
|
+
include_examples 'Type::Float'
|
172
|
+
it { should_not validate(Float::INFINITY) }
|
173
|
+
it { should_not validate(-Float::INFINITY) }
|
174
|
+
end
|
175
|
+
|
176
|
+
describe Type::String do
|
177
|
+
its(:to_s) { should match(/Type::String/) }
|
178
|
+
it_should_behave_like 'Type::Definition::Scalar'
|
179
|
+
it { should cast(:abc).to('abc') }
|
180
|
+
end
|
181
|
+
|
182
|
+
describe Type::Array do
|
183
|
+
its(:to_s) { should match(/Type::Array/) }
|
184
|
+
it { should be_a_kind_of Type::Definition::Collection }
|
185
|
+
it { should validate(['asdf']) }
|
186
|
+
it { should cast(['foo']).unchanged }
|
187
|
+
it { should cast(['asdf', 1]).unchanged }
|
188
|
+
end
|
189
|
+
|
190
|
+
describe Type::Array.of(:String) do
|
191
|
+
its(:to_s) { should match(/Type::Array\(.*String.*\)/) }
|
192
|
+
it { should be_a_kind_of Type::Definition::Collection::Constrained }
|
193
|
+
it { should validate(['asdf']) }
|
194
|
+
it { should_not validate([nil, 'asdf']) }
|
195
|
+
it { should_not validate([:asdf]) }
|
196
|
+
it { should cast([:abc, 1]).to(['abc', '1']) }
|
197
|
+
it { should_not cast([nil, 1]) }
|
198
|
+
end
|
199
|
+
|
200
|
+
describe Type::Array.of(:String?) do
|
201
|
+
it { should be_a_kind_of Type::Definition::Collection::Constrained }
|
202
|
+
it { should validate(['asdf']) }
|
203
|
+
it { should validate([nil, 'asdf']) }
|
204
|
+
it { should_not validate([:asdf]) }
|
205
|
+
it { should cast([:abc, 1]).to(['abc', '1']) }
|
206
|
+
it { should cast([nil, 1]).to([nil, '1']) }
|
207
|
+
end
|
208
|
+
|
209
|
+
describe Type::Hash do
|
210
|
+
its(:to_s) { should match(/Type::Hash/) }
|
211
|
+
it { should cast([[1, 2], [3, 4]]).to(1 => 2, 3 => 4) }
|
212
|
+
it { should_not cast(17) }
|
213
|
+
end
|
214
|
+
|
215
|
+
describe Type::Hash.of(:String => :Integer) do
|
216
|
+
its(:to_s) { should match(/Type::Hash\(.*String.*Integer.*\)/) }
|
217
|
+
it { should be_a_kind_of Type::Definition::Collection::Constrained }
|
218
|
+
it { should validate('foo' => 12) }
|
219
|
+
it { should_not validate(foo: 12) }
|
220
|
+
it { should_not validate('foo' => '12') }
|
221
|
+
it { should cast('foo' => '12', :bar => 3).to('foo' => 12, 'bar' => 3) }
|
222
|
+
it { should cast('foo' => 12, 'bar' => 3).unchanged }
|
223
|
+
it { should cast([['12', 34], [56, '78']]).to('12' => 34, '56' => 78) }
|
224
|
+
it { should_not cast('foo' => 'foo') }
|
225
|
+
end
|
226
|
+
|
227
|
+
describe Type::Set do
|
228
|
+
it { should_not validate([123, 456]) }
|
229
|
+
it { should validate(Set.new([123, 456])) }
|
230
|
+
it { should_not validate(17) }
|
231
|
+
it { should cast([123, 456]).to(Set.new([123, 456])) }
|
232
|
+
it { should cast(Set.new([123, 456])).to(Set.new([123, 456])) }
|
233
|
+
it { should_not cast(17) }
|
234
|
+
end
|
235
|
+
|
236
|
+
describe Type::Set.of(:Integer) do
|
237
|
+
its(:to_s) { should match(/Type::Set(.*Integer.*)/) }
|
238
|
+
it { should validate(Set.new([1, 2, 3, 4])) }
|
239
|
+
it { should_not validate([1, 2, 3, 4]) }
|
240
|
+
it { should cast(Set.new([1, 2, 3, 4])).unchanged }
|
241
|
+
it { should cast([1, 2, 3, 4]).to(Set.new([1, 2, 3, 4])) }
|
242
|
+
end
|