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,17 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
echo -e "\033[0;36mRuby Appraiser: running\033[0m"
|
3
|
+
bundle exec ruby-appraiser --mode=staged
|
4
|
+
result_code=$?
|
5
|
+
if [ $result_code -gt "0" ]; then
|
6
|
+
echo -en "\033[0;31m" # RED
|
7
|
+
echo "[✘] Ruby Appraiser found newly-created defects and "
|
8
|
+
echo " has blocked your commit."
|
9
|
+
echo " Fix the defects and commit again."
|
10
|
+
echo " To bypass, commit again with --no-verify."
|
11
|
+
echo -en "\033[0m" # RESET
|
12
|
+
exit $result_code
|
13
|
+
else
|
14
|
+
echo -en "\033[0;32m" # GREEN
|
15
|
+
echo "[✔] Ruby Appraiser ok"
|
16
|
+
echo -en "\033[0m" #RESET
|
17
|
+
fi
|
data/.gitignore
ADDED
data/CHANGELOG.md
ADDED
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# Contributing
|
2
|
+
|
3
|
+
`Type` is [Apache-liennsed](LICENSE.txt).
|
4
|
+
|
5
|
+
## Git-Flow
|
6
|
+
|
7
|
+
`Type` follows the [git-flow][] branching model, which means that every
|
8
|
+
commit on `master` is a release. The default working branch is `develop`, so
|
9
|
+
in general please keep feature pull-requests based against the current
|
10
|
+
`develop`.
|
11
|
+
|
12
|
+
- fork the repo on github
|
13
|
+
- use the git-flow model to start your feature (based on develop) or
|
14
|
+
hotfix (based on master)
|
15
|
+
- make some commits (please include specs & changelog)
|
16
|
+
- submit a pull-request
|
17
|
+
|
18
|
+
## Bug Reporting
|
19
|
+
|
20
|
+
Please include clear steps-to-reproduce. Spec files are especially welcome;
|
21
|
+
a failing spec can be contributed as a pull-request against `master`, but make
|
22
|
+
sure it's not already fixed in develop.
|
23
|
+
|
24
|
+
## Documentation
|
25
|
+
|
26
|
+
`Type` uses YARDOC, and so must your pull-requests if you add functionality or
|
27
|
+
change the api.
|
28
|
+
|
29
|
+
## Ruby Appraiser
|
30
|
+
|
31
|
+
`Type` uses the [ruby-appraiser][] gem via [pre-commit][] hook, which can be
|
32
|
+
activated by installing [icefox/git-hooks][] and running `git-hooks --install`.
|
33
|
+
Reek and Rubocop are strong guidelines; use them to reduce defects as much as
|
34
|
+
you can, but if you believe clarity will be sacrificed they can be bypassed
|
35
|
+
with the `--no-verify` flag.
|
36
|
+
|
37
|
+
[git-flow]: http://nvie.com/posts/a-successful-git-branching-model/
|
38
|
+
[pre-commit]: .githooks/pre-commit/ruby-appraiser
|
39
|
+
[ruby-appraiser]: https://github.com/simplymeasured/ruby-appraiser
|
40
|
+
[icefox/git-hooks]: https://github.com/icefox/git-hooks
|
41
|
+
[pull-request-hack]: http://felixge.de/2013/03/11/the-pull-request-hack.html
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright 2013 Simply Measured
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,218 @@
|
|
1
|
+
# Type
|
2
|
+
|
3
|
+
`Type` is a ruby library for type validation and type casting. It allows you to
|
4
|
+
have guarantees on data structures, and is exceptionally useful for working with
|
5
|
+
external APIs that blow up opaquely on type errors.
|
6
|
+
|
7
|
+
See [the Changelog](CHANGELOG.md) for version history.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
gem 'type'
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install type
|
22
|
+
|
23
|
+
## Basic API
|
24
|
+
|
25
|
+
`Type::Definition`s respond to two public methods: `#cast!` and `valid?`, each
|
26
|
+
of which take a single argument and either cast or validate the given object.
|
27
|
+
For convenience, named type definitions have global aliases defined on `Type`:
|
28
|
+
|
29
|
+
~~~ ruby
|
30
|
+
# For example, `Type::Int32`, which is a built-in `Type::Definition`
|
31
|
+
Type::Int32?(input) # alias for Type::Int32.valid?(input)
|
32
|
+
Type::Int32!(input) # alias for Type::Int32.cast!(input)
|
33
|
+
~~~
|
34
|
+
|
35
|
+
## Usage
|
36
|
+
|
37
|
+
`Type` comes with a variety of built-in type defintions, which can be used for
|
38
|
+
validation or casting.
|
39
|
+
|
40
|
+
### Scalar Type Definitions:
|
41
|
+
|
42
|
+
The most basic type definitions are scalar
|
43
|
+
|
44
|
+
~~~ ruby
|
45
|
+
# Validating
|
46
|
+
Type::Int32?(1)
|
47
|
+
# => true
|
48
|
+
Type::Int32?(8_589_934_592) # out of Int32 range
|
49
|
+
# => false
|
50
|
+
Type::Int32?(3.14)
|
51
|
+
# => false
|
52
|
+
Type::Int32?('three')
|
53
|
+
# => false
|
54
|
+
|
55
|
+
# Casting
|
56
|
+
Type::Int32!(1)
|
57
|
+
# => 1
|
58
|
+
Type::Int32!(1<<33)
|
59
|
+
Type::CastError: Could not cast 8589934592(Fixnum) with Type::Int32
|
60
|
+
Type::Int32!(3.14)
|
61
|
+
# => 3
|
62
|
+
Type::Int32!('three')
|
63
|
+
#! Type::CastError: Could not cast "three"(String) with Type::Int32
|
64
|
+
~~~
|
65
|
+
|
66
|
+
### Nilable Type Definitions:
|
67
|
+
|
68
|
+
Any `Type::Definition` can be declared nilable -- that is, it will report `nil`
|
69
|
+
as a valid value, and will ignore `nil` when casting.
|
70
|
+
|
71
|
+
~~~ ruby
|
72
|
+
# Validating
|
73
|
+
Type::Int32.valid?(nil)
|
74
|
+
# => false
|
75
|
+
Type::Int32.nilable.valid?(nil)
|
76
|
+
# => true
|
77
|
+
|
78
|
+
# Casting
|
79
|
+
Type::Int32.cast!(nil)
|
80
|
+
#! Type::CastError: Could not cast nil(NilClass) with Type::Int32
|
81
|
+
Type::Int32.nilable.cast!(nil)
|
82
|
+
# => nil
|
83
|
+
~~~
|
84
|
+
|
85
|
+
### Collection Type Definitions:
|
86
|
+
|
87
|
+
`Type` also comes with built-in, named definitions for `Array`, `Set`, and
|
88
|
+
`Hash` collections, which are available in the same manner:
|
89
|
+
|
90
|
+
~~~ ruby
|
91
|
+
# Validating
|
92
|
+
Type::Array?([1,2,3])
|
93
|
+
# => true
|
94
|
+
Type::Hash?({'foo'=>'bar'})
|
95
|
+
# => true
|
96
|
+
Type::Set?([1,2,3])
|
97
|
+
# => false
|
98
|
+
Type::Set?(Set.new([1,2,3]))
|
99
|
+
# => true
|
100
|
+
|
101
|
+
# Casting
|
102
|
+
Type::Array!([1,2,3])
|
103
|
+
# => [1,2,3]
|
104
|
+
Type::Hash!([['foo','bar']])
|
105
|
+
# => {'foo'=>'bar'}
|
106
|
+
Type::Set!([1,2,3])
|
107
|
+
# => <Set: {1, 2, 3}>
|
108
|
+
Type::Set!('foo')
|
109
|
+
#! Type::CastError: Could not cast "foo"(String) with Type::Set
|
110
|
+
~~~
|
111
|
+
|
112
|
+
### Constrained Collection Type Definitions:
|
113
|
+
|
114
|
+
The real power of type-casting collections is when their contents can also be
|
115
|
+
constrained:
|
116
|
+
|
117
|
+
~~~ ruby
|
118
|
+
# Validating:
|
119
|
+
# specify any Type::Definition, or the name of a globally-registered one:
|
120
|
+
Type::Array.of(Type::Int32).valid?([12, 13])
|
121
|
+
# => true
|
122
|
+
Type::Array.of(:Int32).valid?(['12', '13'])
|
123
|
+
# => false
|
124
|
+
Type::Array.of(:Int32).valid?(['three','two'])
|
125
|
+
# => false
|
126
|
+
Type::Hash.of(:String => :Int64).valid?({'id'=>'1234567890'})
|
127
|
+
# => false
|
128
|
+
Type::Hash.of(:String => :Int64).valid?({'id'=>1234567890})
|
129
|
+
# => true
|
130
|
+
|
131
|
+
# Casting:
|
132
|
+
Type::Array.of(:Int32).cast!([12, 13])
|
133
|
+
# => [12, 13]
|
134
|
+
Type::Array.of(:Int32).cast!(['12', '13'])
|
135
|
+
# => [12, 13]
|
136
|
+
Type::Array.of(:Int32).cast!(['three','two'])
|
137
|
+
#! Type::CastError: Could not cast ["three", "two"](Array) with Type::Array(Int32),
|
138
|
+
#! caused by <Type::CastError: Could not cast "three"(String) with Type::Int32>
|
139
|
+
Type::Hash.of(:String => :Int64).cast!({'id'=>'1234567890'})
|
140
|
+
# => {'id'=>1234567890}
|
141
|
+
Type::Hash.of(:String => :Int64).cast!({'id'=>1234567890})
|
142
|
+
# => {'id'=>1234567890}
|
143
|
+
~~~
|
144
|
+
|
145
|
+
### Nilable Constrained Collection Type Definitions
|
146
|
+
|
147
|
+
The contents of your constrained collections can also be nilable:
|
148
|
+
|
149
|
+
~~~ ruby
|
150
|
+
# Validating:
|
151
|
+
Type::Array.of(Type::Int32.nilable).valid?([nil, 3])
|
152
|
+
# => true
|
153
|
+
Type::Array.of(:Int32?).valid?([nil,4])
|
154
|
+
# => true
|
155
|
+
|
156
|
+
# Casting
|
157
|
+
Type::Array.of(Type::Int32.nilable).cast!([nil, '3'])
|
158
|
+
# => [nil, 3]
|
159
|
+
Type::Array.of(:Int32?).cast!([nil,4])
|
160
|
+
# => [nil, 4]
|
161
|
+
~~~
|
162
|
+
|
163
|
+
## Advanced Usage
|
164
|
+
|
165
|
+
### Custom Type Defintions
|
166
|
+
|
167
|
+
~~~ ruby
|
168
|
+
my_int32 = Type.scalar do
|
169
|
+
int32_range = (-(1 << 31) ... (1 << 31))
|
170
|
+
validate do |input|
|
171
|
+
input.kind_of?(Integer) && int32_range.include?(input)
|
172
|
+
end
|
173
|
+
cast do |input|
|
174
|
+
Kernel::Integer(input)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
my_int32.valid?('100') # => false
|
179
|
+
my_int32.valid?(100) # => true
|
180
|
+
my_int32.cast!(1<<10) # => 1024
|
181
|
+
my_int32.cast!("100") # => 100
|
182
|
+
|
183
|
+
|
184
|
+
simple_int32 = Type.scalar.from(Integer) do
|
185
|
+
int32_range = (-(1 << 31) ... (1 << 31))
|
186
|
+
validate do |input|
|
187
|
+
int32_range.include?(input)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
simple_int32.valid?('100') # => false
|
192
|
+
simple_int32.valid?(100) # => true
|
193
|
+
simple_int32.cast!(1<<10) # => 1024
|
194
|
+
simple_int32.cast!("100") # => 100
|
195
|
+
|
196
|
+
Type.scalar(:OddInt).from(:Integer) do
|
197
|
+
validate(&:odd?)
|
198
|
+
cast do |input|
|
199
|
+
input.even? ? input + 1 : input
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
Type::OddInt?(4)
|
204
|
+
# => false
|
205
|
+
Type::OddInt!(4)
|
206
|
+
# => 5
|
207
|
+
~~~
|
208
|
+
|
209
|
+
If you find that you're using one or more custom type definitions on a regular
|
210
|
+
basis, please consider contributing them.
|
211
|
+
|
212
|
+
## Contributing
|
213
|
+
|
214
|
+
1. Fork it
|
215
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
216
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
217
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
218
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/lib/type.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'type/version'
|
4
|
+
require 'type/error'
|
5
|
+
require 'type/definition'
|
6
|
+
|
7
|
+
# Type is a library for type-casting and type-validation
|
8
|
+
module Type
|
9
|
+
class << self
|
10
|
+
# @overload find(query)
|
11
|
+
# @param query [Type::Definition]
|
12
|
+
# @overload find(query)
|
13
|
+
# @param query [String, Symbol]
|
14
|
+
# Find a named Type::Defintion. If the query ends with a ?,
|
15
|
+
# a nilable representation of the reolved type definition is returned.
|
16
|
+
# @return [Type::Definition]
|
17
|
+
def find(query)
|
18
|
+
return query if query.kind_of?(Definition)
|
19
|
+
|
20
|
+
query = String(query)
|
21
|
+
nilable = query.end_with?('?') && query.slice!(-1)
|
22
|
+
|
23
|
+
definition = const_get(query)
|
24
|
+
(nilable ? definition.nilable : definition)
|
25
|
+
end
|
26
|
+
alias_method :[], :find
|
27
|
+
|
28
|
+
# @api private
|
29
|
+
# @param [Type::Definition]
|
30
|
+
# @return [void]
|
31
|
+
def register(definition)
|
32
|
+
if (name = definition.name)
|
33
|
+
const_set(name, definition)
|
34
|
+
(class << self; self; end).instance_exec do
|
35
|
+
define_method("#{name}!") { |x| definition.cast!(x) }
|
36
|
+
define_method("#{name}?") { |x| definition.valid?(x) }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Required after, since they rely on the above methods.
|
43
|
+
require 'type/builtin'
|
44
|
+
end
|
data/lib/type/builtin.rb
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
# The built-in types are defined here.
|
4
|
+
module Type
|
5
|
+
scalar(:Integer) do
|
6
|
+
validate do |input|
|
7
|
+
input.kind_of?(::Integer)
|
8
|
+
end
|
9
|
+
cast do |input|
|
10
|
+
Kernel::Integer(input)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
scalar(:Int32).from(:Integer) do
|
15
|
+
int32_range = (-(1 << 31) ... (1 << 31))
|
16
|
+
validate do |input|
|
17
|
+
int32_range.include?(input)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
scalar(:Int64).from(:Integer) do
|
22
|
+
int64_range = (-(1 << 63) ... (1 << 63))
|
23
|
+
validate do |input|
|
24
|
+
int64_range.include?(input)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
scalar(:UInt32).from(:Integer) do
|
29
|
+
int32_range = (0 ... (1 << 32))
|
30
|
+
validate do |input|
|
31
|
+
int32_range.include?(input)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
scalar(:UInt64).from(:Integer) do
|
36
|
+
int64_range = (0 ... (1 << 64))
|
37
|
+
validate do |input|
|
38
|
+
int64_range.include?(input)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
scalar(:Float) do
|
43
|
+
validate do |input|
|
44
|
+
input.kind_of?(::Float)
|
45
|
+
end
|
46
|
+
cast do |input|
|
47
|
+
Kernel::Float(input)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
scalar(:Float32).from(:Float) do
|
52
|
+
validate do |input|
|
53
|
+
input.finite?
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
scalar(:Float64).from(:Float) do
|
58
|
+
validate do |input|
|
59
|
+
input.finite?
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
scalar(:Boolean) do
|
64
|
+
require 'set'
|
65
|
+
booleans = Set.new([true, false])
|
66
|
+
validate do |input|
|
67
|
+
booleans.include?(input)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
scalar(:String) do
|
72
|
+
validate do |input|
|
73
|
+
input.kind_of?(::String)
|
74
|
+
end
|
75
|
+
cast do |input|
|
76
|
+
Kernel::String(input)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
collection(:Array) do
|
81
|
+
validate do |input|
|
82
|
+
input.kind_of?(::Array)
|
83
|
+
end
|
84
|
+
cast do |input|
|
85
|
+
Kernel::Array(input)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
collection(:Hash) do
|
90
|
+
validate do |input|
|
91
|
+
input.kind_of?(::Hash)
|
92
|
+
end
|
93
|
+
cast do |input|
|
94
|
+
if Kernel.respond_to?(:Hash)
|
95
|
+
Kernel::Hash(input)
|
96
|
+
else
|
97
|
+
::Hash[input]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
collection(:Set) do
|
103
|
+
require 'set'
|
104
|
+
validate do |input|
|
105
|
+
input.kind_of?(::Set)
|
106
|
+
end
|
107
|
+
cast do |input|
|
108
|
+
::Set.new(input)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|