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 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