dry-data 0.4.2 → 0.5.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.
- checksums.yaml +4 -4
- data/.travis.yml +1 -0
- data/CHANGELOG.md +21 -0
- data/Gemfile +0 -2
- data/README.md +204 -144
- data/dry-data.gemspec +1 -0
- data/lib/dry/data.rb +1 -7
- data/lib/dry/data/constrained.rb +41 -0
- data/lib/dry/data/constraints.rb +11 -3
- data/lib/dry/data/decorator.rb +44 -0
- data/lib/dry/data/default.rb +22 -0
- data/lib/dry/data/enum.rb +24 -0
- data/lib/dry/data/optional.rb +4 -9
- data/lib/dry/data/struct.rb +2 -2
- data/lib/dry/data/sum_type.rb +5 -9
- data/lib/dry/data/type.rb +14 -46
- data/lib/dry/data/type/array.rb +1 -1
- data/lib/dry/data/type/hash.rb +1 -1
- data/lib/dry/data/type_builder.rb +31 -0
- data/lib/dry/data/types.rb +3 -3
- data/lib/dry/data/types/form.rb +9 -9
- data/lib/dry/data/value.rb +2 -0
- data/lib/dry/data/version.rb +1 -1
- metadata +21 -5
- data/lib/dry/data/dsl.rb +0 -15
- data/lib/dry/data/type/constrained.rb +0 -35
- data/lib/dry/data/type/enum.rb +0 -27
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1b1120eb4914c94c999641d81a01ede2deb790f2
|
4
|
+
data.tar.gz: 58700f1d2f8d0de68428212b15feba6111de499f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 12003922d8e399f312bb39ebc53c5f37bde3be4a7c5d5d9e3d667fb97358df733810fe096c60c0ca831f9dbcad7ed3b9b8cd81f2017699d12cbe8b950d4bfa05
|
7
|
+
data.tar.gz: ff1ee72ab2ea48e10458352e82a1d07521f644760107b33f66b67bda5f1067c7dcb77c9fb0d609489a2d278a92c1fe11d139c88d508fe67415a87155fdd5701d
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,24 @@
|
|
1
|
+
# v0.5.0 2016-01-11
|
2
|
+
|
3
|
+
## Added
|
4
|
+
|
5
|
+
* `Type#default` interface for defining a type with a default value (solnic)
|
6
|
+
|
7
|
+
## Changed
|
8
|
+
|
9
|
+
* [BREAKING] `Dry::Data::Type.new` accepts constructor and *options* now (solnic)
|
10
|
+
* Renamed `Dry::Data::Type::{Enum,Constrained}` => `Dry::Data::{Enum,Constrained}` (solnic)
|
11
|
+
* `dry-logic` is now a dependency for constrained types (solnic)
|
12
|
+
* Constrained types are now always available (solnic)
|
13
|
+
* `strict.*` category uses constrained types with `:type?` predicate (solnic)
|
14
|
+
* `SumType#call` no longer needs to rescue from `TypeError` (solnic)
|
15
|
+
|
16
|
+
## Fixed
|
17
|
+
|
18
|
+
* `attribute` raises proper error when type definition is missing (solnic)
|
19
|
+
|
20
|
+
[Compare v0.4.2...v0.5.0](https://github.com/dryrb/dry-data/compare/v0.4.2...v0.5.0)
|
21
|
+
|
1
22
|
# v0.4.2 2015-12-27
|
2
23
|
|
3
24
|
## Added
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -14,7 +14,50 @@
|
|
14
14
|
[][codeclimate]
|
15
15
|
[][inchpages]
|
16
16
|
|
17
|
-
A simple type system for Ruby with support for coercions
|
17
|
+
A simple and extendible type system for Ruby with support for kernel coercions,
|
18
|
+
form coercions, sum types, constrained types and default-value types.
|
19
|
+
|
20
|
+
Used by:
|
21
|
+
|
22
|
+
* [dry-validation](https://github.com/dryrb/dry-validation) for params coercions
|
23
|
+
* [rom-repository](https://github.com/rom-rb/rom-repository) for auto-mapped structs
|
24
|
+
* [rom](https://github.com/rom-rb/rom)'s adapters for relation schema definitions
|
25
|
+
* your project...?
|
26
|
+
|
27
|
+
Articles:
|
28
|
+
|
29
|
+
* ["Invalid Object Is An Anti-Pattern"](http://solnic.eu/2015/12/28/invalid-object-is-an-anti-pattern.html)
|
30
|
+
|
31
|
+
## dry-data vs virtus
|
32
|
+
|
33
|
+
[Virtus](https://github.com/solnic/virtus) has been a successful library, unfortunately
|
34
|
+
it is "only" a by-product of an ActiveRecord ORM which carries many issues typical
|
35
|
+
to ActiveRecord-like features that we all know from Rails, especially when it
|
36
|
+
comes to very complicated coercion logic, mixing unrelated concerns, polluting
|
37
|
+
application layer with concerns that should be handled at the bounderies etc.
|
38
|
+
|
39
|
+
`dry-data` has been created to become a better tool that solves *similar* (but
|
40
|
+
not identical!) problems related to type-safety and coercions. It is a superior
|
41
|
+
solution because:
|
42
|
+
|
43
|
+
* Types are [categorized](#built-in-type-categories), which is especially important for coercions
|
44
|
+
* Types are objects and they are easily reusable
|
45
|
+
* Has [structs](#structs) and [values](#values) with *a simple DSL*
|
46
|
+
* Has [constrained types](#constrained-types)
|
47
|
+
* Has [optional types](#optional-types)
|
48
|
+
* Has [defaults](#defaults)
|
49
|
+
* Has [sum-types](#sum-types)
|
50
|
+
* Has [enums](#enums)
|
51
|
+
* Has [hash type with type schemas](#hashes)
|
52
|
+
* Has [array type with member type](#arrays)
|
53
|
+
* Suitable for many use-cases while remaining simple, in example:
|
54
|
+
* Params coercions
|
55
|
+
* Domain "models"
|
56
|
+
* Defining various domain-specific, shared information using enums or values
|
57
|
+
* Annotating objects
|
58
|
+
* and more...
|
59
|
+
* There's no const-missing magic and complicated const lookups like in Virtus
|
60
|
+
* AND is roughly 10-12x faster than Virtus
|
18
61
|
|
19
62
|
## Installation
|
20
63
|
|
@@ -46,107 +89,139 @@ Built-in types are grouped under 5 categories:
|
|
46
89
|
- `form` - non-strict coercion types suitable for form params
|
47
90
|
- `maybe` - accepts either a nil or something else
|
48
91
|
|
92
|
+
### Configuring Types Module
|
93
|
+
|
94
|
+
In `dry-data` a type is an object with a constructor that knows how to handle
|
95
|
+
input. On top of that there are high-level types like a sum-type, constrained type,
|
96
|
+
optional type or default value type.
|
97
|
+
|
98
|
+
To acccess all the built-in type objects you can configure `dry-data` with a
|
99
|
+
namespace module:
|
100
|
+
|
101
|
+
``` ruby
|
102
|
+
module Types
|
103
|
+
end
|
104
|
+
|
105
|
+
Dry::Data.configure do |config|
|
106
|
+
config.namespace = Types
|
107
|
+
end
|
108
|
+
|
109
|
+
# after defining your custom types (if you've got any) you can finalize setup
|
110
|
+
Dry::Data.finalize
|
111
|
+
|
112
|
+
# this defines all types under your namespace, in example:
|
113
|
+
Types::Coercible::String
|
114
|
+
# => #<Dry::Data::Type:0x007feffb104aa8 @constructor=#<Method: Kernel.String>, @primitive=String>
|
115
|
+
```
|
116
|
+
|
117
|
+
With types accessible as constants you can easily compose more complex types,
|
118
|
+
like sum-types or constrained types, in hash schemas or structs:
|
119
|
+
|
120
|
+
``` ruby
|
121
|
+
Dry::Data.configure do |config|
|
122
|
+
config.namespace = Types
|
123
|
+
end
|
124
|
+
|
125
|
+
Dry::Data.finalize
|
126
|
+
|
127
|
+
module Types
|
128
|
+
Email = String.constrained(format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i)
|
129
|
+
Age = Int.constrained(gt: 18)
|
130
|
+
end
|
131
|
+
|
132
|
+
class User < Dry::Data::Struct
|
133
|
+
attribute :name, Types::String
|
134
|
+
attribute :email, Types::Email
|
135
|
+
attribute :age, Types::Age
|
136
|
+
end
|
137
|
+
```
|
138
|
+
|
49
139
|
### Built-in Type Categories
|
50
140
|
|
141
|
+
Assuming you configured types under `Types` module namespace:
|
142
|
+
|
51
143
|
Non-coercible:
|
52
144
|
|
53
|
-
- `
|
54
|
-
- `
|
55
|
-
- `
|
56
|
-
- `
|
57
|
-
- `
|
58
|
-
- `
|
59
|
-
- `
|
60
|
-
- `
|
145
|
+
- `Types::Nil`
|
146
|
+
- `Types::Symbol`
|
147
|
+
- `Types::Class`
|
148
|
+
- `Types::True`
|
149
|
+
- `Types::False`
|
150
|
+
- `Types::Date`
|
151
|
+
- `Types::DateTime`
|
152
|
+
- `Types::Time`
|
61
153
|
|
62
154
|
Coercible types using kernel coercion methods:
|
63
155
|
|
64
|
-
- `
|
65
|
-
- `
|
66
|
-
- `
|
67
|
-
- `
|
68
|
-
- `
|
69
|
-
- `
|
156
|
+
- `Types::Coercible::String`
|
157
|
+
- `Types::Coercible::Int`
|
158
|
+
- `Types::Coercible::Float`
|
159
|
+
- `Types::Coercible::Decimal`
|
160
|
+
- `Types::Coercible::Array`
|
161
|
+
- `Types::Coercible::Hash`
|
70
162
|
|
71
163
|
Optional strict types:
|
72
164
|
|
73
|
-
- `
|
74
|
-
- `
|
75
|
-
- `
|
76
|
-
- `
|
77
|
-
- `
|
78
|
-
- `
|
165
|
+
- `Types::Maybe::Strict::String`
|
166
|
+
- `Types::Maybe::Strict::Int`
|
167
|
+
- `Types::Maybe::Strict::Float`
|
168
|
+
- `Types::Maybe::Strict::Decimal`
|
169
|
+
- `Types::Maybe::Strict::Array`
|
170
|
+
- `Types::Maybe::Strict::Hash`
|
79
171
|
|
80
172
|
Optional coercible types:
|
81
173
|
|
82
|
-
- `
|
83
|
-
- `
|
84
|
-
- `
|
85
|
-
- `
|
86
|
-
- `
|
87
|
-
- `
|
174
|
+
- `Types::Maybe::Coercible::String`
|
175
|
+
- `Types::Maybe::Coercible::Int`
|
176
|
+
- `Types::Maybe::Coercible::Float`
|
177
|
+
- `Types::Maybe::Coercible::Decimal`
|
178
|
+
- `Types::Maybe::Coercible::Array`
|
179
|
+
- `Types::Maybe::Coercible::Hash`
|
88
180
|
|
89
181
|
Coercible types suitable for form param processing:
|
90
182
|
|
91
|
-
- `
|
92
|
-
- `
|
93
|
-
- `
|
94
|
-
- `
|
95
|
-
- `
|
96
|
-
- `
|
97
|
-
- `
|
98
|
-
- `
|
99
|
-
- `
|
100
|
-
- `
|
183
|
+
- `Types::Form::Nil`
|
184
|
+
- `Types::Form::Date`
|
185
|
+
- `Types::Form::DateTime`
|
186
|
+
- `Types::Form::Time`
|
187
|
+
- `Types::Form::True`
|
188
|
+
- `Types::Form::False`
|
189
|
+
- `Types::Form::Bool`
|
190
|
+
- `Types::Form::Int`
|
191
|
+
- `Types::Form::Float`
|
192
|
+
- `Types::Form::Decimal`
|
101
193
|
|
102
|
-
###
|
194
|
+
### Strict vs Coercible Types
|
103
195
|
|
104
196
|
``` ruby
|
105
|
-
#
|
106
|
-
|
107
|
-
|
108
|
-
float[3.2] # => 3.2
|
109
|
-
float["3.2"] # "3.2"
|
110
|
-
|
111
|
-
# strict type-check category
|
112
|
-
int = Dry::Data["strict.int"]
|
113
|
-
|
114
|
-
int[1] # => 1
|
115
|
-
int['1'] # => raises TypeError
|
197
|
+
Types::Strict::Int[1] # => 1
|
198
|
+
Types::Strict::Int['1'] # => raises Dry::Data::ConstraintError
|
116
199
|
|
117
200
|
# coercible type-check group
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
string[:foo] # => 'foo'
|
122
|
-
array[:foo] # => [:foo]
|
201
|
+
Types::Coercible::String[:foo] # => 'foo'
|
202
|
+
Types::Coercible::Array[:foo] # => [:foo]
|
123
203
|
|
124
204
|
# form group
|
125
|
-
|
126
|
-
date['2015-11-29'] # => #<Date: 2015-11-29 ((2457356j,0s,0n),+0s,2299161j)>
|
205
|
+
Types::Form::Date['2015-11-29'] # => #<Date: 2015-11-29 ((2457356j,0s,0n),+0s,2299161j)>
|
127
206
|
```
|
128
207
|
|
129
|
-
### Optional
|
208
|
+
### Optional Types
|
130
209
|
|
131
210
|
All built-in types have their optional versions too, you can access them under
|
132
|
-
`"
|
211
|
+
`"Types::Maybe::Strict"` and `"Maybe::Coercible"` categories:
|
133
212
|
|
134
213
|
``` ruby
|
135
|
-
|
136
|
-
|
137
|
-
maybe_int[nil] # None
|
138
|
-
maybe_int[123] # Some(123)
|
214
|
+
Types::Maybe::Int[nil] # None
|
215
|
+
Types::Maybe::Int[123] # Some(123)
|
139
216
|
|
140
|
-
|
141
|
-
|
142
|
-
maybe_int[nil] # None
|
143
|
-
maybe_int['12.3'] # Some(12.3)
|
217
|
+
Types::Maybe::Coercible::Float[nil] # None
|
218
|
+
Types::Maybe::Coercible::Float['12.3'] # Some(12.3)
|
144
219
|
```
|
145
220
|
|
146
221
|
You can define your own optional types too:
|
147
222
|
|
148
223
|
``` ruby
|
149
|
-
maybe_string =
|
224
|
+
maybe_string = Types::Strict::String.optional
|
150
225
|
|
151
226
|
maybe_string[nil]
|
152
227
|
# => None
|
@@ -164,19 +239,31 @@ maybe_string['something'].fmap(&:upcase).value
|
|
164
239
|
# => "SOMETHING"
|
165
240
|
```
|
166
241
|
|
242
|
+
### Defaults
|
243
|
+
|
244
|
+
A type with a default value will return the configured value when the input is `nil`:
|
245
|
+
|
246
|
+
``` ruby
|
247
|
+
PostStatus = Types::Strict::String.default('draft')
|
248
|
+
|
249
|
+
PostStatus[nil] # "draft"
|
250
|
+
PostStatus["published"] # "published"
|
251
|
+
PostStatus[true] # raises ConstraintError
|
252
|
+
```
|
253
|
+
|
167
254
|
### Sum-types
|
168
255
|
|
169
256
|
You can specify sum types using `|` operator, it is an explicit way of defining
|
170
257
|
what are the valid types of a value.
|
171
258
|
|
172
259
|
In example `dry-data` defines `bool` type which is a sum-type consisting of `true`
|
173
|
-
and `false` types which is expressed as `
|
260
|
+
and `false` types which is expressed as `Types::True | Types::False`
|
174
261
|
(and it has its strict version, too).
|
175
262
|
|
176
263
|
Another common case is defining that something can be either `nil` or something else:
|
177
264
|
|
178
265
|
``` ruby
|
179
|
-
nil_or_string =
|
266
|
+
nil_or_string = Types::Nil | Types::Strict::String
|
180
267
|
|
181
268
|
nil_or_string[nil] # => nil
|
182
269
|
nil_or_string["hello"] # => "hello"
|
@@ -191,20 +278,11 @@ a lower level guarantee that you're not instantiating objects that are broken.
|
|
191
278
|
All types support constraints API, but not all constraints are suitable for a
|
192
279
|
particular primitive, it's up to you to set up constraints that make sense.
|
193
280
|
|
194
|
-
Under the hood it uses [`dry-
|
281
|
+
Under the hood it uses [`dry-logic`](https://github.com/dryrb/dry-logic)
|
195
282
|
and all of its predicates are supported.
|
196
283
|
|
197
|
-
IMPORTANT: `dry-data` does not have a runtime dependency on `dry-validation` so
|
198
|
-
if you want to use contrained types you need to add it to your Gemfile
|
199
|
-
|
200
|
-
If you want to use constrained type you need to require it explicitly:
|
201
|
-
|
202
284
|
``` ruby
|
203
|
-
|
204
|
-
```
|
205
|
-
|
206
|
-
``` ruby
|
207
|
-
string = Dry::Data["strict.string"].constrained(min_size: 3)
|
285
|
+
string = Types::Strict::String.constrained(min_size: 3)
|
208
286
|
|
209
287
|
string['foo']
|
210
288
|
# => "foo"
|
@@ -212,7 +290,7 @@ string['foo']
|
|
212
290
|
string['fo']
|
213
291
|
# => Dry::Data::ConstraintError: "fo" violates constraints
|
214
292
|
|
215
|
-
email =
|
293
|
+
email = Types::Strict::String.constrained(
|
216
294
|
format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i
|
217
295
|
)
|
218
296
|
|
@@ -223,48 +301,7 @@ email["jane"]
|
|
223
301
|
# => Dry::Data::ConstraintError: "fo" violates constraints
|
224
302
|
```
|
225
303
|
|
226
|
-
###
|
227
|
-
|
228
|
-
Types can be stored as easily accessible constants in a configured namespace:
|
229
|
-
|
230
|
-
``` ruby
|
231
|
-
module Types; end
|
232
|
-
|
233
|
-
Dry::Data.configure do |config|
|
234
|
-
config.namespace = Types
|
235
|
-
end
|
236
|
-
|
237
|
-
# after defining your custom types (if you've got any) you can finalize setup
|
238
|
-
Dry::Data.finalize
|
239
|
-
|
240
|
-
# this defines all types under your namespace
|
241
|
-
Types::Coercible::String
|
242
|
-
# => #<Dry::Data::Type:0x007feffb104aa8 @constructor=#<Method: Kernel.String>, @primitive=String>
|
243
|
-
```
|
244
|
-
|
245
|
-
With types accessible as constants you can easily compose more complex types,
|
246
|
-
like sum-types or constrained types, in hash schemas or structs:
|
247
|
-
|
248
|
-
``` ruby
|
249
|
-
Dry::Data.configure do |config|
|
250
|
-
config.namespace = Types
|
251
|
-
end
|
252
|
-
|
253
|
-
Dry::Data.finalize
|
254
|
-
|
255
|
-
module Types
|
256
|
-
Email = String.constrained(format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i)
|
257
|
-
Age = Int.constrained(gt: 18)
|
258
|
-
end
|
259
|
-
|
260
|
-
class User < Dry::Data::Struct
|
261
|
-
attribute :name, Types::String
|
262
|
-
attribute :email, Types::Email
|
263
|
-
attribute :age, Types::Age
|
264
|
-
end
|
265
|
-
```
|
266
|
-
|
267
|
-
### Defining Enums
|
304
|
+
### Enums
|
268
305
|
|
269
306
|
In many cases you may want to define an enum. For example in a blog application
|
270
307
|
a post may have a finite list of statuses. Apart from accessing the current status
|
@@ -272,8 +309,6 @@ value it is useful to have all possible values accessible too. Furthermore an
|
|
272
309
|
enum is a `int => value` map, so you can store integers somewhere and have them
|
273
310
|
mapped to enum values conveniently.
|
274
311
|
|
275
|
-
You can define enums for every type but it probably only makes sense for `string`:
|
276
|
-
|
277
312
|
``` ruby
|
278
313
|
# assuming we have types loaded into `Types` namespace
|
279
314
|
# we can easily define an enum for our post struct
|
@@ -303,77 +338,102 @@ Post::Statuses[nil]
|
|
303
338
|
# => Dry::Data::ConstraintError: nil violates constraints
|
304
339
|
```
|
305
340
|
|
306
|
-
###
|
341
|
+
### Hashes
|
307
342
|
|
308
343
|
The built-in hash type has constructors that you can use to define hashes with
|
309
344
|
explicit schemas and coercible values using the built-in types.
|
310
345
|
|
311
|
-
|
346
|
+
#### Hash Schema
|
312
347
|
|
313
348
|
``` ruby
|
314
349
|
# using simple kernel coercions
|
315
|
-
hash =
|
350
|
+
hash = Types::Hash.schema(name: Types::String, age: Types::Coercible::Int)
|
316
351
|
|
317
352
|
hash[name: 'Jane', age: '21']
|
318
353
|
# => { :name => "Jane", :age => 21 }
|
319
354
|
|
320
355
|
# using form param coercions
|
321
|
-
hash =
|
356
|
+
hash = Types::Hash.schema(name: Types::String, birthdate: Form::Date)
|
322
357
|
|
323
358
|
hash[name: 'Jane', birthdate: '1994-11-11']
|
324
359
|
# => { :name => "Jane", :birthdate => #<Date: 1994-11-11 ((2449668j,0s,0n),+0s,2299161j)> }
|
325
360
|
```
|
326
361
|
|
327
|
-
|
362
|
+
#### Strict Schema
|
328
363
|
|
329
364
|
Strict hash will raise errors when keys are missing or value types are incorrect.
|
330
365
|
|
331
366
|
``` ruby
|
332
|
-
hash =
|
367
|
+
hash = Types::Hash.strict(name: 'string', age: 'coercible.int')
|
333
368
|
|
334
369
|
hash[email: 'jane@doe.org', name: 'Jane', age: 21]
|
335
370
|
# => Dry::Data::SchemaKeyError: :email is missing in Hash input
|
336
371
|
```
|
337
372
|
|
338
|
-
|
373
|
+
#### Symbolized Schema
|
339
374
|
|
340
375
|
Symbolized hash will turn string key names into symbols
|
341
376
|
|
342
377
|
``` ruby
|
343
|
-
hash =
|
378
|
+
hash = Types::Hash.symbolized(name: Types::String, age: Types::Coercible::Int)
|
344
379
|
|
345
380
|
hash['name' => 'Jane', 'age' => '21']
|
346
381
|
# => { :name => "Jane", :age => 21 }
|
347
382
|
```
|
348
383
|
|
349
|
-
###
|
384
|
+
### Arrays
|
350
385
|
|
351
|
-
|
352
|
-
|
386
|
+
The built-in array type supports defining member type:
|
387
|
+
|
388
|
+
``` ruby
|
389
|
+
PostStatuses = Types::Strict::Array.member(Types::Coercible::String)
|
390
|
+
|
391
|
+
PostStatuses[[:foo, :bar]] # ["foo", "bar"]
|
392
|
+
```
|
393
|
+
|
394
|
+
### Structs
|
395
|
+
|
396
|
+
You can define struct objects which will have readers for specified attributes
|
397
|
+
using a simple dsl:
|
353
398
|
|
354
399
|
``` ruby
|
355
400
|
class User < Dry::Data::Struct
|
356
|
-
attribute :name,
|
357
|
-
attribute :age,
|
401
|
+
attribute :name, Types::Maybe::Coercible::String
|
402
|
+
attribute :age, Types::Coercible::Int
|
358
403
|
end
|
359
404
|
|
360
|
-
|
361
|
-
user_type = Dry::Data["user"]
|
362
|
-
|
363
|
-
user = user_type[name: nil, age: '21']
|
405
|
+
user = User.new(name: nil, age: '21')
|
364
406
|
|
365
407
|
user.name # None
|
366
408
|
user.age # 21
|
367
409
|
|
368
|
-
user =
|
410
|
+
user = User(name: 'Jane', age: '21')
|
369
411
|
|
370
412
|
user.name # => Some("Jane")
|
371
413
|
user.age # => 21
|
372
414
|
```
|
373
415
|
|
416
|
+
### Values
|
417
|
+
|
418
|
+
You can define value objects which will behave like structs and have equality
|
419
|
+
methods too:
|
420
|
+
|
421
|
+
``` ruby
|
422
|
+
class Location < Dry::Data::Value
|
423
|
+
attribute :lat, Types::Strict::Float
|
424
|
+
attribute :lat, Types::Strict::Float
|
425
|
+
end
|
426
|
+
|
427
|
+
loc1 = Location.new(lat: 1.23, lng: 4.56)
|
428
|
+
loc2 = Location.new(lat: 1.23, lng: 4.56)
|
429
|
+
|
430
|
+
loc1 == loc2
|
431
|
+
# true
|
432
|
+
```
|
433
|
+
|
374
434
|
## Status and Roadmap
|
375
435
|
|
376
|
-
This library is in an early stage of development but you are
|
436
|
+
This library is in an early stage of development but you are encouraged to try it
|
377
437
|
out and provide feedback.
|
378
438
|
|
379
439
|
For planned features check out [the issues](https://github.com/dryrb/dry-data/labels/feature).
|
data/dry-data.gemspec
CHANGED
@@ -31,6 +31,7 @@ Gem::Specification.new do |spec|
|
|
31
31
|
spec.add_runtime_dependency 'dry-container', '~> 0.2'
|
32
32
|
spec.add_runtime_dependency 'dry-equalizer', '~> 0.2'
|
33
33
|
spec.add_runtime_dependency 'dry-configurable', '~> 0.1'
|
34
|
+
spec.add_runtime_dependency 'dry-logic', '~> 0.1'
|
34
35
|
spec.add_runtime_dependency 'inflecto', '~> 0.0.0', '>= 0.0.2'
|
35
36
|
spec.add_runtime_dependency 'kleisli', '~> 0.2'
|
36
37
|
|
data/lib/dry/data.rb
CHANGED
@@ -13,7 +13,6 @@ require 'dry/data/container'
|
|
13
13
|
require 'dry/data/type'
|
14
14
|
require 'dry/data/struct'
|
15
15
|
require 'dry/data/value'
|
16
|
-
require 'dry/data/dsl'
|
17
16
|
|
18
17
|
module Dry
|
19
18
|
module Data
|
@@ -53,7 +52,7 @@ module Dry
|
|
53
52
|
def self.register_class(klass)
|
54
53
|
container.register(
|
55
54
|
Inflecto.underscore(klass).gsub('/', '.'),
|
56
|
-
Type.new(klass.method(:new), klass)
|
55
|
+
Type.new(klass.method(:new), primitive: klass)
|
57
56
|
)
|
58
57
|
end
|
59
58
|
|
@@ -98,11 +97,6 @@ module Dry
|
|
98
97
|
Inflecto.underscore(klass).gsub('/', '.')
|
99
98
|
end
|
100
99
|
|
101
|
-
def self.type(*args, &block)
|
102
|
-
dsl = DSL.new(container)
|
103
|
-
block ? yield(dsl) : registry[args.first]
|
104
|
-
end
|
105
|
-
|
106
100
|
def self.type_map
|
107
101
|
@type_map ||= ThreadSafe::Cache.new
|
108
102
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'dry/data/decorator'
|
2
|
+
require 'dry/data/constraints'
|
3
|
+
|
4
|
+
module Dry
|
5
|
+
module Data
|
6
|
+
class Constrained
|
7
|
+
include Decorator
|
8
|
+
include TypeBuilder
|
9
|
+
|
10
|
+
attr_reader :rule
|
11
|
+
|
12
|
+
def initialize(type, options)
|
13
|
+
super
|
14
|
+
@rule = options.fetch(:rule)
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(input)
|
18
|
+
result = try(input)
|
19
|
+
|
20
|
+
if valid?(result)
|
21
|
+
result
|
22
|
+
else
|
23
|
+
raise ConstraintError, "#{input.inspect} violates constraints"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
alias_method :[], :call
|
27
|
+
|
28
|
+
def try(input)
|
29
|
+
type[input]
|
30
|
+
end
|
31
|
+
|
32
|
+
def valid?(input)
|
33
|
+
super && rule.(input).success?
|
34
|
+
end
|
35
|
+
|
36
|
+
def constrained(options)
|
37
|
+
with(rule: rule & Data.Rule(primitive, options))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/dry/data/constraints.rb
CHANGED
@@ -1,8 +1,16 @@
|
|
1
|
-
require 'dry/
|
2
|
-
require 'dry/
|
1
|
+
require 'dry/logic/rule_compiler'
|
2
|
+
require 'dry/logic/predicates'
|
3
3
|
|
4
4
|
module Dry
|
5
5
|
module Data
|
6
|
+
module Predicates
|
7
|
+
include Logic::Predicates
|
8
|
+
|
9
|
+
predicate(:type?) do |type, value|
|
10
|
+
value.kind_of?(type)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
6
14
|
def self.Rule(primitive, options)
|
7
15
|
rule_compiler.(
|
8
16
|
options.map { |key, val|
|
@@ -12,7 +20,7 @@ module Dry
|
|
12
20
|
end
|
13
21
|
|
14
22
|
def self.rule_compiler
|
15
|
-
@rule_compiler ||=
|
23
|
+
@rule_compiler ||= Logic::RuleCompiler.new(Data::Predicates)
|
16
24
|
end
|
17
25
|
end
|
18
26
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Dry
|
2
|
+
module Data
|
3
|
+
module Decorator
|
4
|
+
attr_reader :type, :options
|
5
|
+
|
6
|
+
def initialize(type, options = {})
|
7
|
+
@type = type
|
8
|
+
@options = options
|
9
|
+
end
|
10
|
+
|
11
|
+
def constructor
|
12
|
+
type.constructor
|
13
|
+
end
|
14
|
+
|
15
|
+
def valid?(input)
|
16
|
+
type.valid?(input)
|
17
|
+
end
|
18
|
+
|
19
|
+
def respond_to_missing?(meth, include_private = false)
|
20
|
+
super || type.respond_to?(meth)
|
21
|
+
end
|
22
|
+
|
23
|
+
def with(new_options)
|
24
|
+
self.class.new(type, options.merge(new_options))
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def method_missing(meth, *args, &block)
|
30
|
+
if type.respond_to?(meth)
|
31
|
+
response = type.__send__(meth, *args, &block)
|
32
|
+
|
33
|
+
if response.kind_of?(type.class)
|
34
|
+
self.class.new(response, options)
|
35
|
+
else
|
36
|
+
response
|
37
|
+
end
|
38
|
+
else
|
39
|
+
super
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'dry/data/decorator'
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Data
|
5
|
+
class Default
|
6
|
+
include Decorator
|
7
|
+
include TypeBuilder
|
8
|
+
|
9
|
+
attr_reader :value
|
10
|
+
|
11
|
+
def initialize(type, options)
|
12
|
+
super
|
13
|
+
@value = options.fetch(:value)
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(input)
|
17
|
+
input.nil? ? value : type[input]
|
18
|
+
end
|
19
|
+
alias_method :[], :call
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'dry/data/decorator'
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Data
|
5
|
+
class Enum
|
6
|
+
include Decorator
|
7
|
+
|
8
|
+
attr_reader :values
|
9
|
+
|
10
|
+
def initialize(type, options)
|
11
|
+
super
|
12
|
+
@values = options.fetch(:values).freeze
|
13
|
+
@values.each(&:freeze)
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(input)
|
17
|
+
case input
|
18
|
+
when Fixnum then type[values[input]]
|
19
|
+
else type[input] end
|
20
|
+
end
|
21
|
+
alias_method :[], :call
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/dry/data/optional.rb
CHANGED
@@ -1,15 +1,10 @@
|
|
1
|
+
require 'dry/data/decorator'
|
2
|
+
|
1
3
|
module Dry
|
2
4
|
module Data
|
3
5
|
class Optional
|
4
|
-
|
5
|
-
|
6
|
-
def initialize(type)
|
7
|
-
@type = type
|
8
|
-
end
|
9
|
-
|
10
|
-
def valid?(input)
|
11
|
-
type.valid?(input)
|
12
|
-
end
|
6
|
+
include Decorator
|
7
|
+
include TypeBuilder
|
13
8
|
|
14
9
|
def call(input)
|
15
10
|
Maybe(type[input])
|
data/lib/dry/data/struct.rb
CHANGED
data/lib/dry/data/sum_type.rb
CHANGED
@@ -16,16 +16,12 @@ module Dry
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def call(input)
|
19
|
-
|
20
|
-
value = left[input]
|
19
|
+
value = left.try(input)
|
21
20
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
end
|
27
|
-
rescue TypeError
|
28
|
-
right[input]
|
21
|
+
if left.valid?(value)
|
22
|
+
value
|
23
|
+
else
|
24
|
+
right[value]
|
29
25
|
end
|
30
26
|
end
|
31
27
|
alias_method :[], :call
|
data/lib/dry/data/type.rb
CHANGED
@@ -1,35 +1,18 @@
|
|
1
1
|
require 'dry/data/type/hash'
|
2
2
|
require 'dry/data/type/array'
|
3
|
-
require 'dry/data/
|
4
|
-
|
5
|
-
require 'dry/data/sum_type'
|
6
|
-
require 'dry/data/optional'
|
3
|
+
require 'dry/data/type_builder'
|
7
4
|
|
8
5
|
module Dry
|
9
6
|
module Data
|
10
7
|
class Type
|
11
|
-
|
12
|
-
|
8
|
+
include Dry::Equalizer(:constructor, :options)
|
9
|
+
include TypeBuilder
|
13
10
|
|
14
|
-
|
15
|
-
attr_reader :rule
|
16
|
-
|
17
|
-
def initialize(constructor, primitive, rule)
|
18
|
-
super(constructor, primitive)
|
19
|
-
@rule = rule
|
20
|
-
end
|
11
|
+
attr_reader :constructor
|
21
12
|
|
22
|
-
|
23
|
-
result = super(input)
|
13
|
+
attr_reader :options
|
24
14
|
|
25
|
-
|
26
|
-
result
|
27
|
-
else
|
28
|
-
raise ConstraintError, "#{input.inspect} violates constraints"
|
29
|
-
end
|
30
|
-
end
|
31
|
-
alias_method :[], :call
|
32
|
-
end
|
15
|
+
attr_reader :primitive
|
33
16
|
|
34
17
|
def self.[](primitive)
|
35
18
|
if primitive == ::Array
|
@@ -41,29 +24,14 @@ module Dry
|
|
41
24
|
end
|
42
25
|
end
|
43
26
|
|
44
|
-
def self.
|
45
|
-
if input.is_a?(primitive)
|
46
|
-
input
|
47
|
-
else
|
48
|
-
raise TypeError, "#{input.inspect} has invalid type"
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
def self.passthrough_constructor(input)
|
27
|
+
def self.constructor(input)
|
53
28
|
input
|
54
29
|
end
|
55
30
|
|
56
|
-
def initialize(constructor,
|
31
|
+
def initialize(constructor, options = {})
|
57
32
|
@constructor = constructor
|
58
|
-
@
|
59
|
-
|
60
|
-
|
61
|
-
def enum(*values)
|
62
|
-
Enum.new(values, constrained(inclusion: values))
|
63
|
-
end
|
64
|
-
|
65
|
-
def optional
|
66
|
-
Optional.new(Data['nil'] | self)
|
33
|
+
@options = options
|
34
|
+
@primitive = options.fetch(:primitive)
|
67
35
|
end
|
68
36
|
|
69
37
|
def name
|
@@ -75,12 +43,12 @@ module Dry
|
|
75
43
|
end
|
76
44
|
alias_method :[], :call
|
77
45
|
|
78
|
-
def
|
79
|
-
input
|
46
|
+
def try(input)
|
47
|
+
call(input)
|
80
48
|
end
|
81
49
|
|
82
|
-
def
|
83
|
-
|
50
|
+
def valid?(input)
|
51
|
+
input.is_a?(primitive)
|
84
52
|
end
|
85
53
|
end
|
86
54
|
end
|
data/lib/dry/data/type/array.rb
CHANGED
@@ -16,7 +16,7 @@ module Dry
|
|
16
16
|
array_constructor = self.class
|
17
17
|
.method(:constructor).to_proc.curry.(constructor, member_constructor)
|
18
18
|
|
19
|
-
self.class.new(array_constructor, primitive)
|
19
|
+
self.class.new(array_constructor, primitive: primitive, member: member_constructor)
|
20
20
|
end
|
21
21
|
end
|
22
22
|
end
|
data/lib/dry/data/type/hash.rb
CHANGED
@@ -0,0 +1,31 @@
|
|
1
|
+
module Dry
|
2
|
+
module Data
|
3
|
+
module TypeBuilder
|
4
|
+
def |(other)
|
5
|
+
SumType.new(self, other)
|
6
|
+
end
|
7
|
+
|
8
|
+
def optional
|
9
|
+
Optional.new(Data['nil'] | self)
|
10
|
+
end
|
11
|
+
|
12
|
+
def constrained(options)
|
13
|
+
Constrained.new(self, rule: Data.Rule(primitive, options))
|
14
|
+
end
|
15
|
+
|
16
|
+
def default(value)
|
17
|
+
Default.new(self, value: value)
|
18
|
+
end
|
19
|
+
|
20
|
+
def enum(*values)
|
21
|
+
Enum.new(constrained(inclusion: values), values: values)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
require 'dry/data/default'
|
28
|
+
require 'dry/data/constrained'
|
29
|
+
require 'dry/data/enum'
|
30
|
+
require 'dry/data/optional'
|
31
|
+
require 'dry/data/sum_type'
|
data/lib/dry/data/types.rb
CHANGED
@@ -26,7 +26,7 @@ module Dry
|
|
26
26
|
COERCIBLE.each do |name, primitive|
|
27
27
|
register(
|
28
28
|
"coercible.#{name}",
|
29
|
-
Type[primitive].new(Kernel.method(primitive.name), primitive)
|
29
|
+
Type[primitive].new(Kernel.method(primitive.name), primitive: primitive)
|
30
30
|
)
|
31
31
|
end
|
32
32
|
|
@@ -34,7 +34,7 @@ module Dry
|
|
34
34
|
ALL_PRIMITIVES.each do |name, primitive|
|
35
35
|
register(
|
36
36
|
"strict.#{name}",
|
37
|
-
Type[primitive].new(Type.method(:
|
37
|
+
Type[primitive].new(Type.method(:constructor), primitive: primitive).constrained(type: primitive)
|
38
38
|
)
|
39
39
|
end
|
40
40
|
|
@@ -42,7 +42,7 @@ module Dry
|
|
42
42
|
ALL_PRIMITIVES.each do |name, primitive|
|
43
43
|
register(
|
44
44
|
name.to_s,
|
45
|
-
Type[primitive].new(Type.method(:
|
45
|
+
Type[primitive].new(Type.method(:constructor), primitive: primitive)
|
46
46
|
)
|
47
47
|
end
|
48
48
|
|
data/lib/dry/data/types/form.rb
CHANGED
@@ -3,27 +3,27 @@ require 'dry/data/coercions/form'
|
|
3
3
|
module Dry
|
4
4
|
module Data
|
5
5
|
register('form.nil') do
|
6
|
-
Type.new(Coercions::Form.method(:to_nil), NilClass)
|
6
|
+
Type.new(Coercions::Form.method(:to_nil), primitive: NilClass)
|
7
7
|
end
|
8
8
|
|
9
9
|
register('form.date') do
|
10
|
-
Type.new(Coercions::Form.method(:to_date), Date)
|
10
|
+
Type.new(Coercions::Form.method(:to_date), primitive: Date)
|
11
11
|
end
|
12
12
|
|
13
13
|
register('form.date_time') do
|
14
|
-
Type.new(Coercions::Form.method(:to_date_time), DateTime)
|
14
|
+
Type.new(Coercions::Form.method(:to_date_time), primitive: DateTime)
|
15
15
|
end
|
16
16
|
|
17
17
|
register('form.time') do
|
18
|
-
Type.new(Coercions::Form.method(:to_time), Time)
|
18
|
+
Type.new(Coercions::Form.method(:to_time), primitive: Time)
|
19
19
|
end
|
20
20
|
|
21
21
|
register('form.true') do
|
22
|
-
Type.new(Coercions::Form.method(:to_true), TrueClass)
|
22
|
+
Type.new(Coercions::Form.method(:to_true), primitive: TrueClass)
|
23
23
|
end
|
24
24
|
|
25
25
|
register('form.false') do
|
26
|
-
Type.new(Coercions::Form.method(:to_false), FalseClass)
|
26
|
+
Type.new(Coercions::Form.method(:to_false), primitive: FalseClass)
|
27
27
|
end
|
28
28
|
|
29
29
|
register('form.bool') do
|
@@ -31,15 +31,15 @@ module Dry
|
|
31
31
|
end
|
32
32
|
|
33
33
|
register('form.int') do
|
34
|
-
Type.new(Coercions::Form.method(:to_int), Fixnum)
|
34
|
+
Type.new(Coercions::Form.method(:to_int), primitive: Fixnum)
|
35
35
|
end
|
36
36
|
|
37
37
|
register('form.float') do
|
38
|
-
Type.new(Coercions::Form.method(:to_float), Float)
|
38
|
+
Type.new(Coercions::Form.method(:to_float), primitive: Float)
|
39
39
|
end
|
40
40
|
|
41
41
|
register('form.decimal') do
|
42
|
-
Type.new(Coercions::Form.method(:to_decimal), BigDecimal)
|
42
|
+
Type.new(Coercions::Form.method(:to_decimal), primitive: BigDecimal)
|
43
43
|
end
|
44
44
|
end
|
45
45
|
end
|
data/lib/dry/data/value.rb
CHANGED
data/lib/dry/data/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dry-data
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Piotr Solnica
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2016-01-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thread_safe
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0.1'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: dry-logic
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.1'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.1'
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: inflecto
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -166,17 +180,19 @@ files:
|
|
166
180
|
- lib/dry/data.rb
|
167
181
|
- lib/dry/data/coercions/form.rb
|
168
182
|
- lib/dry/data/compiler.rb
|
183
|
+
- lib/dry/data/constrained.rb
|
169
184
|
- lib/dry/data/constraints.rb
|
170
185
|
- lib/dry/data/container.rb
|
171
|
-
- lib/dry/data/
|
186
|
+
- lib/dry/data/decorator.rb
|
187
|
+
- lib/dry/data/default.rb
|
188
|
+
- lib/dry/data/enum.rb
|
172
189
|
- lib/dry/data/optional.rb
|
173
190
|
- lib/dry/data/struct.rb
|
174
191
|
- lib/dry/data/sum_type.rb
|
175
192
|
- lib/dry/data/type.rb
|
176
193
|
- lib/dry/data/type/array.rb
|
177
|
-
- lib/dry/data/type/constrained.rb
|
178
|
-
- lib/dry/data/type/enum.rb
|
179
194
|
- lib/dry/data/type/hash.rb
|
195
|
+
- lib/dry/data/type_builder.rb
|
180
196
|
- lib/dry/data/types.rb
|
181
197
|
- lib/dry/data/types/form.rb
|
182
198
|
- lib/dry/data/value.rb
|
data/lib/dry/data/dsl.rb
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
require 'dry/data/constraints'
|
2
|
-
|
3
|
-
module Dry
|
4
|
-
module Data
|
5
|
-
class Type
|
6
|
-
class Constrained < Type
|
7
|
-
attr_reader :rule
|
8
|
-
|
9
|
-
def initialize(constructor, primitive, rule)
|
10
|
-
super(constructor, primitive)
|
11
|
-
@rule = rule
|
12
|
-
end
|
13
|
-
|
14
|
-
def call(input)
|
15
|
-
result = super(input)
|
16
|
-
|
17
|
-
if valid?(result)
|
18
|
-
result
|
19
|
-
else
|
20
|
-
raise ConstraintError, "#{input.inspect} violates constraints"
|
21
|
-
end
|
22
|
-
end
|
23
|
-
alias_method :[], :call
|
24
|
-
|
25
|
-
def valid?(input)
|
26
|
-
super && rule.(input).success?
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
def constrained(options)
|
31
|
-
Constrained.new(constructor, primitive, Data.Rule(primitive, options))
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
data/lib/dry/data/type/enum.rb
DELETED
@@ -1,27 +0,0 @@
|
|
1
|
-
module Dry
|
2
|
-
module Data
|
3
|
-
class Type
|
4
|
-
class Enum
|
5
|
-
attr_reader :values
|
6
|
-
attr_reader :type
|
7
|
-
|
8
|
-
def initialize(values, type)
|
9
|
-
@values = values.freeze
|
10
|
-
@type = type
|
11
|
-
values.each(&:freeze)
|
12
|
-
end
|
13
|
-
|
14
|
-
def primitive
|
15
|
-
type.primitive
|
16
|
-
end
|
17
|
-
|
18
|
-
def call(input)
|
19
|
-
case input
|
20
|
-
when Fixnum then type[values[input]]
|
21
|
-
else type[input] end
|
22
|
-
end
|
23
|
-
alias_method :[], :call
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|