dry-data 0.4.2 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0d798aac2e4dd90dc419ed04f7366d04a5aae2a1
4
- data.tar.gz: 35060cab0302057a57e3f098ec2842cb9824840d
3
+ metadata.gz: 1b1120eb4914c94c999641d81a01ede2deb790f2
4
+ data.tar.gz: 58700f1d2f8d0de68428212b15feba6111de499f
5
5
  SHA512:
6
- metadata.gz: b60ca79d4da30f0af580450b3ea05b4d2fc09cf45235f419ef38d7de4a0f0e87cb5640e925433d7aa5d6a6a3153447b7505f973e9460e23a529ded9903827893
7
- data.tar.gz: d71b05244589161fbe4a464f11eeb56ed94b7a8408b4c29e6ad934231b998545e9b3fdf3ba4bbcc01c097763489c6c0a43571f7b91a14f831a63cb859494a9d5
6
+ metadata.gz: 12003922d8e399f312bb39ebc53c5f37bde3be4a7c5d5d9e3d667fb97358df733810fe096c60c0ca831f9dbcad7ed3b9b8cd81f2017699d12cbe8b950d4bfa05
7
+ data.tar.gz: ff1ee72ab2ea48e10458352e82a1d07521f644760107b33f66b67bda5f1067c7dcb77c9fb0d609489a2d278a92c1fe11d139c88d508fe67415a87155fdd5701d
@@ -8,6 +8,7 @@ rvm:
8
8
  - 2.0
9
9
  - 2.1
10
10
  - 2.2
11
+ - 2.3.0
11
12
  - rbx-2
12
13
  - jruby-9000
13
14
  - ruby-head
@@ -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
@@ -2,8 +2,6 @@ source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
4
 
5
- gem 'dry-validation'
6
-
7
5
  group :tools do
8
6
  gem 'byebug', platform: :mri
9
7
  end
data/README.md CHANGED
@@ -14,7 +14,50 @@
14
14
  [![Test Coverage](https://codeclimate.com/github/dryrb/dry-data/badges/coverage.svg)][codeclimate]
15
15
  [![Inline docs](http://inch-ci.org/github/dryrb/dry-data.svg?branch=master)][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
- - `nil`
54
- - `symbol`
55
- - `class`
56
- - `true`
57
- - `false`
58
- - `date`
59
- - `date_time`
60
- - `time`
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
- - `coercible.string`
65
- - `coercible.int`
66
- - `coercible.float`
67
- - `coercible.decimal`
68
- - `coercible.array`
69
- - `coercible.hash`
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
- - `maybe.strict.string`
74
- - `maybe.strict.int`
75
- - `maybe.strict.float`
76
- - `maybe.strict.decimal`
77
- - `maybe.strict.array`
78
- - `maybe.strict.hash`
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
- - `maybe.coercible.string`
83
- - `maybe.coercible.int`
84
- - `maybe.coercible.float`
85
- - `maybe.coercible.decimal`
86
- - `maybe.coercible.array`
87
- - `maybe.coercible.hash`
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
- - `form.nil`
92
- - `form.date`
93
- - `form.date_time`
94
- - `form.time`
95
- - `form.true`
96
- - `form.false`
97
- - `form.bool`
98
- - `form.int`
99
- - `form.float`
100
- - `form.decimal`
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
- ### Accessing Built-in Types
194
+ ### Strict vs Coercible Types
103
195
 
104
196
  ``` ruby
105
- # default passthrough category
106
- float = Dry::Data["float"]
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
- string = Dry::Data["coercible.string"]
119
- array = Dry::Data["coercible.array"]
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
- date = Dry::Data["form.date"]
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 types
208
+ ### Optional Types
130
209
 
131
210
  All built-in types have their optional versions too, you can access them under
132
- `"maybe.strict"` and `"maybe.coercible"` categories:
211
+ `"Types::Maybe::Strict"` and `"Maybe::Coercible"` categories:
133
212
 
134
213
  ``` ruby
135
- maybe_int = Dry::Data["maybe.strict.int"]
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
- maybe_coercible_float = Dry::Data["maybe.coercible.float"]
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 = Dry::Data["string"].optional
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 `Dry::Data['true'] | Dry::Data['false']`
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 = Dry::Data['strict.nil'] | Dry::Data['strict.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-validation`](https://github.com/dryrb/dry-validation)
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
- require "dry/data/type/constrained"
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 = Dry::Data['strict.string'].constrained(
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
- ### Setting Type Constants
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
- ### Defining a hash with explicit schema
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
- ### Hash Schema
346
+ #### Hash Schema
312
347
 
313
348
  ``` ruby
314
349
  # using simple kernel coercions
315
- hash = Dry::Data['hash'].schema(name: 'string', age: 'coercible.int')
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 = Dry::Data['hash'].schema(name: 'string', birthdate: 'form.date')
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
- ### Strict Hash
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 = Dry::Data['hash'].strict(name: 'string', age: 'coercible.int')
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
- ### Symbolized Hash
373
+ #### Symbolized Schema
339
374
 
340
375
  Symbolized hash will turn string key names into symbols
341
376
 
342
377
  ``` ruby
343
- hash = Dry::Data['hash'].symbolized(name: 'string', age: 'coercible.int')
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
- ### Defining a struct
384
+ ### Arrays
350
385
 
351
- You can define struct objects which will have attribute readers for specified
352
- attributes using a simple dsl:
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, "maybe.coercible.string"
357
- attribute :age, "coercible.int"
401
+ attribute :name, Types::Maybe::Coercible::String
402
+ attribute :age, Types::Coercible::Int
358
403
  end
359
404
 
360
- # becomes available like any other type
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 = user_type[name: 'Jane', age: '21']
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 encauraged to try it
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).
@@ -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
 
@@ -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
@@ -1,8 +1,16 @@
1
- require 'dry/validation/rule_compiler'
2
- require 'dry/validation/predicates'
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 ||= Validation::RuleCompiler.new(Validation::Predicates)
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
@@ -1,15 +1,10 @@
1
+ require 'dry/data/decorator'
2
+
1
3
  module Dry
2
4
  module Data
3
5
  class Optional
4
- attr_reader :type
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])
@@ -10,8 +10,8 @@ module Dry
10
10
  Data.register_class(klass) unless klass == Value
11
11
  end
12
12
 
13
- def self.attribute(*args)
14
- attributes(Hash[[args]])
13
+ def self.attribute(name, type)
14
+ attributes(name => type)
15
15
  end
16
16
 
17
17
  def self.attributes(new_schema)
@@ -16,16 +16,12 @@ module Dry
16
16
  end
17
17
 
18
18
  def call(input)
19
- begin
20
- value = left[input]
19
+ value = left.try(input)
21
20
 
22
- if left.valid?(value)
23
- value
24
- else
25
- right[value]
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
@@ -1,35 +1,18 @@
1
1
  require 'dry/data/type/hash'
2
2
  require 'dry/data/type/array'
3
- require 'dry/data/type/enum'
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
- attr_reader :constructor
12
- attr_reader :primitive
8
+ include Dry::Equalizer(:constructor, :options)
9
+ include TypeBuilder
13
10
 
14
- class Constrained < Type
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
- def call(input)
23
- result = super(input)
13
+ attr_reader :options
24
14
 
25
- if rule.(result).success?
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.strict_constructor(primitive, input)
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, primitive)
31
+ def initialize(constructor, options = {})
57
32
  @constructor = constructor
58
- @primitive = primitive
59
- end
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 valid?(input)
79
- input.is_a?(primitive)
46
+ def try(input)
47
+ call(input)
80
48
  end
81
49
 
82
- def |(other)
83
- SumType.new(self, other)
50
+ def valid?(input)
51
+ input.is_a?(primitive)
84
52
  end
85
53
  end
86
54
  end
@@ -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
@@ -58,7 +58,7 @@ module Dry
58
58
 
59
59
  self.class.new(
60
60
  self.class.method(meth).to_proc.curry.(constructor, value_constructors),
61
- primitive
61
+ primitive: primitive, schema: value_constructors
62
62
  )
63
63
  end
64
64
  end
@@ -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'
@@ -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(:strict_constructor).to_proc.curry.(primitive), primitive)
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(:passthrough_constructor), primitive)
45
+ Type[primitive].new(Type.method(:constructor), primitive: primitive)
46
46
  )
47
47
  end
48
48
 
@@ -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
@@ -1,3 +1,5 @@
1
+ require 'dry/data/struct'
2
+
1
3
  module Dry
2
4
  module Data
3
5
  class Value < Struct
@@ -1,5 +1,5 @@
1
1
  module Dry
2
2
  module Data
3
- VERSION = '0.4.2'.freeze
3
+ VERSION = '0.5.0'.freeze
4
4
  end
5
5
  end
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.2
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: 2015-12-27 00:00:00.000000000 Z
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/dsl.rb
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
@@ -1,15 +0,0 @@
1
- module Dry
2
- module Data
3
- class DSL
4
- attr_reader :container
5
-
6
- def initialize(container)
7
- @container = container
8
- end
9
-
10
- def [](name)
11
- container[name]
12
- end
13
- end
14
- end
15
- end
@@ -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
@@ -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