dry-data 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/CHANGELOG.md +14 -1
- data/README.md +78 -29
- data/lib/dry-data.rb +1 -0
- data/lib/dry/data.rb +9 -2
- data/lib/dry/data/coercions/form.rb +83 -0
- data/lib/dry/data/compiler.rb +39 -0
- data/lib/dry/data/struct.rb +2 -3
- data/lib/dry/data/sum_type.rb +7 -3
- data/lib/dry/data/type/hash.rb +33 -3
- data/lib/dry/data/types.rb +2 -0
- data/lib/dry/data/types/form.rb +41 -0
- data/lib/dry/data/version.rb +1 -1
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 63d84521c44133ae20569f13cfe37198d145b84b
|
4
|
+
data.tar.gz: 34a54445feeb95d3caffec626f3cca303eaa3455
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c4ed0e1ca95bdb0e7d07dd744480da21d0316d96a43dc9e52e35963edbbc45f4eca370b4116a5ef4db51f6c0dda8e68b8f0b836ab1235fd7b2850ecfaa4f4558
|
7
|
+
data.tar.gz: 87f09d1e60fbfef3a96750561ef81e354ced647d6e4371d02e49fd204e6229684b50e194f7ed3c6cf8c8d312e1d329b60de0f876f5a5cfca1fed9ffafb92cfe7
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,16 @@
|
|
1
|
-
v0.0
|
1
|
+
# v0.1.0 2015-11-27
|
2
|
+
|
3
|
+
## Added
|
4
|
+
|
5
|
+
* `form.*` coercible types (solnic)
|
6
|
+
* `Type::Hash#strict` for defining hashes with a strict schema (solnic)
|
7
|
+
* `Type::Hash#symbolized` for defining hashes that will symbolize keys (solnic)
|
8
|
+
* `Dry::Data.register_class` short-cut interface for registering a class and
|
9
|
+
setting its `.new` method as the constructor (solnic)
|
10
|
+
* `Dry::Data::Compiler` for building a type from a simple ast (solnic)
|
11
|
+
|
12
|
+
[Compare v0.0.1...HEAD](https://github.com/dryrb/dry-data/compare/v0.0.1...HEAD)
|
13
|
+
|
14
|
+
# v0.0.1 2015-10-05
|
2
15
|
|
3
16
|
First public release
|
data/README.md
CHANGED
@@ -24,36 +24,30 @@ Or install it yourself as:
|
|
24
24
|
|
25
25
|
$ gem install dry-data
|
26
26
|
|
27
|
-
## Why?
|
28
|
-
|
29
|
-
Unlike seemingly similar libraries like virtus, attrio, fast_attrs, attribs etc.
|
30
|
-
`Dry::Data` provides you an interface to explicitly specify data types you want
|
31
|
-
to use in your application domain which gives you type-safety and *simple* coercion
|
32
|
-
mechanism using built-in coercion methods on the kernel.
|
33
|
-
|
34
|
-
Main difference is that `Dry::Data` is not designed to handle all kinds of complex
|
35
|
-
coercions that are typically required when dealing with, let's say, form params
|
36
|
-
in a web application. Its primary focus is to allow you to specify the exact shape
|
37
|
-
of the custom application data types to avoid silly bugs that are often hard to debug
|
38
|
-
(`NoMethodError: undefined method `size' for nil:NilClass` anyone?).
|
39
|
-
|
40
27
|
## Usage
|
41
28
|
|
42
|
-
|
43
|
-
|
44
|
-
|
29
|
+
You can use `dry-data` for defining various data types in your application, like
|
30
|
+
domain entities and value objects or hashes with coercible values used to handle
|
31
|
+
params.
|
45
32
|
|
33
|
+
Built-in types are grouped under 5 categories:
|
46
34
|
|
47
|
-
|
35
|
+
- default: pass-through without any checks
|
36
|
+
- `strict` - doesn't coerce and checks the input type against the primitive class
|
37
|
+
- `coercible` - tries to coerce and raises type-error if it failed
|
38
|
+
- `form` - non-strict coercion types suitable for form params
|
39
|
+
- `maybe` - accepts either a nil or something else
|
40
|
+
|
41
|
+
### Built-in Type Categories
|
48
42
|
|
49
43
|
Coercible types using kernel coercion methods:
|
50
44
|
|
51
|
-
- `string`
|
52
|
-
- `int`
|
53
|
-
- `float`
|
54
|
-
- `decimal`
|
55
|
-
- `array`
|
56
|
-
- `hash`
|
45
|
+
- `coercible.string`
|
46
|
+
- `coercible.int`
|
47
|
+
- `coercible.float`
|
48
|
+
- `coercible.decimal`
|
49
|
+
- `coercible.array`
|
50
|
+
- `coercible.hash`
|
57
51
|
|
58
52
|
Non-coercible:
|
59
53
|
|
@@ -64,14 +58,19 @@ Non-coercible:
|
|
64
58
|
- `date_time`
|
65
59
|
- `time`
|
66
60
|
|
67
|
-
|
61
|
+
Form-coercible types:
|
68
62
|
|
69
|
-
|
63
|
+
- `form.date`
|
64
|
+
- `form.date_time`
|
65
|
+
- `form.time`
|
66
|
+
- `form.true`
|
67
|
+
- `form.false`
|
68
|
+
- `form.bool`
|
69
|
+
- `form.int`
|
70
|
+
- `form.float`
|
71
|
+
- `form.decimal`
|
70
72
|
|
71
|
-
|
72
|
-
- `strict` - doesn't coerce and checks the input type against the primitive class
|
73
|
-
- `coercible` - tries to coerce and raises type-error if it failed
|
74
|
-
- `maybe` - accepts either a nil or something else
|
73
|
+
### Accessing Built-in Types
|
75
74
|
|
76
75
|
``` ruby
|
77
76
|
# default passthrough category
|
@@ -92,6 +91,10 @@ array = Dry::Data["coercible.array"]
|
|
92
91
|
|
93
92
|
string[:foo] # => 'foo'
|
94
93
|
array[:foo] # => [:foo]
|
94
|
+
|
95
|
+
# form group
|
96
|
+
date = Dry::Data["form.date"]
|
97
|
+
date['2015-11-29'] # => #<Date: 2015-11-29 ((2457356j,0s,0n),+0s,2299161j)>
|
95
98
|
```
|
96
99
|
|
97
100
|
### Optional types
|
@@ -132,8 +135,54 @@ maybe_string['something'].fmap(&:upcase).value
|
|
132
135
|
# => "SOMETHING"
|
133
136
|
```
|
134
137
|
|
138
|
+
### Defining a hash with explicit schema
|
139
|
+
|
140
|
+
The built-in hash type has constructors that you can use to define hashes with
|
141
|
+
explicit schemas and coercible values using the built-in types.
|
142
|
+
|
143
|
+
### Hash Schema
|
144
|
+
|
145
|
+
``` ruby
|
146
|
+
# using simple kernel coercions
|
147
|
+
hash = Dry::Data['hash'].schema(name: 'string', age: 'coercible.int')
|
148
|
+
|
149
|
+
hash[name: 'Jane', age: '21']
|
150
|
+
# => { :name => "Jane", :age => 21 }
|
151
|
+
|
152
|
+
# using form param coercions
|
153
|
+
hash = Dry::Data['hash'].schema(name: 'string', birthdate: 'form.date')
|
154
|
+
|
155
|
+
hash[name: 'Jane', birthdate: '1994-11-11']
|
156
|
+
# => { :name => "Jane", :birthdate => #<Date: 1994-11-11 ((2449668j,0s,0n),+0s,2299161j)> }
|
157
|
+
```
|
158
|
+
|
159
|
+
### Strict Hash
|
160
|
+
|
161
|
+
Strict hash will raise errors when keys are missing or value types are incorrect.
|
162
|
+
|
163
|
+
``` ruby
|
164
|
+
hash = Dry::Data['hash'].strict(name: 'string', age: 'coercible.int')
|
165
|
+
|
166
|
+
hash[email: 'jane@doe.org', name: 'Jane', age: 21]
|
167
|
+
# => Dry::Data::SchemaKeyError: :email is missing in Hash input
|
168
|
+
```
|
169
|
+
|
170
|
+
### Symbolized Hash
|
171
|
+
|
172
|
+
Symbolized hash will turn string key names into symbols
|
173
|
+
|
174
|
+
``` ruby
|
175
|
+
hash = Dry::Data['hash'].symbolized(name: 'string', age: 'coercible.int')
|
176
|
+
|
177
|
+
hash['name' => 'Jane', 'age' => '21']
|
178
|
+
# => { :name => "Jane", :age => 21 }
|
179
|
+
```
|
180
|
+
|
135
181
|
### Defining a struct
|
136
182
|
|
183
|
+
You can define struct objects which will have attribute readers for specified
|
184
|
+
attributes using a simple dsl:
|
185
|
+
|
137
186
|
``` ruby
|
138
187
|
class User < Dry::Data::Struct
|
139
188
|
attribute :name, "maybe.coercible.string"
|
data/lib/dry-data.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'dry/data'
|
data/lib/dry/data.rb
CHANGED
@@ -33,8 +33,15 @@ module Dry
|
|
33
33
|
@container ||= Container.new
|
34
34
|
end
|
35
35
|
|
36
|
-
def self.register(name, type)
|
37
|
-
container.register(name, type)
|
36
|
+
def self.register(name, type = nil, &block)
|
37
|
+
container.register(name, type || block.call)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.register_class(klass)
|
41
|
+
container.register(
|
42
|
+
Inflecto.underscore(klass).gsub('/', '.'),
|
43
|
+
Type.new(klass.method(:new), klass)
|
44
|
+
)
|
38
45
|
end
|
39
46
|
|
40
47
|
def self.[](name)
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'bigdecimal'
|
3
|
+
require 'bigdecimal/util'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Data
|
7
|
+
module Coercions
|
8
|
+
module Form
|
9
|
+
TRUE_VALUES = %w[1 on t true y yes].freeze
|
10
|
+
FALSE_VALUES = %w[0 off f false n no].freeze
|
11
|
+
BOOLEAN_MAP = Hash[TRUE_VALUES.product([true]) + FALSE_VALUES.product([false])].freeze
|
12
|
+
|
13
|
+
def self.to_date(input)
|
14
|
+
Date.parse(input)
|
15
|
+
rescue ArgumentError
|
16
|
+
input
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.to_date_time(input)
|
20
|
+
DateTime.parse(input)
|
21
|
+
rescue ArgumentError
|
22
|
+
input
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.to_time(input)
|
26
|
+
Time.parse(input)
|
27
|
+
rescue ArgumentError
|
28
|
+
input
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.to_true(input)
|
32
|
+
BOOLEAN_MAP.fetch(input, input)
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.to_false(input)
|
36
|
+
BOOLEAN_MAP.fetch(input, input)
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.to_int(input)
|
40
|
+
if input == ''
|
41
|
+
nil
|
42
|
+
else
|
43
|
+
result = input.to_i
|
44
|
+
|
45
|
+
if result === 0 && input != '0'
|
46
|
+
input
|
47
|
+
else
|
48
|
+
result
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.to_float(input)
|
54
|
+
if input == ''
|
55
|
+
nil
|
56
|
+
else
|
57
|
+
result = input.to_f
|
58
|
+
|
59
|
+
if result == 0.0 && (input != '0' || input != '0.0')
|
60
|
+
input
|
61
|
+
else
|
62
|
+
result
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.to_decimal(input)
|
68
|
+
if input == ''
|
69
|
+
nil
|
70
|
+
else
|
71
|
+
result = to_float(input)
|
72
|
+
|
73
|
+
if result.is_a?(Float)
|
74
|
+
result.to_d
|
75
|
+
else
|
76
|
+
result
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Dry
|
2
|
+
module Data
|
3
|
+
class Compiler
|
4
|
+
attr_reader :types
|
5
|
+
|
6
|
+
def initialize(types)
|
7
|
+
@types = types
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(ast)
|
11
|
+
visit(ast)
|
12
|
+
end
|
13
|
+
|
14
|
+
def visit(node, *args)
|
15
|
+
send(:"visit_#{node[0]}", node[1], *args)
|
16
|
+
end
|
17
|
+
|
18
|
+
def visit_type(node)
|
19
|
+
type, args = node
|
20
|
+
|
21
|
+
if args
|
22
|
+
send(:"visit_#{type}", args)
|
23
|
+
else
|
24
|
+
types[type]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def visit_hash(node)
|
29
|
+
constructor, schema = node
|
30
|
+
types['hash'].public_send(constructor, schema.map { |key| visit(key) }.reduce(:merge))
|
31
|
+
end
|
32
|
+
|
33
|
+
def visit_key(node)
|
34
|
+
name, type = node
|
35
|
+
{ name => type }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/dry/data/struct.rb
CHANGED
@@ -7,8 +7,7 @@ module Dry
|
|
7
7
|
|
8
8
|
def self.inherited(klass)
|
9
9
|
super
|
10
|
-
|
11
|
-
Data.register(name, Type.new(klass.method(:new), klass))
|
10
|
+
Data.register_class(klass)
|
12
11
|
end
|
13
12
|
|
14
13
|
def self.attribute(*args)
|
@@ -19,7 +18,7 @@ module Dry
|
|
19
18
|
prev_schema = schema || {}
|
20
19
|
|
21
20
|
@schema = prev_schema.merge(new_schema)
|
22
|
-
@constructor = Data['coercible.hash'].
|
21
|
+
@constructor = Data['coercible.hash'].strict(schema)
|
23
22
|
|
24
23
|
attr_reader(*(new_schema.keys - prev_schema.keys))
|
25
24
|
|
data/lib/dry/data/sum_type.rb
CHANGED
@@ -33,11 +33,15 @@ module Dry
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def call(input)
|
36
|
-
|
37
|
-
|
38
|
-
|
36
|
+
result = left[input]
|
37
|
+
|
38
|
+
if left.valid?(result)
|
39
|
+
result
|
40
|
+
else
|
39
41
|
right[input]
|
40
42
|
end
|
43
|
+
rescue TypeError
|
44
|
+
right[input]
|
41
45
|
end
|
42
46
|
alias_method :[], :call
|
43
47
|
|
data/lib/dry/data/type/hash.rb
CHANGED
@@ -2,7 +2,29 @@ module Dry
|
|
2
2
|
module Data
|
3
3
|
class Type
|
4
4
|
class Hash < Type
|
5
|
-
def self.
|
5
|
+
def self.safe_constructor(hash_constructor, value_constructors, input)
|
6
|
+
attributes = hash_constructor[input]
|
7
|
+
|
8
|
+
value_constructors.each_with_object({}) do |(key, value_constructor), result|
|
9
|
+
if attributes.key?(key)
|
10
|
+
result[key] = value_constructor[attributes[key]]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.symbolized_constructor(hash_constructor, value_constructors, input)
|
16
|
+
attributes = hash_constructor[input]
|
17
|
+
|
18
|
+
value_constructors.each_with_object({}) do |(key, value_constructor), result|
|
19
|
+
key_name = key.to_s
|
20
|
+
|
21
|
+
if attributes.key?(key_name)
|
22
|
+
result[key.to_sym] = value_constructor[attributes[key_name]]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.strict_constructor(hash_constructor, value_constructors, input)
|
6
28
|
attributes = hash_constructor[input]
|
7
29
|
|
8
30
|
value_constructors.each_with_object({}) do |(key, value_constructor), result|
|
@@ -17,13 +39,21 @@ module Dry
|
|
17
39
|
end
|
18
40
|
end
|
19
41
|
|
20
|
-
def
|
42
|
+
def strict(type_map)
|
43
|
+
schema(type_map, :strict_constructor)
|
44
|
+
end
|
45
|
+
|
46
|
+
def symbolized(type_map)
|
47
|
+
schema(type_map, :symbolized_constructor)
|
48
|
+
end
|
49
|
+
|
50
|
+
def schema(type_map, meth = :safe_constructor)
|
21
51
|
value_constructors = type_map.each_with_object({}) { |(name, type_id), result|
|
22
52
|
result[name] = Data[type_id]
|
23
53
|
}
|
24
54
|
|
25
55
|
self.class.new(
|
26
|
-
self.class.method(
|
56
|
+
self.class.method(meth).to_proc.curry.(constructor, value_constructors),
|
27
57
|
primitive
|
28
58
|
)
|
29
59
|
end
|
data/lib/dry/data/types.rb
CHANGED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'dry/data/coercions/form'
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Data
|
5
|
+
register('form.date') do
|
6
|
+
Type.new(Coercions::Form.method(:to_date), Date)
|
7
|
+
end
|
8
|
+
|
9
|
+
register('form.date_time') do
|
10
|
+
Type.new(Coercions::Form.method(:to_date_time), DateTime)
|
11
|
+
end
|
12
|
+
|
13
|
+
register('form.time') do
|
14
|
+
Type.new(Coercions::Form.method(:to_time), Time)
|
15
|
+
end
|
16
|
+
|
17
|
+
register('form.true') do
|
18
|
+
Type.new(Coercions::Form.method(:to_true), TrueClass)
|
19
|
+
end
|
20
|
+
|
21
|
+
register('form.false') do
|
22
|
+
Type.new(Coercions::Form.method(:to_true), FalseClass)
|
23
|
+
end
|
24
|
+
|
25
|
+
register('form.bool') do
|
26
|
+
self['form.true'] | self['form.false']
|
27
|
+
end
|
28
|
+
|
29
|
+
register('form.int') do
|
30
|
+
Type.new(Coercions::Form.method(:to_int), Fixnum)
|
31
|
+
end
|
32
|
+
|
33
|
+
register('form.float') do
|
34
|
+
Type.new(Coercions::Form.method(:to_float), Float)
|
35
|
+
end
|
36
|
+
|
37
|
+
register('form.decimal') do
|
38
|
+
Type.new(Coercions::Form.method(:to_decimal), BigDecimal)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
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.0
|
4
|
+
version: 0.1.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-
|
11
|
+
date: 2015-11-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dry-container
|
@@ -119,7 +119,10 @@ files:
|
|
119
119
|
- bin/console
|
120
120
|
- bin/setup
|
121
121
|
- dry-data.gemspec
|
122
|
+
- lib/dry-data.rb
|
122
123
|
- lib/dry/data.rb
|
124
|
+
- lib/dry/data/coercions/form.rb
|
125
|
+
- lib/dry/data/compiler.rb
|
123
126
|
- lib/dry/data/container.rb
|
124
127
|
- lib/dry/data/dsl.rb
|
125
128
|
- lib/dry/data/struct.rb
|
@@ -128,6 +131,7 @@ files:
|
|
128
131
|
- lib/dry/data/type/array.rb
|
129
132
|
- lib/dry/data/type/hash.rb
|
130
133
|
- lib/dry/data/types.rb
|
134
|
+
- lib/dry/data/types/form.rb
|
131
135
|
- lib/dry/data/version.rb
|
132
136
|
homepage: https://github.com/dryrb/dry-data
|
133
137
|
licenses:
|
@@ -155,3 +159,4 @@ signing_key:
|
|
155
159
|
specification_version: 4
|
156
160
|
summary: Simple type-system for Ruby
|
157
161
|
test_files: []
|
162
|
+
has_rdoc:
|