type 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -0,0 +1,6 @@
1
+ # encoding: utf-8
2
+
3
+ module Type
4
+ # The Type gem is semantically versioned.
5
+ VERSION = '0.1.0'
6
+ end
@@ -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