value_semantics 1.0.1 → 2.0.1

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
  SHA256:
3
- metadata.gz: 0d5af9c70cace0213a309865f7fb2eaf09759d3f654d530d577d9067ee8e025d
4
- data.tar.gz: 31ef2d58e797173040691bc6ac57703751102c40519c8bcd4913f6fa24e97481
3
+ metadata.gz: 235fbd4782ce1bc943f12688408c20a5b6893e7b57882c63b725953115de74d3
4
+ data.tar.gz: 6adbdbcc36d8093fd323fb2d734557025df4b0dc9983e6ba343690a229dfed40
5
5
  SHA512:
6
- metadata.gz: 051b338a932d31e4e74a2699bd45a9a740e7fe8673f8833e566254c4be8262f90947160784db895d1706c40a8f910db21ea0999a90a3df6d65481dd60932a626
7
- data.tar.gz: 8860483b155c84402fc6964967b29ba1cef4f6a1b46409752b714d139c78625b02d0bcf184a4eb671bb2b94c4b4442938fcacec32e07f854f934e2defb87e811
6
+ metadata.gz: 179eed3a45a75b8f6546c99fd4fdcd2cb987885a4b00f36caa359a3e7b19854dd0401d3b6f3c4cd5b108256492be94d068dfa1765eb6a6844b9ccd610212cffe
7
+ data.tar.gz: c9eefd7b84e63507513c2d3b88469730c346899a2a7ae109bc1159f2ed764d71102a7dbbd9e7ae0ea9d2caad1ee82c1c6b740c69eb508caa50571687d0f51c75
data/README.md CHANGED
@@ -166,13 +166,14 @@ proper, valid values, where possible.
166
166
  For example, an object with an `IPAddr` attribute may allow string values,
167
167
  which are then coerced into `IPAddr` objects.
168
168
 
169
- To implement coercion, define a class method called `coerce_#{attr}` which
170
- accepts a raw value, and returns the coerced value.
169
+ Using the option `coerce: true`,
170
+ coercion happens through a custom class method called `coerce_#{attr}`,
171
+ which takes the raw value as an argument, and returns the coerced value.
171
172
 
172
173
  ```ruby
173
174
  class Server
174
175
  include ValueSemantics.for_attributes {
175
- address IPAddr
176
+ address IPAddr, coerce: true
176
177
  }
177
178
 
178
179
  def self.coerce_address(value)
@@ -195,12 +196,51 @@ Server.new(address: 42)
195
196
  #=> Value for attribute 'address' is not valid: 42
196
197
  ```
197
198
 
198
- If coercion is not possible, you can return the value unchanged,
199
- allowing the validator to fail.
200
- Another option is to raise an error within the coercion method.
199
+ You can also use any callable object as a coercer.
200
+ That means, you could use a lambda:
201
+
202
+ ```ruby
203
+ class Server
204
+ include ValueSemantics.for_attributes {
205
+ address IPAddr, coerce: ->(value) { IPAddr.new(value) }
206
+ }
207
+ end
208
+ ```
209
+
210
+ Or a custom class:
211
+
212
+ ```ruby
213
+ class MyAddressCoercer
214
+ def call(value)
215
+ IPAddr.new(value)
216
+ end
217
+ end
218
+
219
+ class Server
220
+ include ValueSemantics.for_attributes {
221
+ address IPAddr, coerce: MyAddressCoercer.new
222
+ }
223
+ end
224
+ ```
225
+
226
+ Or reuse an existing class method:
227
+
228
+ ```ruby
229
+ class Server
230
+ include ValueSemantics.for_attributes {
231
+ address IPAddr, coerce: IPAddr.method(:new)
232
+ }
233
+ end
234
+ ```
201
235
 
202
236
  Coercion happens before validation.
237
+ If coercion is not possible, coercers can return the raw value unchanged,
238
+ allowing the validator to fail with a nice, descriptive exception.
239
+ Another option is to raise an error within the coercion method.
240
+
203
241
  Default attribute values also pass through coercion.
242
+ For example, the default value could be a string,
243
+ which would then be coerced into an `IPAddr` object.
204
244
 
205
245
 
206
246
  ## All Together
@@ -209,7 +249,7 @@ Default attribute values also pass through coercion.
209
249
  class Person
210
250
  include ValueSemantics.for_attributes {
211
251
  name String, default: "Anon Emous"
212
- birthday Either(Date, nil)
252
+ birthday Either(Date, nil), coerce: true
213
253
  }
214
254
 
215
255
  def self.coerce_birthday(value)
@@ -231,7 +271,6 @@ Person.new(birthday: nil)
231
271
  #=> #<Person name="Anon Emous" birthday=nil>
232
272
  ```
233
273
 
234
-
235
274
  ## Installation
236
275
 
237
276
  Add this line to your application's Gemfile:
@@ -249,6 +288,10 @@ Or install it yourself as:
249
288
  $ gem install value_semantics
250
289
 
251
290
 
291
+ ## TODO
292
+
293
+ - Allow defaults to be generated by calling a method (e.g. to get the current time)
294
+
252
295
  ## Contributing
253
296
 
254
297
  Bug reports and pull requests are welcome on GitHub at:
@@ -75,19 +75,21 @@ module ValueSemantics
75
75
  end
76
76
 
77
77
  class Attribute
78
- attr_reader :name, :has_default, :default_value, :validator
78
+ NOT_SPECIFIED = Object.new.freeze
79
79
 
80
- def initialize(name:, has_default:, default_value:, validator:)
80
+ attr_reader :name, :validator, :coercer
81
+
82
+ def initialize(name:, default_value:, validator:, coercer:)
81
83
  @name = name.to_sym
82
- @has_default = has_default
83
84
  @default_value = default_value
84
85
  @validator = validator
86
+ @coercer = coercer
85
87
  freeze
86
88
  end
87
89
 
88
90
  def determine_from!(attr_hash, klass)
89
91
  raw_value = attr_hash.fetch(name) do
90
- if has_default
92
+ if default_specified?
91
93
  default_value
92
94
  else
93
95
  raise ArgumentError, "Value missing for attribute '#{name}'"
@@ -104,15 +106,21 @@ module ValueSemantics
104
106
  end
105
107
 
106
108
  def coerce(attr_value, klass)
107
- if klass.respond_to?(coercion_method)
109
+ return attr_value unless coercer # coercion not enabled
110
+
111
+ if coercer.equal?(true)
108
112
  klass.public_send(coercion_method, attr_value)
109
113
  else
110
- attr_value
114
+ coercer.call(attr_value)
111
115
  end
112
116
  end
113
117
 
118
+ def default_specified?
119
+ !@default_value.equal?(NOT_SPECIFIED)
120
+ end
121
+
114
122
  def default_value
115
- if has_default
123
+ if default_specified?
116
124
  @default_value
117
125
  else
118
126
  fail "Attribute does not have a default value"
@@ -133,8 +141,6 @@ module ValueSemantics
133
141
  end
134
142
 
135
143
  class DSL
136
- NOT_SPECIFIED = Object.new
137
-
138
144
  def self.run(&block)
139
145
  dsl = new
140
146
  dsl.instance_eval(&block)
@@ -163,12 +169,12 @@ module ValueSemantics
163
169
  ArrayOf.new(element_validator)
164
170
  end
165
171
 
166
- def def_attr(attr_name, validator=Anything, default: NOT_SPECIFIED)
172
+ def def_attr(attr_name, validator=Anything, default: Attribute::NOT_SPECIFIED, coerce: nil)
167
173
  __attributes << Attribute.new(
168
174
  name: attr_name,
169
- has_default: !(NOT_SPECIFIED.equal?(default)),
170
- default_value: default,
171
175
  validator: validator,
176
+ default_value: default,
177
+ coercer: coerce
172
178
  )
173
179
  end
174
180
 
@@ -1,3 +1,3 @@
1
1
  module ValueSemantics
2
- VERSION = "1.0.1"
2
+ VERSION = "2.0.1"
3
3
  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: 1.0.1
4
+ version: 2.0.1
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-09-09 00:00:00.000000000 Z
11
+ date: 2018-10-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler