type 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|