value_semantics 0.1.1 → 1.0.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
  SHA256:
3
- metadata.gz: 17f5d6eaef0f928cf467ac69b5dc9d4f0dfcc09a6991ad460e44f394c5b94d87
4
- data.tar.gz: c85d1e46558e4599e52df186a5b45f47193cf88246c4267c76b46f5f959f9f55
3
+ metadata.gz: 36400a694002cc8c879dfbad099d365e2674edf63ad258794094e2bb41f894da
4
+ data.tar.gz: f8f67ee234971124bcb65e487d59ed3126c8f51406b1bc4cc9d93eb1d70a5262
5
5
  SHA512:
6
- metadata.gz: 3cd1bfb5150cbb4fef561da3d433dde03d4a4432253f697c94790ac6e30832a1901bc5989f8a48e062f55a9a80f5c9b19cdf99b20b16ac3704e872ebd4851057
7
- data.tar.gz: bd16975818cb48f84cf07472a4723ffe6b04a74036d52f6a7e3c3fe8a5d04b4ef6270a6ea7d2a25ff137b56d3a3905392706ea91825134b7c7630a1c985fc53b
6
+ metadata.gz: 9d8becb2999466ceb24b0c9e6139ab089a64f019304c8803e6412e6a4818965dac59c601fc65f950c7e29cd5f4e27895a37e677baac6054e554e4fe749f2b5a6
7
+ data.tar.gz: 67d37d2f3fe4f07a6c54fdec2546e3a2b2e12ee92b1aa69a914382fee08ad9b0a2bfc00948e38f1b17b1608a6247763e507e255640add73c0ea0a3c5d6bac9b0
data/.travis.yml CHANGED
@@ -1,10 +1,13 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.4.1
3
+ - 2.3.7
4
+ - 2.4.4
5
+ - 2.5.1
4
6
  script: bundle exec rspec
5
7
  deploy:
6
8
  provider: rubygems
7
9
  on:
8
10
  tags: true
11
+ rvm: 2.5.1
9
12
  api_key:
10
13
  secure: nL74QuUczEpA0qbhSBN2zjGdviWgKB3wR6vFvwervv1MZNWmwOQUYe99Oq9kPeyc8/x2MR/H6PQm5qbrk/WAfRede01WxlZ/EBUW+9CYGrxcBsGONx9IULO8A0I8/yN/YJHW2vjo3dfR66EwVsXTVWq8U63PRRcwJIyTqnIiUm2sxauMQoPRBbXG+pD9v/EJSn3ugpdtxp0lVYDn8LDKk5Ho4/wbpY4ML11XUJa9mz9CyR/GsAzdy5FTXaDMOwuWOVEx9cab7m4qPOBhmlJY4TrmooFpxTxRwChcvByjq1IboEd2M3RT5on7Q/xDTlHSOuT0OS8mnS2AocGT4a1gC+W/xOlghgEcN+xs2V5mfucR6+iUYlCy32uz1w3ey7T2X5xN4ubut09r1xLi7eu1NisAoAc+GOJ4TIxQNqkeRhY4X/fs8j7SMfOEMDr6pPxSLKZxgSvExt+IbdcZD/uQ7rTBQkadYCbc9MX5dHazBievmar3ZsFffbIf+n13FVDXsaPgRt7DlFM5dqGrEwVwt1jFRhdFuDCjkj4QWOLn7E1uY3XqgrqGvgUBlF8Znwc6qicW8zxV4SIWhqIzCOH6L9WIZGLHNq0remoCd9sq9Ter9av3jL+6UmZRRAr+JceeZfZmsYIXKomECzleM9FXMx7FXlpjJKOlf3JnrfeCTwI=
data/README.md CHANGED
@@ -1,4 +1,7 @@
1
- # ValueSemantics
1
+ [![Build Status](https://travis-ci.org/tomdalling/value_semantics.svg?branch=master)](https://travis-ci.org/tomdalling/value_semantics)
2
+
3
+ ValueSemantics
4
+ ==============
2
5
 
3
6
  Create value classes quickly, with all the [conventions of a good value object](https://github.com/zverok/good-value-object).
4
7
 
@@ -10,7 +13,9 @@ These are intended for internal use, as opposed to validating user input like Ac
10
13
  Invalid or missing attributes cause an exception intended for developers,
11
14
  not an error message intended for the user.
12
15
 
13
- ## Basic Usage
16
+
17
+ Basic Usage
18
+ -----------
14
19
 
15
20
  ```ruby
16
21
  require 'value_semantics'
@@ -62,10 +67,15 @@ mandatory due to Ruby's precedence rules.
62
67
  The `do`/`end` syntax will not work unless you surround the whole thing with parenthesis.
63
68
 
64
69
 
65
- ## Validation (Types)
70
+ Validation (Types)
71
+ ------------------
72
+
73
+ Each attribute may optionally have a validator, to check that values are correct.
66
74
 
67
75
  Validators are objects that implement the `===` method,
68
- which means you can use `Class` objects (like `String`) and also `Regexp` objects:
76
+ which means you can use `Class` objects (like `String`),
77
+ and also things like regular expressions.
78
+ Anything that you can use in a `case`/`when` expression will work.
69
79
 
70
80
  ```ruby
71
81
  class Person
@@ -86,6 +96,41 @@ Person.new(birthday: "hello", ...)
86
96
  #=> Value for attribute 'birthday' is not valid: "hello"
87
97
  ```
88
98
 
99
+
100
+ ### Built-in Validators
101
+
102
+ The ValueSemantics DSL comes with a small number of built-in validators,
103
+ for common situations:
104
+
105
+ ```ruby
106
+ class LightSwitch
107
+ include ValueSemantics.for_attributes {
108
+
109
+ # Boolean: only allows `true` or `false`
110
+ on? Boolean()
111
+
112
+ # ArrayOf: validates elements in an array
113
+ light_ids ArrayOf(Integer)
114
+
115
+ # Either: value must match at least one of a list of validators
116
+ color Either(Integer, String, nil)
117
+
118
+ # these validators are composable
119
+ wierd_attr Either(Boolean(), ArrayOf(Boolean()))
120
+ }
121
+ end
122
+
123
+ LightSwitch.new(
124
+ on?: true,
125
+ light_ids: [11, 12, 13],
126
+ color: "#FFAABB",
127
+ wierd_attr: [true, false, true, true],
128
+ )
129
+ ```
130
+
131
+
132
+ ### Custom Validators
133
+
89
134
  A custom validator might look something like this:
90
135
 
91
136
  ```ruby
@@ -110,70 +155,78 @@ Person.new(age: 8)
110
155
  Default attribute values also pass through validation.
111
156
 
112
157
 
113
- ## Coercion
158
+ Coercion
159
+ --------
160
+
161
+ Coercion allows non-standard or "convenience" values to be converted into
162
+ proper, valid values, where possible.
163
+
164
+ For example, an object with an `IPAddr` attribute may allow string values,
165
+ which are then coerced into `IPAddr` objects.
114
166
 
115
- Coercion blocks can convert invalid values into valid ones, where possible.
167
+ To implement coercion, define a class method called `coerce_#{attr}` which
168
+ accepts a raw value, and returns the coerced value.
116
169
 
117
170
  ```ruby
118
171
  class Server
119
172
  include ValueSemantics.for_attributes {
120
- address IPAddr do |value|
121
- if value.is_a?(String)
122
- IPAddr.new(value)
123
- else
124
- value
125
- end
126
- end
173
+ address IPAddr
127
174
  }
175
+
176
+ def self.coerce_address(value)
177
+ if value.is_a?(String)
178
+ IPAddr.new(value)
179
+ else
180
+ value
181
+ end
182
+ end
128
183
  end
129
184
 
130
- Server.new(address: '127.0.0.1') # works
131
- Server.new(address: IPAddr.new('127.0.0.1')) # works
185
+ Server.new(address: '127.0.0.1')
186
+ #=> #<Server address=#<IPAddr: IPv4:127.0.0.1/255.255.255.255>>
187
+
188
+ Server.new(address: IPAddr.new('127.0.0.1'))
189
+ #=> #<Server address=#<IPAddr: IPv4:127.0.0.1/255.255.255.255>>
190
+
132
191
  Server.new(address: 42)
133
192
  #=> ArgumentError:
134
193
  #=> Value for attribute 'address' is not valid: 42
135
194
  ```
136
195
 
137
- If coercion is not possible, the value is to returned unchanged, allowing the validator to fail.
138
- Another option is to raise an error within the coercion block.
196
+ If coercion is not possible, you can return the value unchanged,
197
+ allowing the validator to fail.
198
+ Another option is to raise an error within the coercion method.
139
199
 
140
200
  Coercion happens before validation.
141
201
  Default attribute values also pass through coercion.
142
202
 
143
- The coercion block runs in the context of the value object,
144
- so you can call methods from the value object.
145
- For example:
146
203
 
147
- ```
148
- class Server
204
+ ## All Together
205
+
206
+ ```ruby
207
+ class Person
149
208
  include ValueSemantics.for_attributes {
150
- address IPAddr do |value|
151
- coerce_address(value)
152
- end
209
+ name String, default: "Anon Emous"
210
+ birthday Either(Date, nil)
153
211
  }
154
212
 
155
- def coerce_address(value)
213
+ def self.coerce_birthday(value)
156
214
  if value.is_a?(String)
157
- IPAddr.new(value)
215
+ Date.parse(value)
158
216
  else
159
217
  value
160
218
  end
161
219
  end
162
220
  end
163
- ```
164
221
 
165
- ## All Together
222
+ Person.new(name: "Tom", birthday: "2020-12-25")
223
+ #=> #<Person name="Tom" birthday=#<Date: 2020-12-25 ((2459209j,0s,0n),+0s,2299161j)>>
166
224
 
167
- ```ruby
168
- class Coordinate
169
- include ValueSemantics.for_attributes {
170
- latitude Float, default: 0 { |value| value.to_f }
171
- longitude Float, default: 0 { |value| value.to_f }
172
- }
173
- end
225
+ Person.new(birthday: Date.today)
226
+ #=> #<Person name="Anon Emous" birthday=#<Date: 2018-09-04 ((2458366j,0s,0n),+0s,2299161j)>>
174
227
 
175
- Coordinate.new(longitude: "123")
176
- #=> #<Coordinate latitude=0.0 longitude=123.0>
228
+ Person.new(birthday: nil)
229
+ #=> #<Person name="Anon Emous" birthday=nil>
177
230
  ```
178
231
 
179
232
 
@@ -1,3 +1,3 @@
1
1
  module ValueSemantics
2
- VERSION = "0.1.1"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -32,7 +32,7 @@ module ValueSemantics
32
32
  remaining_attrs = given_attrs.dup
33
33
 
34
34
  self.class.attributes.each do |attr|
35
- key, value = attr.determine_from!(remaining_attrs, self)
35
+ key, value = attr.determine_from!(remaining_attrs, self.class)
36
36
  instance_variable_set(attr.instance_variable, value)
37
37
  remaining_attrs.delete(key)
38
38
  end
@@ -75,18 +75,17 @@ module ValueSemantics
75
75
  end
76
76
 
77
77
  class Attribute
78
- attr_reader :name, :has_default, :default_value, :coercer
78
+ attr_reader :name, :has_default, :default_value
79
79
 
80
- def initialize(name:, has_default:, default_value:, validator:, coercer:)
80
+ def initialize(name:, has_default:, default_value:, validator:)
81
81
  @name = name.to_sym
82
82
  @has_default = has_default
83
83
  @default_value = default_value
84
84
  @validator = validator
85
- @coercer = coercer
86
85
  freeze
87
86
  end
88
87
 
89
- def determine_from!(attr_hash, value_object)
88
+ def determine_from!(attr_hash, klass)
90
89
  raw_value = attr_hash.fetch(name) do
91
90
  if has_default
92
91
  default_value
@@ -95,7 +94,7 @@ module ValueSemantics
95
94
  end
96
95
  end
97
96
 
98
- coerced_value = value_object.instance_exec(raw_value, &coercer)
97
+ coerced_value = coerce(raw_value, klass)
99
98
 
100
99
  if validate?(coerced_value)
101
100
  [name, coerced_value]
@@ -104,6 +103,14 @@ module ValueSemantics
104
103
  end
105
104
  end
106
105
 
106
+ def coerce(attr_value, klass)
107
+ if klass.respond_to?(coercion_method)
108
+ klass.public_send(coercion_method, attr_value)
109
+ else
110
+ attr_value
111
+ end
112
+ end
113
+
107
114
  def default_value
108
115
  if has_default
109
116
  @default_value
@@ -119,6 +126,10 @@ module ValueSemantics
119
126
  def instance_variable
120
127
  '@' + name.to_s.chomp('!').chomp('?')
121
128
  end
129
+
130
+ def coercion_method
131
+ "coerce_#{name}"
132
+ end
122
133
  end
123
134
 
124
135
  class DSL
@@ -136,29 +147,83 @@ module ValueSemantics
136
147
  @__attributes = []
137
148
  end
138
149
 
139
- def method_missing(attr_name, validator=AnythingValidator,
140
- default: NOT_SPECIFIED, &coercion_block)
150
+ def Boolean
151
+ Boolean
152
+ end
153
+
154
+ def Either(*subvalidators)
155
+ Either.new(subvalidators)
156
+ end
157
+
158
+ def Anything
159
+ Anything
160
+ end
141
161
 
162
+ def ArrayOf(element_validator)
163
+ ArrayOf.new(element_validator)
164
+ end
165
+
166
+ def declare_attribute(attr_name, validator=Anything, default: NOT_SPECIFIED)
142
167
  __attributes << Attribute.new(
143
168
  name: attr_name,
144
169
  has_default: default != NOT_SPECIFIED,
145
170
  default_value: default,
146
171
  validator: validator,
147
- coercer: coercion_block || IdentityCoercer,
148
172
  )
149
173
  end
150
174
 
175
+ def method_missing(name, *args, &block)
176
+ if respond_to_missing?(name)
177
+ declare_attribute(name, *args, &block)
178
+ else
179
+ super
180
+ end
181
+ end
182
+
151
183
  def respond_to_missing?(method_name, include_private = false)
152
- true
184
+ first_letter = method_name.to_s[0]
185
+ (first_letter == first_letter.downcase) || super
153
186
  end
154
187
  end
155
188
 
156
- module AnythingValidator
189
+ module Boolean
190
+ extend self
191
+
192
+ def ===(value)
193
+ true.eql?(value) || false.eql?(value)
194
+ end
195
+ end
196
+
197
+ module Anything
157
198
  def self.===(value)
158
199
  true
159
200
  end
160
201
  end
161
202
 
162
- IdentityCoercer = ->(value) { value }
203
+ class Either
204
+ attr_reader :subvalidators
205
+
206
+ def initialize(subvalidators)
207
+ @subvalidators = subvalidators
208
+ freeze
209
+ end
210
+
211
+ def ===(value)
212
+ subvalidators.any? { |sv| sv === value }
213
+ end
214
+ end
215
+
216
+ class ArrayOf
217
+ attr_reader :element_validator
218
+
219
+ def initialize(element_validator)
220
+ @element_validator = element_validator
221
+ freeze
222
+ end
223
+
224
+ def ===(value)
225
+ Array === value && value.all? { |element| element_validator === element }
226
+ end
227
+ end
163
228
 
164
229
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: value_semantics
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tom Dalling
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-08-22 00:00:00.000000000 Z
11
+ date: 2018-09-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler