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,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
|