kind 1.7.0 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.sh +20 -0
- data/.travis.yml +3 -1
- data/Gemfile +31 -6
- data/README.md +549 -55
- data/kind.gemspec +2 -2
- data/lib/kind.rb +59 -34
- data/lib/kind/active_model/kind_validator.rb +96 -0
- data/lib/kind/active_model/validation.rb +7 -0
- data/lib/kind/checker.rb +41 -3
- data/lib/kind/error.rb +12 -2
- data/lib/kind/maybe.rb +36 -11
- data/lib/kind/types.rb +14 -3
- data/lib/kind/undefined.rb +1 -1
- data/lib/kind/validator.rb +40 -0
- data/lib/kind/version.rb +1 -1
- data/test.sh +11 -0
- metadata +9 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3bb8430c79ca97835ebdf6604e0c5afc4301f912db778e5f930fb9e699ae08c1
|
4
|
+
data.tar.gz: 0557c3df900513e5ec7d45fec79fdf7502f0cb06a8e1538ff935e0228ef11136
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0ad03f0d27484914f45607d9e1fd7cfdaf8000d700e31f069b274b5f977f42a8736a3d03d0cba1f3d06679f0f5a05b56d7c3b4acfd2a90cce20e524de6f9154a
|
7
|
+
data.tar.gz: 6f8e54590261939a43a52895921d80fb693a0e16361d011f53a80f1d7269aaade1e8871ae44374586ec488c026a02fced9057fdfcb300c532bd3a018498e954a
|
data/.travis.sh
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
bundle exec rake test
|
4
|
+
|
5
|
+
ruby_v=$(ruby -v)
|
6
|
+
|
7
|
+
ACTIVEMODEL_VERSION='3.2' bundle update && bundle exec rake test
|
8
|
+
ACTIVEMODEL_VERSION='4.0' bundle update && bundle exec rake test
|
9
|
+
ACTIVEMODEL_VERSION='4.1' bundle update && bundle exec rake test
|
10
|
+
ACTIVEMODEL_VERSION='4.2' bundle update && bundle exec rake test
|
11
|
+
ACTIVEMODEL_VERSION='5.0' bundle update && bundle exec rake test
|
12
|
+
ACTIVEMODEL_VERSION='5.1' bundle update && bundle exec rake test
|
13
|
+
|
14
|
+
if [[ ! $ruby_v =~ '2.2.0' ]]; then
|
15
|
+
ACTIVEMODEL_VERSION='5.2' bundle update && bundle exec rake test
|
16
|
+
fi
|
17
|
+
|
18
|
+
if [[ $ruby_v =~ '2.5.' ]] || [[ $ruby_v =~ '2.6.' ]] || [[ $ruby_v =~ '2.7.' ]]; then
|
19
|
+
ACTIVEMODEL_VERSION='6.0' bundle update && bundle exec rake test
|
20
|
+
fi
|
data/.travis.yml
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
---
|
2
2
|
language: ruby
|
3
3
|
|
4
|
+
sudo: false
|
5
|
+
|
4
6
|
cache: bundler
|
5
7
|
|
6
8
|
rvm:
|
@@ -22,7 +24,7 @@ before_script:
|
|
22
24
|
- chmod +x ./cc-test-reporter
|
23
25
|
- "./cc-test-reporter before-build"
|
24
26
|
|
25
|
-
script:
|
27
|
+
script: "./.travis.sh"
|
26
28
|
|
27
29
|
after_success:
|
28
30
|
- "./cc-test-reporter after-build -t simplecov"
|
data/Gemfile
CHANGED
@@ -1,8 +1,33 @@
|
|
1
|
-
source
|
1
|
+
source 'https://rubygems.org'
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
|
4
|
+
|
5
|
+
activemodel_version = ENV['ACTIVEMODEL_VERSION']
|
6
|
+
|
7
|
+
activemodel = case activemodel_version
|
8
|
+
when '3.2' then '3.2.22'
|
9
|
+
when '4.0' then '4.0.13'
|
10
|
+
when '4.1' then '4.1.16'
|
11
|
+
when '4.2' then '4.2.11'
|
12
|
+
when '5.0' then '5.0.7'
|
13
|
+
when '5.1' then '5.1.7'
|
14
|
+
when '5.2' then '5.2.4'
|
15
|
+
when '6.0' then '6.0.0'
|
16
|
+
end
|
17
|
+
|
18
|
+
group :test do
|
19
|
+
if activemodel_version
|
20
|
+
gem 'activesupport', activemodel, require: false
|
21
|
+
gem 'activemodel', activemodel, require: false
|
22
|
+
gem 'minitest', activemodel_version < '4.1' ? '~> 4.2' : '~> 5.0'
|
23
|
+
else
|
24
|
+
gem 'minitest', '~> 5.0'
|
25
|
+
end
|
5
26
|
|
6
|
-
gem
|
7
|
-
|
8
|
-
|
27
|
+
gem 'simplecov', require: false
|
28
|
+
end
|
29
|
+
|
30
|
+
gem 'rake', '~> 13.0'
|
31
|
+
|
32
|
+
# Specify your gem's dependencies in type_validator.gemspec
|
33
|
+
gemspec
|
data/README.md
CHANGED
@@ -6,36 +6,49 @@
|
|
6
6
|
|
7
7
|
# Kind <!-- omit in toc -->
|
8
8
|
|
9
|
-
|
9
|
+
A simple type system (at runtime) for Ruby - free of dependencies.
|
10
10
|
|
11
11
|
**Motivation:**
|
12
12
|
|
13
13
|
As a creator of Ruby gems, I have a common need that I have to handle in many of my projects: type checking of method arguments.
|
14
14
|
|
15
|
-
One of the goals of this project is to do simple type checking like `"some string".is_a?(String)`, but
|
15
|
+
One of the goals of this project is to do simple type checking like `"some string".is_a?(String)`, but, exposing useful abstractions to do it. e.g: [Kind.of.\<Type\> methods](#verifying-the-kind-of-some-object), [active model validations](#kindvalidator-activemodelvalidations), [maybe monad](#kindmaybe).
|
16
16
|
|
17
17
|
## Table of Contents <!-- omit in toc -->
|
18
18
|
- [Required Ruby version](#required-ruby-version)
|
19
19
|
- [Installation](#installation)
|
20
20
|
- [Usage](#usage)
|
21
|
-
- [Verifying the kind of some object](#verifying-the-kind-of-some-object)
|
22
|
-
|
21
|
+
- [Kind.of.\<Type\>() - Verifying the kind of some object](#kindoftype---verifying-the-kind-of-some-object)
|
22
|
+
- [Method aliases to perform a strict validation](#method-aliases-to-perform-a-strict-validation)
|
23
|
+
- [Kind.of.\<Type\>.or_nil()](#kindoftypeor_nil)
|
24
|
+
- [Kind.of.\<Type\>.instance?()](#kindoftypeinstance)
|
25
|
+
- [Kind.is.\<Type\>() - Verifying if some class/module is the expected kind.](#kindistype---verifying-if-some-classmodule-is-the-expected-kind)
|
23
26
|
- [How to create a new type checker?](#how-to-create-a-new-type-checker)
|
24
27
|
- [Creating/Verifiyng type checkers dynamically](#creatingverifiyng-type-checkers-dynamically)
|
25
|
-
- [Registering new (custom) type
|
28
|
+
- [Registering new (custom) type checker](#registering-new-custom-type-checker)
|
26
29
|
- [What happens if a custom type checker has a namespace?](#what-happens-if-a-custom-type-checker-has-a-namespace)
|
27
30
|
- [Type checkers](#type-checkers)
|
28
31
|
- [Classes' type checkers](#classes-type-checkers)
|
29
32
|
- [Modules' type checkers](#modules-type-checkers)
|
30
33
|
- [Specials' type checkers](#specials-type-checkers)
|
34
|
+
- [Kind::Validator (ActiveModel::Validations)](#kindvalidator-activemodelvalidations)
|
35
|
+
- [Usage](#usage-1)
|
36
|
+
- [Defining the default validation strategy](#defining-the-default-validation-strategy)
|
37
|
+
- [Using the `allow_nil` and `strict` options](#using-the-allow_nil-and-strict-options)
|
31
38
|
- [Kind::Undefined](#kindundefined)
|
32
39
|
- [Kind.of.\<Type\>.or_undefined()](#kindoftypeor_undefined)
|
33
40
|
- [Kind::Maybe](#kindmaybe)
|
34
|
-
|
35
|
-
- [Kind::Maybe#
|
41
|
+
- [Replacing blocks by lambdas](#replacing-blocks-by-lambdas)
|
42
|
+
- [Kind::Maybe[] and Kind::Maybe#then method aliases](#kindmaybe-and-kindmaybethen-method-aliases)
|
43
|
+
- [Replacing blocks by lambdas](#replacing-blocks-by-lambdas-1)
|
44
|
+
- [Kind::None() and Kind::Some()](#kindnone-and-kindsome)
|
36
45
|
- [Kind.of.Maybe()](#kindofmaybe)
|
37
46
|
- [Kind::Optional](#kindoptional)
|
47
|
+
- [Replacing blocks by lambdas](#replacing-blocks-by-lambdas-2)
|
48
|
+
- [Kind.of.\<Type\>.as_optional](#kindoftypeas_optional)
|
49
|
+
- [Kind::Maybe#try](#kindmaybetry)
|
38
50
|
- [Kind::Empty](#kindempty)
|
51
|
+
- [Similar Projects](#similar-projects)
|
39
52
|
- [Development](#development)
|
40
53
|
- [Contributing](#contributing)
|
41
54
|
- [License](#license)
|
@@ -76,14 +89,14 @@ sum(1, 1) # 2
|
|
76
89
|
sum('1', 1) # Kind::Error ("\"1\" expected to be a kind of Numeric")
|
77
90
|
```
|
78
91
|
|
79
|
-
### Verifying the kind of some object
|
92
|
+
### Kind.of.\<Type\>() - Verifying the kind of some object
|
80
93
|
|
81
94
|
By default, basic verifications are strict. So, when you perform `Kind.of.Hash(value)`, if the given value was a Hash, the value itself will be returned, but if it isn't the right type, an error will be raised.
|
82
95
|
|
83
96
|
```ruby
|
84
|
-
Kind.of.Hash(nil)
|
85
|
-
Kind.of.Hash('')
|
86
|
-
Kind.of.Hash(
|
97
|
+
Kind.of.Hash(nil) # **raise Kind::Error, "nil expected to be a kind of Hash"**
|
98
|
+
Kind.of.Hash('') # raise Kind::Error, "'' expected to be a kind of Hash"
|
99
|
+
Kind.of.Hash(a: 1) # {a: 1}
|
87
100
|
|
88
101
|
# ---
|
89
102
|
|
@@ -92,6 +105,15 @@ Kind.of.Boolean(true) # true
|
|
92
105
|
Kind.of.Boolean(false) # false
|
93
106
|
```
|
94
107
|
|
108
|
+
> **Note:** `Kind.of.<Type>` supports the to_proc protocol.
|
109
|
+
> And it will perform a strict validation as expected.
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
collection = [ {number: 1}, 'number 2', {number: 3}, :number_4 ]
|
113
|
+
|
114
|
+
collection.map(&Kind.of.Hash) # Kind::Error ("number 2" expected to be a kind of Hash)
|
115
|
+
```
|
116
|
+
|
95
117
|
When the verified value is nil, it is possible to define a default value with the same type to be returned.
|
96
118
|
|
97
119
|
```ruby
|
@@ -104,7 +126,25 @@ Kind.of.Hash(value, or: {}) # {}
|
|
104
126
|
Kind.of.Boolean(nil, or: true) # true
|
105
127
|
```
|
106
128
|
|
107
|
-
As an alternative syntax, you can use the `Kind::Of` instead of the `Kind.of` method. e.g: `Kind::Of::Hash('')`
|
129
|
+
> **Note:** As an alternative syntax, you can use the `Kind::Of` instead of the `Kind.of` method. e.g: `Kind::Of::Hash('')`
|
130
|
+
|
131
|
+
#### Method aliases to perform a strict validation
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
Kind.of.Hash[nil] # raise Kind::Error, "nil expected to be a kind of Hash"
|
135
|
+
Kind.of.Hash[''] # raise Kind::Error, "'' expected to be a kind of Hash"
|
136
|
+
Kind.of.Hash[a: 1] # {a: 1}
|
137
|
+
Kind.of.Hash['', or: {}] # {}
|
138
|
+
|
139
|
+
# or
|
140
|
+
|
141
|
+
Kind.of.Hash.instance(nil) # raise Kind::Error, "nil expected to be a kind of Hash"
|
142
|
+
Kind.of.Hash.instance('') # raise Kind::Error, "'' expected to be a kind of Hash"
|
143
|
+
Kind.of.Hash.instance(a: 1) # {a: 1}
|
144
|
+
Kind.of.Hash.instance('', or: {}) # {}
|
145
|
+
```
|
146
|
+
|
147
|
+
### Kind.of.\<Type\>.or_nil()
|
108
148
|
|
109
149
|
But if you don't need a strict type verification, use the `.or_nil` method.
|
110
150
|
|
@@ -118,36 +158,62 @@ Kind.of.Boolean.or_nil('') # nil
|
|
118
158
|
Kind.of.Boolean.or_nil(true) # true
|
119
159
|
```
|
120
160
|
|
121
|
-
|
161
|
+
### Kind.of.\<Type\>.instance?()
|
162
|
+
|
163
|
+
Use the method `.instance?` to verify if the given object has the expected type.
|
122
164
|
|
123
165
|
```ruby
|
124
|
-
Kind.of.Hash.instance?(
|
125
|
-
#
|
166
|
+
Kind.of.Hash.instance?({}) # true
|
167
|
+
Kind.of.Hash.instance?({}, HashWithIndifferentAccess.new) # true
|
168
|
+
|
169
|
+
Kind.of.Hash.instance?('') # false
|
170
|
+
Kind.of.Hash.instance?({}, '') # false
|
126
171
|
|
127
172
|
# ---
|
128
173
|
|
129
|
-
Kind.of.Boolean.instance?(
|
130
|
-
Kind.of.Boolean.instance?(true)
|
131
|
-
|
174
|
+
Kind.of.Boolean.instance?(true) # true
|
175
|
+
Kind.of.Boolean.instance?(true, false) # true
|
176
|
+
|
177
|
+
Kind.of.Boolean.instance?(nil) # false
|
178
|
+
Kind.of.Boolean.instance?(false, true, nil) # false
|
132
179
|
```
|
133
180
|
|
134
|
-
|
181
|
+
> **Note:** When `.instance?` is called without an argument,
|
182
|
+
> it will return a lambda which will perform the kind verification.
|
135
183
|
|
136
184
|
```ruby
|
137
|
-
|
138
|
-
Kind.of.Hash[''] # raise Kind::Error, "'' expected to be a kind of Hash"
|
139
|
-
Kind.of.Hash[a: 1] # {a: 1}
|
140
|
-
Kind.of.Hash['', or: {}] # {}
|
185
|
+
collection = [ {number: 1}, 'number 2', {number: 3}, :number_4 ]
|
141
186
|
|
142
|
-
|
187
|
+
collection
|
188
|
+
.select(&Kind.of.Hash.instance?) # [{:number=>1}, {:number=>3}]
|
189
|
+
```
|
143
190
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
191
|
+
> **Note:** You can use a different syntax to perform an instance verification.
|
192
|
+
> To do this, use Kind.of.\<Type\>?()
|
193
|
+
|
194
|
+
```ruby
|
195
|
+
Kind.of.Hash?({}) # true
|
196
|
+
Kind.of.Hash?({}, HashWithIndifferentAccess.new) # true
|
197
|
+
|
198
|
+
Kind.of.Hash?('') # false
|
199
|
+
Kind.of.Hash?({}, '') # false
|
200
|
+
|
201
|
+
# ---
|
202
|
+
|
203
|
+
Kind.of.Boolean?(true) # true
|
204
|
+
Kind.of.Boolean?(false, true) # true
|
205
|
+
|
206
|
+
Kind.of.Boolean?(nil) # false
|
207
|
+
Kind.of.Boolean?(false, true, nil) # false
|
208
|
+
|
209
|
+
# ---
|
210
|
+
|
211
|
+
collection = [ {number: 1}, 'number 2', {number: 3}, :number_4 ]
|
212
|
+
|
213
|
+
collection.select(&Kind.of.Hash?) # [{:number=>1}, {:number=>3}]
|
148
214
|
```
|
149
215
|
|
150
|
-
###
|
216
|
+
### Kind.is.\<Type\>() - Verifying if some class/module is the expected kind.
|
151
217
|
|
152
218
|
You can use `Kind.is` to verify if some class has the expected type as its ancestor.
|
153
219
|
|
@@ -167,6 +233,49 @@ Kind.of.Hash.class?(Hash) # true
|
|
167
233
|
Kind.of.Hash.class?(ActiveSupport::HashWithIndifferentAccess) # true
|
168
234
|
```
|
169
235
|
|
236
|
+
> **Note:** The `Kind.is` could check the inheritance of Classes/Modules.
|
237
|
+
|
238
|
+
```ruby
|
239
|
+
#
|
240
|
+
# Verifying if the attribute value is the class or a subclass.
|
241
|
+
#
|
242
|
+
class Human; end
|
243
|
+
class Person < Human; end
|
244
|
+
class User < Human; end
|
245
|
+
|
246
|
+
Kind.is(Human, User) # true
|
247
|
+
Kind.is(Human, Human) # true
|
248
|
+
Kind.is(Human, Person) # true
|
249
|
+
|
250
|
+
Kind.is(Human, Struct) # false
|
251
|
+
|
252
|
+
#
|
253
|
+
# Verifying if the attribute value is the module or if it is a class that includes the module
|
254
|
+
#
|
255
|
+
module Human; end
|
256
|
+
class Person; include Human; end
|
257
|
+
class User; include Human; end
|
258
|
+
|
259
|
+
Kind.is(Human, User) # true
|
260
|
+
Kind.is(Human, Human) # true
|
261
|
+
Kind.is(Human, Person) # true
|
262
|
+
|
263
|
+
Kind.is(Human, Struct) # false
|
264
|
+
|
265
|
+
#
|
266
|
+
# Verifying if the attribute value is the module or if it is a module that extends the module
|
267
|
+
#
|
268
|
+
module Human; end
|
269
|
+
module Person; extend Human; end
|
270
|
+
module User; extend Human; end
|
271
|
+
|
272
|
+
Kind.is(Human, User) # true
|
273
|
+
Kind.is(Human, Human) # true
|
274
|
+
Kind.is(Human, Person) # true
|
275
|
+
|
276
|
+
Kind.is(Human, Struct) # false
|
277
|
+
```
|
278
|
+
|
170
279
|
[⬆️ Back to Top](#table-of-contents-)
|
171
280
|
|
172
281
|
### How to create a new type checker?
|
@@ -191,6 +300,22 @@ Kind.of(User, {}) # Kind::Error ({} expected to be a kind of User)
|
|
191
300
|
Kind.of(Hash, {}) # {}
|
192
301
|
Kind.of(Hash, user) # Kind::Error (<User ...> expected to be a kind of Hash)
|
193
302
|
|
303
|
+
# ----------------------------------------- #
|
304
|
+
# Verifiyng if the value is a kind instance #
|
305
|
+
# ----------------------------------------- #
|
306
|
+
|
307
|
+
Kind.of?(Numeric, 1) # true
|
308
|
+
Kind.of?(Numeric, 1, 2.0) # true
|
309
|
+
|
310
|
+
Kind.of?(Numeric, '1') # false
|
311
|
+
Kind.of?(Numeric, 1, '2.0') # false
|
312
|
+
|
313
|
+
# Note: Kind.of?(Type) without arguments will return a
|
314
|
+
# lambda that will perform an instance verification
|
315
|
+
#
|
316
|
+
[1, '2', 3.0, '4']
|
317
|
+
.select(&Kind.of?(Numeric)) # [1, 3.0]
|
318
|
+
|
194
319
|
# ---------------------------------- #
|
195
320
|
# Creating type checkers dynamically #
|
196
321
|
# ---------------------------------- #
|
@@ -205,6 +330,15 @@ kind_of_user.instance?(User) # true
|
|
205
330
|
kind_of_user.class?(Hash) # false
|
206
331
|
kind_of_user.class?(User) # true
|
207
332
|
|
333
|
+
# ------------------------------------ #
|
334
|
+
# Using methods which returns a lambda #
|
335
|
+
# ------------------------------------ #
|
336
|
+
collection = [User.new, User.new, 0, {} nil, User.new]
|
337
|
+
|
338
|
+
collection.select(&Kind.of(User).instance?).size == 3 # true
|
339
|
+
|
340
|
+
collection.map(&Kind.of(User).as_optional).select(&:some?).size == 3 # true
|
341
|
+
|
208
342
|
# Creating type checkers dynamically is cheap
|
209
343
|
# because a singleton object is created to be available for use.
|
210
344
|
|
@@ -220,7 +354,7 @@ end
|
|
220
354
|
Kind.is(User, AdminUser) # true
|
221
355
|
```
|
222
356
|
|
223
|
-
#### Registering new (custom) type
|
357
|
+
#### Registering new (custom) type checker
|
224
358
|
|
225
359
|
Use `Kind::Types.add()`. e.g:
|
226
360
|
|
@@ -340,10 +474,162 @@ The list of types (classes and modules) available to use with `Kind.of.*` or `Ki
|
|
340
474
|
- `Kind.of.Module()`
|
341
475
|
- `Kind.of.Lambda()`
|
342
476
|
- `Kind.of.Boolean()`
|
343
|
-
- `Kind.of.Callable()`: verifies if the given value `respond_to?(:call)
|
477
|
+
- `Kind.of.Callable()`: verifies if the given value `respond_to?(:call)`.
|
344
478
|
- `Kind.of.Maybe()` or its alias `Kind.of.Optional()`
|
345
479
|
|
346
|
-
|
480
|
+
**Note:** Remember, you can use the `Kind.is.*` method to check if some given value is a class/module with all type checkers above.
|
481
|
+
|
482
|
+
[⬆️ Back to Top](#table-of-contents-)
|
483
|
+
|
484
|
+
## Kind::Validator (ActiveModel::Validations)
|
485
|
+
|
486
|
+
This module enables the capability to validate types via [`ActiveModel::Validations >= 3.2, < 6.1.0`](https://api.rubyonrails.org/classes/ActiveModel/Validations.html). e.g
|
487
|
+
|
488
|
+
```ruby
|
489
|
+
class Person
|
490
|
+
include ActiveModel::Validations
|
491
|
+
|
492
|
+
attr_accessor :first_name, :last_name
|
493
|
+
|
494
|
+
validates :first_name, :last_name, kind: String
|
495
|
+
end
|
496
|
+
```
|
497
|
+
|
498
|
+
And to make use of it, you will need to do an explicitly require. e.g:
|
499
|
+
|
500
|
+
```ruby
|
501
|
+
# In some Gemfile
|
502
|
+
gem 'kind', require: 'kind/active_model/validation'
|
503
|
+
|
504
|
+
# In some .rb file
|
505
|
+
require 'kind/active_model/validation'
|
506
|
+
```
|
507
|
+
|
508
|
+
### Usage
|
509
|
+
|
510
|
+
**[Object#kind_of?](https://ruby-doc.org/core-2.6.4/Object.html#method-i-kind_of-3F)**
|
511
|
+
|
512
|
+
```ruby
|
513
|
+
validates :name, kind: { of: String }
|
514
|
+
|
515
|
+
# Use an array to verify if the attribute
|
516
|
+
# is an instance of one of the classes/modules.
|
517
|
+
|
518
|
+
validates :status, kind: { of: [String, Symbol]}
|
519
|
+
```
|
520
|
+
|
521
|
+
**[Kind.is](#verifying-the-kind-of-some-classmodule)**
|
522
|
+
|
523
|
+
```ruby
|
524
|
+
#
|
525
|
+
# Verifying if the attribute value is the class or a subclass.
|
526
|
+
#
|
527
|
+
class Human; end
|
528
|
+
class Person < Human; end
|
529
|
+
class User < Human; end
|
530
|
+
|
531
|
+
validates :human_kind, kind: { is: Human }
|
532
|
+
|
533
|
+
#
|
534
|
+
# Verifying if the attribute value is the module or if it is a class that includes the module
|
535
|
+
#
|
536
|
+
module Human; end
|
537
|
+
class Person; include Human; end
|
538
|
+
class User; include Human; end
|
539
|
+
|
540
|
+
validates :human_kind, kind: { is: Human }
|
541
|
+
|
542
|
+
#
|
543
|
+
# Verifying if the attribute value is the module or if it is a module that extends the module
|
544
|
+
#
|
545
|
+
module Human; end
|
546
|
+
module Person; extend Human; end
|
547
|
+
module User; extend Human; end
|
548
|
+
|
549
|
+
validates :human_kind, kind: { is: Human }
|
550
|
+
|
551
|
+
# or use an array to verify if the attribute
|
552
|
+
# is a kind of one those classes/modules.
|
553
|
+
|
554
|
+
validates :human_kind, kind: { is: [Person, User] }
|
555
|
+
```
|
556
|
+
|
557
|
+
**[Object#instance_of?](https://ruby-doc.org/core-2.6.4/Object.html#method-i-instance_of-3F)**
|
558
|
+
|
559
|
+
```ruby
|
560
|
+
validates :name, kind: { instance_of: String }
|
561
|
+
|
562
|
+
# or use an array to verify if the attribute
|
563
|
+
# is an instance of one of the classes/modules.
|
564
|
+
|
565
|
+
validates :name, kind: { instance_of: [String, Symbol] }
|
566
|
+
```
|
567
|
+
|
568
|
+
|
569
|
+
**[Object#respond_to?](https://ruby-doc.org/core-2.6.4/Object.html#method-i-respond_to-3F)**
|
570
|
+
|
571
|
+
```ruby
|
572
|
+
validates :handler, kind: { respond_to: :call }
|
573
|
+
```
|
574
|
+
|
575
|
+
**Array.new.all? { |item| item.kind_of?(Class) }**
|
576
|
+
|
577
|
+
```ruby
|
578
|
+
validates :account_types, kind: { array_of: String }
|
579
|
+
|
580
|
+
# or use an array to verify if the attribute
|
581
|
+
# is an instance of one of the classes
|
582
|
+
|
583
|
+
validates :account_types, kind: { array_of: [String, Symbol] }
|
584
|
+
```
|
585
|
+
|
586
|
+
**Array.new.all? { |item| expected_values.include?(item) }**
|
587
|
+
|
588
|
+
```ruby
|
589
|
+
# Verifies if the attribute value
|
590
|
+
# is an array with some or all the expected values.
|
591
|
+
|
592
|
+
validates :account_types, kind: { array_with: ['foo', 'bar'] }
|
593
|
+
```
|
594
|
+
|
595
|
+
#### Defining the default validation strategy
|
596
|
+
|
597
|
+
By default, you can define the attribute type directly (without a hash). e.g.
|
598
|
+
|
599
|
+
```ruby
|
600
|
+
validates :name, kind: String
|
601
|
+
# or
|
602
|
+
validates :name, kind: [String, Symbol]
|
603
|
+
```
|
604
|
+
|
605
|
+
To changes this behavior you can set another strategy to validates the attributes types:
|
606
|
+
|
607
|
+
```ruby
|
608
|
+
Kind::Validator.default_strategy = :instance_of
|
609
|
+
|
610
|
+
# Tip: Create an initializer if you are in a Rails application.
|
611
|
+
```
|
612
|
+
|
613
|
+
And these are the available options to define the default strategy:
|
614
|
+
- `kind_of` *(default)*
|
615
|
+
- `instance_of`
|
616
|
+
|
617
|
+
#### Using the `allow_nil` and `strict` options
|
618
|
+
|
619
|
+
You can use the `allow_nil` option with any of the kind validations. e.g.
|
620
|
+
|
621
|
+
```ruby
|
622
|
+
validates :name, kind: String, allow_nil: true
|
623
|
+
```
|
624
|
+
|
625
|
+
And as any active model validation, kind validations works with the `strict: true`
|
626
|
+
option and with the `validates!` method. e.g.
|
627
|
+
|
628
|
+
```ruby
|
629
|
+
validates :first_name, kind: String, strict: true
|
630
|
+
# or
|
631
|
+
validates! :last_name, kind: String
|
632
|
+
```
|
347
633
|
|
348
634
|
[⬆️ Back to Top](#table-of-contents-)
|
349
635
|
|
@@ -411,7 +697,27 @@ puts optional.value_or(1) # 1
|
|
411
697
|
puts optional.value_or { 1 } # 1
|
412
698
|
```
|
413
699
|
|
414
|
-
|
700
|
+
#### Replacing blocks by lambdas
|
701
|
+
|
702
|
+
```ruby
|
703
|
+
Add = -> params do
|
704
|
+
a, b = Kind.of.Hash(params, or: Empty::HASH).values_at(:a, :b)
|
705
|
+
|
706
|
+
a + b if Kind.of.Numeric?(a, b)
|
707
|
+
end
|
708
|
+
|
709
|
+
# --
|
710
|
+
|
711
|
+
Kind::Maybe.new(a: 1, b: 2).map(&Add).value_or(0) # 3
|
712
|
+
|
713
|
+
# --
|
714
|
+
|
715
|
+
Kind::Maybe.new([]).map(&Add).value_or(0) # 0
|
716
|
+
Kind::Maybe.new({}).map(&Add).value_or(0) # 0
|
717
|
+
Kind::Maybe.new(nil).map(&Add).value_or(0) # 0
|
718
|
+
```
|
719
|
+
|
720
|
+
### Kind::Maybe[] and Kind::Maybe#then method aliases
|
415
721
|
|
416
722
|
You can use `Kind::Maybe[]` (brackets) instead of the `.new` to transform values in a `Kind::Maybe`. Another alias is `.then` to the `.map` method.
|
417
723
|
|
@@ -425,39 +731,57 @@ result =
|
|
425
731
|
puts result # 42
|
426
732
|
```
|
427
733
|
|
428
|
-
|
429
|
-
|
430
|
-
If you don't want to use a map to access the value, you could use the `#try` method to access it. So, if the value wasn't `nil` or `Kind::Undefined`, it will be returned.
|
734
|
+
#### Replacing blocks by lambdas
|
431
735
|
|
432
736
|
```ruby
|
433
|
-
|
737
|
+
Add = -> params do
|
738
|
+
a, b = Kind.of.Hash(params, or: Empty::HASH).values_at(:a, :b)
|
434
739
|
|
435
|
-
|
740
|
+
a + b if Kind.of.Numeric.instance?(a, b)
|
741
|
+
end
|
436
742
|
|
437
|
-
|
743
|
+
# --
|
438
744
|
|
439
|
-
|
440
|
-
# Nil value #
|
441
|
-
#############
|
745
|
+
Kind::Maybe[a: 1, b: 2].then(&Add).value_or(0) # 3
|
442
746
|
|
443
|
-
|
747
|
+
# --
|
444
748
|
|
445
|
-
|
749
|
+
Kind::Maybe[1].then(&Add).value_or(0) # 0
|
750
|
+
Kind::Maybe['2'].then(&Add).value_or(0) # 0
|
751
|
+
Kind::Maybe[nil].then(&Add).value_or(0) # 0
|
752
|
+
```
|
446
753
|
|
447
|
-
|
754
|
+
### Kind::None() and Kind::Some()
|
448
755
|
|
449
|
-
|
450
|
-
|
451
|
-
#########################
|
756
|
+
If you need to ensure the return of `Kind::Maybe` results from your methods/lambdas,
|
757
|
+
you could use the methods `Kind::None` and `Kind::Some` to do this. e.g:
|
452
758
|
|
453
|
-
|
759
|
+
```ruby
|
760
|
+
Add = -> params do
|
761
|
+
a, b = Kind.of.Hash(params, or: Empty::HASH).values_at(:a, :b)
|
454
762
|
|
455
|
-
|
763
|
+
return Kind::None unless Kind.of.Numeric?(a, b)
|
456
764
|
|
457
|
-
|
458
|
-
|
765
|
+
Kind::Some(a + b)
|
766
|
+
end
|
459
767
|
|
460
|
-
|
768
|
+
# --
|
769
|
+
|
770
|
+
Add.call(1) # #<Kind::Maybe::None:0x0000... @value=nil>
|
771
|
+
Add.call({}) # #<Kind::Maybe::None:0x0000... @value=nil>
|
772
|
+
Add.call(a: 1) # #<Kind::Maybe::None:0x0000... @value=nil>
|
773
|
+
Add.call(b: 2) # #<Kind::Maybe::None:0x0000... @value=nil>
|
774
|
+
|
775
|
+
Add.call(a:1, b: 2) # #<Kind::Maybe::Some:0x0000... @value=3>
|
776
|
+
|
777
|
+
# --
|
778
|
+
|
779
|
+
Kind::Maybe[a: 1, b: 2].then(&Add).value_or(0) # 3
|
780
|
+
|
781
|
+
Kind::Maybe[1].then(&Add).value_or(0) # 0
|
782
|
+
Kind::Maybe['2'].then(&Add).value_or(0) # 0
|
783
|
+
Kind::Maybe[nil].then(&Add).value_or(0) # 0
|
784
|
+
```
|
461
785
|
|
462
786
|
### Kind.of.Maybe()
|
463
787
|
|
@@ -514,7 +838,174 @@ result2 =
|
|
514
838
|
puts result2 # 35
|
515
839
|
```
|
516
840
|
|
517
|
-
|
841
|
+
#### Replacing blocks by lambdas
|
842
|
+
|
843
|
+
```ruby
|
844
|
+
Add = -> params do
|
845
|
+
a, b = Kind.of.Hash(params, or: Empty::HASH).values_at(:a, :b)
|
846
|
+
|
847
|
+
a + b if Kind.of.Numeric.instance?(a, b)
|
848
|
+
end
|
849
|
+
|
850
|
+
# --
|
851
|
+
|
852
|
+
Kind::Optional[a: 1, b: 2].then(&Add).value_or(0) # 3
|
853
|
+
|
854
|
+
# --
|
855
|
+
|
856
|
+
Kind::Optional[1].then(&Add).value_or(0) # 0
|
857
|
+
Kind::Optional['2'].then(&Add).value_or(0) # 0
|
858
|
+
Kind::Optional[nil].then(&Add).value_or(0) # 0
|
859
|
+
```
|
860
|
+
|
861
|
+
**Note:** The `Kind.of.Optional` is available to check if some value is a `Kind::Optional`.
|
862
|
+
|
863
|
+
[⬆️ Back to Top](#table-of-contents-)
|
864
|
+
|
865
|
+
### Kind.of.\<Type\>.as_optional
|
866
|
+
|
867
|
+
It is very common the need to avoid some computing when a method receives a wrong input.
|
868
|
+
In these scenarios, you could check the given input type as optional and avoid unexpected behavior. e.g:
|
869
|
+
|
870
|
+
```ruby
|
871
|
+
def person_name(params)
|
872
|
+
Kind::Of::Hash.as_optional(params)
|
873
|
+
.map { |data| data if data.values_at(:first_name, :last_name).compact.size == 2 }
|
874
|
+
.map { |data| "#{data[:first_name]} #{data[:last_name]}" }
|
875
|
+
.value_or { 'John Doe' }
|
876
|
+
end
|
877
|
+
|
878
|
+
person_name('') # "John Doe"
|
879
|
+
person_name(nil) # "John Doe"
|
880
|
+
|
881
|
+
person_name(first_name: 'Rodrigo') # "John Doe"
|
882
|
+
person_name(last_name: 'Serradura') # "John Doe"
|
883
|
+
|
884
|
+
person_name(first_name: 'Rodrigo', last_name: 'Serradura') # "Rodrigo Serradura"
|
885
|
+
|
886
|
+
#
|
887
|
+
# See below the previous implementation without using an optional.
|
888
|
+
#
|
889
|
+
def person_name(params)
|
890
|
+
if params.kind_of?(Hash) && params.values_at(:first_name, :last_name).compact.size == 2
|
891
|
+
"#{params[:first_name]} #{params[:last_name]}"
|
892
|
+
else
|
893
|
+
'John Doe'
|
894
|
+
end
|
895
|
+
end
|
896
|
+
```
|
897
|
+
|
898
|
+
> Note: You could use the `.as_optional` method (or it alias `.as_maybe`) with any [type checker](https://github.com/serradura/kind/blob/b177fed9cc2b3347d63963a2a2fd99f989c51a9a/README.md#type-checkers).
|
899
|
+
|
900
|
+
Let's see another example using a collection and how the method `.as_optional` works when it receives no argument.
|
901
|
+
|
902
|
+
```ruby
|
903
|
+
collection = [ {number: 1}, 'number 0', {number: 2}, [0] ]
|
904
|
+
|
905
|
+
collection
|
906
|
+
.select(&Kind.of.Hash.as_optional)
|
907
|
+
.reduce(0) do |total, item|
|
908
|
+
item.try { |data| data[:number] + total } || total
|
909
|
+
end
|
910
|
+
|
911
|
+
collection
|
912
|
+
.map(&Kind.of.Hash.as_optional).select(&:some?)
|
913
|
+
.reduce(0) { |total, item| total + item.value[:number] }
|
914
|
+
|
915
|
+
# Note: All the examples above return 3 as the sum of all hashes with numbers.
|
916
|
+
```
|
917
|
+
|
918
|
+
To finish follows an example of how to use optionals to handle arguments in coupled methods.
|
919
|
+
|
920
|
+
```ruby
|
921
|
+
module PersonIntroduction
|
922
|
+
extend self
|
923
|
+
|
924
|
+
def call(params)
|
925
|
+
optional = Kind::Of::Hash.as_optional(params)
|
926
|
+
|
927
|
+
"Hi my name is #{full_name(optional)}, I'm #{age(optional)} years old."
|
928
|
+
end
|
929
|
+
|
930
|
+
private
|
931
|
+
|
932
|
+
def full_name(optional)
|
933
|
+
optional.map { |data| "#{data[:first_name]} #{data[:last_name]}" }
|
934
|
+
.value_or { 'John Doe' }
|
935
|
+
end
|
936
|
+
|
937
|
+
def age(optional)
|
938
|
+
optional.map { |data| data[:age] }.value_or(0)
|
939
|
+
end
|
940
|
+
end
|
941
|
+
|
942
|
+
#
|
943
|
+
# See below the previous implementation without using an optional.
|
944
|
+
#
|
945
|
+
module PersonIntroduction
|
946
|
+
extend self
|
947
|
+
|
948
|
+
def call(params)
|
949
|
+
"Hi my name is #{full_name(params)}, I'm #{age(params)} years old."
|
950
|
+
end
|
951
|
+
|
952
|
+
private
|
953
|
+
|
954
|
+
def full_name(params)
|
955
|
+
case params
|
956
|
+
when Hash then "#{params[:first_name]} #{params[:last_name]}"
|
957
|
+
else 'John Doe'
|
958
|
+
end
|
959
|
+
end
|
960
|
+
|
961
|
+
def age(params)
|
962
|
+
case params
|
963
|
+
when Hash then params.fetch(:age, 0)
|
964
|
+
else 0
|
965
|
+
end
|
966
|
+
end
|
967
|
+
end
|
968
|
+
```
|
969
|
+
|
970
|
+
[⬆️ Back to Top](#table-of-contents-)
|
971
|
+
|
972
|
+
### Kind::Maybe#try
|
973
|
+
|
974
|
+
If you don't want to use a map to access the value, you could use the `#try` method to access it. So, if the value wasn't `nil` or `Kind::Undefined`, it will be returned.
|
975
|
+
|
976
|
+
```ruby
|
977
|
+
object = 'foo'
|
978
|
+
|
979
|
+
Kind::Maybe[object].try(:upcase) # "FOO"
|
980
|
+
|
981
|
+
Kind::Maybe[{}].try(:fetch, :number, 0) # 0
|
982
|
+
|
983
|
+
Kind::Maybe[{number: 1}].try(:fetch, :number) # 1
|
984
|
+
|
985
|
+
Kind::Maybe[object].try { |value| value.upcase } # "FOO"
|
986
|
+
|
987
|
+
#############
|
988
|
+
# Nil value #
|
989
|
+
#############
|
990
|
+
|
991
|
+
object = nil
|
992
|
+
|
993
|
+
Kind::Maybe[object].try(:upcase) # nil
|
994
|
+
|
995
|
+
Kind::Maybe[object].try { |value| value.upcase } # nil
|
996
|
+
|
997
|
+
#########################
|
998
|
+
# Kind::Undefined value #
|
999
|
+
#########################
|
1000
|
+
|
1001
|
+
object = Kind::Undefined
|
1002
|
+
|
1003
|
+
Kind::Maybe[object].try(:upcase) # nil
|
1004
|
+
|
1005
|
+
Kind::Maybe[object].try { |value| value.upcase } # nil
|
1006
|
+
```
|
1007
|
+
|
1008
|
+
> **Note:** You can use the try method with the `Kind::Optional`.
|
518
1009
|
|
519
1010
|
[⬆️ Back to Top](#table-of-contents-)
|
520
1011
|
|
@@ -565,6 +1056,10 @@ Follows the list of constants, if the alias is available to be created:
|
|
565
1056
|
|
566
1057
|
[⬆️ Back to Top](#table-of-contents-)
|
567
1058
|
|
1059
|
+
## Similar Projects
|
1060
|
+
|
1061
|
+
- [dry-types](https://dry-rb.org/gems/dry-types)
|
1062
|
+
|
568
1063
|
## Development
|
569
1064
|
|
570
1065
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
@@ -575,7 +1070,6 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
575
1070
|
|
576
1071
|
Bug reports and pull requests are welcome on GitHub at https://github.com/serradura/kind. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/serradura/kind/blob/master/CODE_OF_CONDUCT.md).
|
577
1072
|
|
578
|
-
|
579
1073
|
## License
|
580
1074
|
|
581
1075
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|