dry-data 0.2.1 → 0.3.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/CHANGELOG.md +12 -0
- data/Gemfile +2 -0
- data/README.md +121 -4
- data/dry-data.gemspec +2 -0
- data/lib/dry/data.rb +31 -2
- data/lib/dry/data/constraints.rb +20 -0
- data/lib/dry/data/type.rb +30 -1
- data/lib/dry/data/type/enum.rb +27 -0
- data/lib/dry/data/types.rb +2 -0
- data/lib/dry/data/version.rb +1 -1
- metadata +32 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 443ed733b824f1645a6e8fe1b6bc656110166920
|
4
|
+
data.tar.gz: 97fe87cf2594a29d4034f5323876ec654b069c9c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 49bef55bd41e2075c3448658bc4ac79ea62c3deb3dd3a6a79fb3d30142ed389f39559beaa31352b7bc966995042093e6ec1553c7434e85d0a3e3e983d123b644
|
7
|
+
data.tar.gz: a4cf5f5e5f0c4d6510def73f6a4e65a6e3f59624254ee946442cf0f91943c7746224835e3cfe62b934f52043e95f1e7b85e91c3ad8f601da2e5e4003973677fd
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
# v0.3.0 2015-12-09
|
2
|
+
|
3
|
+
## Added
|
4
|
+
|
5
|
+
* `Type#constrained` interface for defining constrained types (solnic)
|
6
|
+
* `Dry::Data` can be configured with a type namespace (solnic)
|
7
|
+
* `Dry::Data.finalize` can be used to define types as constants under configured namespace (solnic)
|
8
|
+
* `Dry::Data::Type#enum` for defining an enum from a specific type (solnic)
|
9
|
+
* New types: `symbol` and `class` along with their `strict` versions (solnic)
|
10
|
+
|
11
|
+
[Compare v0.2.1...v0.3.0](https://github.com/dryrb/dry-data/compare/v0.2.1...v0.3.0)
|
12
|
+
|
1
13
|
# v0.2.1 2015-11-30
|
2
14
|
|
3
15
|
## Added
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -51,6 +51,8 @@ Built-in types are grouped under 5 categories:
|
|
51
51
|
Non-coercible:
|
52
52
|
|
53
53
|
- `nil`
|
54
|
+
- `symbol`
|
55
|
+
- `class`
|
54
56
|
- `true`
|
55
57
|
- `false`
|
56
58
|
- `date`
|
@@ -162,6 +164,121 @@ maybe_string['something'].fmap(&:upcase).value
|
|
162
164
|
# => "SOMETHING"
|
163
165
|
```
|
164
166
|
|
167
|
+
### Constrained Types
|
168
|
+
|
169
|
+
You can create constrained types that will use validation rules to check if the
|
170
|
+
input is not violating any of the configured contraints. You can treat it as
|
171
|
+
a lower level guarantee that you're not instantiating objects that are broken.
|
172
|
+
|
173
|
+
All types support constraints API, but not all constraints are suitable for a
|
174
|
+
particular primitive, it's up to you to set up constraints that make sense.
|
175
|
+
|
176
|
+
Under the hood it uses `dry-validation`[https://github.com/dryrb/dry-validation]
|
177
|
+
and all of its predicates are supported.
|
178
|
+
|
179
|
+
IMPORTANT: `dry-data` does not have a runtime dependency on `dry-validation` so
|
180
|
+
if you want to use contrained types you need to add it to your Gemfile
|
181
|
+
|
182
|
+
``` ruby
|
183
|
+
string = Dry::Data["strict.string"].constrained(min_size: 3)
|
184
|
+
|
185
|
+
string['foo']
|
186
|
+
# => "foo"
|
187
|
+
|
188
|
+
string['fo']
|
189
|
+
# => Dry::Data::ConstraintError: "fo" violates constraints
|
190
|
+
|
191
|
+
email = Dry::Data['strict.string'].constrained(
|
192
|
+
format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i
|
193
|
+
)
|
194
|
+
|
195
|
+
email["jane@doe.org"]
|
196
|
+
# => "jane@doe.org"
|
197
|
+
|
198
|
+
email["jane"]
|
199
|
+
# => Dry::Data::ConstraintError: "fo" violates constraints
|
200
|
+
```
|
201
|
+
|
202
|
+
### Setting Type Constants
|
203
|
+
|
204
|
+
Types can be stored as easily accessible constants in a configured namespace:
|
205
|
+
|
206
|
+
``` ruby
|
207
|
+
module Types; end
|
208
|
+
|
209
|
+
Dry::Data.configure do |config|
|
210
|
+
config.namespace = Types
|
211
|
+
end
|
212
|
+
|
213
|
+
# after defining your custom types (if you've got any) you can finalize setup
|
214
|
+
Dry::Data.finalize
|
215
|
+
|
216
|
+
# this defines all types under your namespace
|
217
|
+
Types::Coercible::String
|
218
|
+
# => #<Dry::Data::Type:0x007feffb104aa8 @constructor=#<Method: Kernel.String>, @primitive=String>
|
219
|
+
```
|
220
|
+
|
221
|
+
With types accessible as constants you can easily compose more complex types,
|
222
|
+
like sum-types or constrained types, in hash schemas or structs:
|
223
|
+
|
224
|
+
``` ruby
|
225
|
+
Dry::Data.configure do |config|
|
226
|
+
config.namespace = Types
|
227
|
+
end
|
228
|
+
|
229
|
+
Dry::Data.finalize
|
230
|
+
|
231
|
+
module Types
|
232
|
+
Email = String.constrained(format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i)
|
233
|
+
Age = Int.constrained(gt: 18)
|
234
|
+
end
|
235
|
+
|
236
|
+
class User < Dry::Data::Struct
|
237
|
+
attribute :name, Types::String
|
238
|
+
attribute :email, Types::Email
|
239
|
+
attribute :age, Types::Age
|
240
|
+
end
|
241
|
+
```
|
242
|
+
|
243
|
+
### Defining Enums
|
244
|
+
|
245
|
+
In many cases you may want to define an enum. For example in a blog application
|
246
|
+
a post may have a finite list of statuses. Apart from accessing the current status
|
247
|
+
value it is useful to have all possible values accessible too. Furthermore an
|
248
|
+
enum is a `int => value` map, so you can store integers somewhere and have them
|
249
|
+
mapped to enum values conveniently.
|
250
|
+
|
251
|
+
You can define enums for every type but it probably only makes sense for `string`:
|
252
|
+
|
253
|
+
``` ruby
|
254
|
+
# assuming we have types loaded into `Types` namespace
|
255
|
+
# we can easily define an enum for our post struct
|
256
|
+
class Post < Dry::Data::Struct
|
257
|
+
Statuses = Types::Strict::String.enum('draft', 'published', 'archived')
|
258
|
+
|
259
|
+
attribute :title, Types::Strict::String
|
260
|
+
attribute :body, Types::Strict::String
|
261
|
+
attribute :status, Statuses
|
262
|
+
end
|
263
|
+
|
264
|
+
# enum values are frozen, let's be paranoid, doesn't hurt and have potential to
|
265
|
+
# eliminate silly bugs
|
266
|
+
Post::Statuses.values.frozen? # => true
|
267
|
+
Post::Statuses.values.all?(&:frozen?) # => true
|
268
|
+
|
269
|
+
# you can access values using indices or actual values
|
270
|
+
Post::Statuses[0] # => "draft"
|
271
|
+
Post::Statuses['draft'] # => "draft"
|
272
|
+
|
273
|
+
# it'll raise if something silly was passed in
|
274
|
+
Post::Statuses['something silly']
|
275
|
+
# => Dry::Data::ConstraintError: "something silly" violates constraints
|
276
|
+
|
277
|
+
# nil is considered as something silly too
|
278
|
+
Post::Statuses[nil]
|
279
|
+
# => Dry::Data::ConstraintError: nil violates constraints
|
280
|
+
```
|
281
|
+
|
165
282
|
### Defining a hash with explicit schema
|
166
283
|
|
167
284
|
The built-in hash type has constructors that you can use to define hashes with
|
@@ -230,12 +347,12 @@ user.name # => Some("Jane")
|
|
230
347
|
user.age # => 21
|
231
348
|
```
|
232
349
|
|
233
|
-
##
|
350
|
+
## Status and Roadmap
|
234
351
|
|
235
|
-
This is early
|
352
|
+
This library is in an early stage of development but you are encauraged to try it
|
353
|
+
out and provide feedback.
|
236
354
|
|
237
|
-
|
238
|
-
* Benchmark against other libs and make sure it's fast enough
|
355
|
+
For planned features check out [the issues](https://github.com/dryrb/dry-data/labels/feature).
|
239
356
|
|
240
357
|
## Development
|
241
358
|
|
data/dry-data.gemspec
CHANGED
@@ -27,7 +27,9 @@ Gem::Specification.new do |spec|
|
|
27
27
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
28
28
|
spec.require_paths = ["lib"]
|
29
29
|
|
30
|
+
spec.add_runtime_dependency 'thread_safe', '~> 0.3'
|
30
31
|
spec.add_runtime_dependency 'dry-container', '~> 0.2'
|
32
|
+
spec.add_runtime_dependency 'dry-configurable', '~> 0.1'
|
31
33
|
spec.add_runtime_dependency 'inflecto', '~> 0.0.0', '>= 0.0.2'
|
32
34
|
spec.add_runtime_dependency 'kleisli', '~> 0.2'
|
33
35
|
|
data/lib/dry/data.rb
CHANGED
@@ -4,6 +4,7 @@ require 'set'
|
|
4
4
|
|
5
5
|
require 'dry-container'
|
6
6
|
require 'inflecto'
|
7
|
+
require 'thread_safe/cache'
|
7
8
|
|
8
9
|
require 'dry/data/version'
|
9
10
|
require 'dry/data/container'
|
@@ -13,6 +14,10 @@ require 'dry/data/dsl'
|
|
13
14
|
|
14
15
|
module Dry
|
15
16
|
module Data
|
17
|
+
extend Dry::Configurable
|
18
|
+
|
19
|
+
setting :namespace, self
|
20
|
+
|
16
21
|
class SchemaError < TypeError
|
17
22
|
def initialize(key, value)
|
18
23
|
super("#{value.inspect} (#{value.class}) has invalid type for :#{key}")
|
@@ -26,9 +31,14 @@ module Dry
|
|
26
31
|
end
|
27
32
|
|
28
33
|
StructError = Class.new(TypeError)
|
34
|
+
ConstraintError = Class.new(TypeError)
|
29
35
|
|
30
36
|
TYPE_SPEC_REGEX = %r[(.+)<(.+)>].freeze
|
31
37
|
|
38
|
+
def self.finalize
|
39
|
+
define_constants(config.namespace, container._container.keys)
|
40
|
+
end
|
41
|
+
|
32
42
|
def self.container
|
33
43
|
@container ||= Container.new
|
34
44
|
end
|
@@ -45,7 +55,7 @@ module Dry
|
|
45
55
|
end
|
46
56
|
|
47
57
|
def self.[](name)
|
48
|
-
type_map.
|
58
|
+
type_map.fetch_or_store(name) do
|
49
59
|
result = name.match(TYPE_SPEC_REGEX)
|
50
60
|
|
51
61
|
type =
|
@@ -60,13 +70,32 @@ module Dry
|
|
60
70
|
end
|
61
71
|
end
|
62
72
|
|
73
|
+
def self.define_constants(namespace, identifiers)
|
74
|
+
names = identifiers.map do |id|
|
75
|
+
parts = id.split('.')
|
76
|
+
[Inflecto.camelize(parts.pop), parts.map(&Inflecto.method(:camelize))]
|
77
|
+
end
|
78
|
+
|
79
|
+
names.map do |(klass, parts)|
|
80
|
+
mod = parts.reduce(namespace) do |a, e|
|
81
|
+
a.constants.include?(e.to_sym) ? a.const_get(e) : a.const_set(e, Module.new)
|
82
|
+
end
|
83
|
+
|
84
|
+
mod.const_set(klass, self[identifier((parts + [klass]).join('::'))])
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.identifier(klass)
|
89
|
+
Inflecto.underscore(klass).gsub('/', '.')
|
90
|
+
end
|
91
|
+
|
63
92
|
def self.type(*args, &block)
|
64
93
|
dsl = DSL.new(container)
|
65
94
|
block ? yield(dsl) : registry[args.first]
|
66
95
|
end
|
67
96
|
|
68
97
|
def self.type_map
|
69
|
-
@type_map ||=
|
98
|
+
@type_map ||= ThreadSafe::Cache.new
|
70
99
|
end
|
71
100
|
end
|
72
101
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'dry-equalizer' # FIXME: this should not be needed
|
2
|
+
|
3
|
+
require 'dry/validation/rule_compiler'
|
4
|
+
require 'dry/validation/predicates'
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module Data
|
8
|
+
def self.Rule(primitive, options)
|
9
|
+
rule_compiler.(
|
10
|
+
options.map { |key, val|
|
11
|
+
[:val, [primitive, [:predicate, [:"#{key}?", [val]]]]]
|
12
|
+
}
|
13
|
+
).reduce(:and)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.rule_compiler
|
17
|
+
@rule_compiler ||= Validation::RuleCompiler.new(Validation::Predicates)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/dry/data/type.rb
CHANGED
@@ -1,16 +1,37 @@
|
|
1
1
|
require 'dry/data/type/optional'
|
2
2
|
require 'dry/data/type/hash'
|
3
3
|
require 'dry/data/type/array'
|
4
|
+
require 'dry/data/type/enum'
|
4
5
|
|
5
6
|
require 'dry/data/sum_type'
|
7
|
+
require 'dry/data/constraints'
|
6
8
|
|
7
9
|
module Dry
|
8
10
|
module Data
|
9
11
|
class Type
|
10
12
|
attr_reader :constructor
|
11
|
-
|
12
13
|
attr_reader :primitive
|
13
14
|
|
15
|
+
class Constrained < Type
|
16
|
+
attr_reader :rule
|
17
|
+
|
18
|
+
def initialize(constructor, primitive, rule)
|
19
|
+
super(constructor, primitive)
|
20
|
+
@rule = rule
|
21
|
+
end
|
22
|
+
|
23
|
+
def call(input)
|
24
|
+
result = super(input)
|
25
|
+
|
26
|
+
if rule.(result).success?
|
27
|
+
result
|
28
|
+
else
|
29
|
+
raise ConstraintError, "#{input.inspect} violates constraints"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
alias_method :[], :call
|
33
|
+
end
|
34
|
+
|
14
35
|
def self.[](primitive)
|
15
36
|
if primitive == ::Array
|
16
37
|
Type::Array
|
@@ -38,6 +59,14 @@ module Dry
|
|
38
59
|
@primitive = primitive
|
39
60
|
end
|
40
61
|
|
62
|
+
def constrained(options)
|
63
|
+
Constrained.new(constructor, primitive, Data.Rule(primitive, options))
|
64
|
+
end
|
65
|
+
|
66
|
+
def enum(*values)
|
67
|
+
Enum.new(values, constrained(inclusion: values))
|
68
|
+
end
|
69
|
+
|
41
70
|
def name
|
42
71
|
primitive.name
|
43
72
|
end
|
@@ -0,0 +1,27 @@
|
|
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
|
data/lib/dry/data/types.rb
CHANGED
data/lib/dry/data/version.rb
CHANGED
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dry-data
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.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-12-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: thread_safe
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.3'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.3'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: dry-container
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -24,6 +38,20 @@ dependencies:
|
|
24
38
|
- - "~>"
|
25
39
|
- !ruby/object:Gem::Version
|
26
40
|
version: '0.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: dry-configurable
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.1'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.1'
|
27
55
|
- !ruby/object:Gem::Dependency
|
28
56
|
name: inflecto
|
29
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -123,12 +151,14 @@ files:
|
|
123
151
|
- lib/dry/data.rb
|
124
152
|
- lib/dry/data/coercions/form.rb
|
125
153
|
- lib/dry/data/compiler.rb
|
154
|
+
- lib/dry/data/constraints.rb
|
126
155
|
- lib/dry/data/container.rb
|
127
156
|
- lib/dry/data/dsl.rb
|
128
157
|
- lib/dry/data/struct.rb
|
129
158
|
- lib/dry/data/sum_type.rb
|
130
159
|
- lib/dry/data/type.rb
|
131
160
|
- lib/dry/data/type/array.rb
|
161
|
+
- lib/dry/data/type/enum.rb
|
132
162
|
- lib/dry/data/type/hash.rb
|
133
163
|
- lib/dry/data/type/optional.rb
|
134
164
|
- lib/dry/data/types.rb
|