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.
@@ -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