dry-data 0.0.1 → 0.1.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: 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: