dry-data 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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:
|