dry-data 0.2.1 → 0.3.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: 51e010763886f9dde2222d1c7846875136242d2a
4
- data.tar.gz: fcdcfcc354e6d9ffa962a03f9d7994e9f4ae4cf0
3
+ metadata.gz: 443ed733b824f1645a6e8fe1b6bc656110166920
4
+ data.tar.gz: 97fe87cf2594a29d4034f5323876ec654b069c9c
5
5
  SHA512:
6
- metadata.gz: 04c10d34579e5a5fd825a36eba44ebc4d8b5637d9e7c4387f026188b2e17a2b148c03c27c914ce2ee40bd027a2b7b7fba731b24e88a20c8e9c89679ac0827259
7
- data.tar.gz: 288618ca2bfa6b61c8e73681b1151391e388739c24fd3662607b56a18e80a5f22ad3f2544c20865fa77051bc2f494416824e0c6702bdac7d1267dfab0f556256
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
@@ -2,6 +2,8 @@ source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
4
 
5
+ gem 'dry-validation'
6
+
5
7
  group :tools do
6
8
  gem 'benchmark-ips'
7
9
  gem 'virtus'
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
- ## WIP
350
+ ## Status and Roadmap
234
351
 
235
- This is early alpha with a rough plan to:
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
- * Add constrained types (ie a string with a strict length, a number with a strict range etc.)
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.fetch(name) do
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
@@ -11,6 +11,8 @@ module Dry
11
11
 
12
12
  NON_COERCIBLE = {
13
13
  nil: NilClass,
14
+ symbol: Symbol,
15
+ class: Class,
14
16
  true: TrueClass,
15
17
  false: FalseClass,
16
18
  date: Date,
@@ -1,5 +1,5 @@
1
1
  module Dry
2
2
  module Data
3
- VERSION = '0.2.1'.freeze
3
+ VERSION = '0.3.0'.freeze
4
4
  end
5
5
  end
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.2.1
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-30 00:00:00.000000000 Z
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