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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 706c2f7f4ad11b612b658585553e3bb850b53b76
4
- data.tar.gz: a93749b4327258e7e1a76893b4e4e9f6526b9611
3
+ metadata.gz: 63d84521c44133ae20569f13cfe37198d145b84b
4
+ data.tar.gz: 34a54445feeb95d3caffec626f3cca303eaa3455
5
5
  SHA512:
6
- metadata.gz: 37ceb712ef8b4c3e62791694460d0f1a010ff0e33ea8c9db971bf99455fdf1434501f5c2003be3b8b1856ee5a138f2d78a717ff07117278b0722f5de9e055f8e
7
- data.tar.gz: 752bdf83d32e5909bd183d582486b7132d19e58c5d2a50898286246c20a045476cdbfc63cddc5e324dc716f6c0a13677b3e5a352daaa50159c49ae9d10d7e9dd
6
+ metadata.gz: c4ed0e1ca95bdb0e7d07dd744480da21d0316d96a43dc9e52e35963edbbc45f4eca370b4116a5ef4db51f6c0dda8e68b8f0b836ab1235fd7b2850ecfaa4f4558
7
+ data.tar.gz: 87f09d1e60fbfef3a96750561ef81e354ced647d6e4371d02e49fd204e6229684b50e194f7ed3c6cf8c8d312e1d329b60de0f876f5a5cfca1fed9ffafb92cfe7
@@ -9,7 +9,7 @@ rvm:
9
9
  - 2.1
10
10
  - 2.2
11
11
  - rbx-2
12
- - jruby
12
+ - jruby-9000
13
13
  - ruby-head
14
14
  - jruby-head
15
15
  env:
@@ -1,3 +1,16 @@
1
- v0.0.1 2015-10-05
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
- Primary usage of this library is defining domain data types that your application
43
- will work with. The interface consists of lower-level type definitions and a higher-level
44
- virtus-like interface for defining structs.
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
- ### Accessing built-in types
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
- More types will be added soon.
61
+ Form-coercible types:
68
62
 
69
- Types are grouped under 4 categories:
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
- - default: pass-through without any checks
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"
@@ -0,0 +1 @@
1
+ require 'dry/data'
@@ -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
@@ -7,8 +7,7 @@ module Dry
7
7
 
8
8
  def self.inherited(klass)
9
9
  super
10
- name = Inflecto.underscore(klass).gsub('/', '.')
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'].schema(schema)
21
+ @constructor = Data['coercible.hash'].strict(schema)
23
22
 
24
23
  attr_reader(*(new_schema.keys - prev_schema.keys))
25
24
 
@@ -33,11 +33,15 @@ module Dry
33
33
  end
34
34
 
35
35
  def call(input)
36
- begin
37
- left[input]
38
- rescue TypeError
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
 
@@ -2,7 +2,29 @@ module Dry
2
2
  module Data
3
3
  class Type
4
4
  class Hash < Type
5
- def self.constructor(hash_constructor, value_constructors, input)
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 schema(type_map)
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(:constructor).to_proc.curry.(constructor, value_constructors),
56
+ self.class.method(meth).to_proc.curry.(constructor, value_constructors),
27
57
  primitive
28
58
  )
29
59
  end
@@ -59,3 +59,5 @@ module Dry
59
59
  register("strict.bool", self["strict.true"] | self["strict.false"])
60
60
  end
61
61
  end
62
+
63
+ require 'dry/data/types/form'
@@ -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
@@ -1,5 +1,5 @@
1
1
  module Dry
2
2
  module Data
3
- VERSION = '0.0.1'.freeze
3
+ VERSION = '0.1.0'.freeze
4
4
  end
5
5
  end
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.1
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-10-05 00:00:00.000000000 Z
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: