value_semantics 3.2.0 → 3.6.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 +4 -4
- data/CHANGELOG.md +81 -0
- data/README.md +242 -72
- data/lib/value_semantics.rb +48 -333
- data/lib/value_semantics/anything.rb +11 -0
- data/lib/value_semantics/array_coercer.rb +23 -0
- data/lib/value_semantics/array_of.rb +18 -0
- data/lib/value_semantics/attribute.rb +100 -0
- data/lib/value_semantics/bool.rb +11 -0
- data/lib/value_semantics/class_methods.rb +34 -0
- data/lib/value_semantics/dsl.rb +106 -0
- data/lib/value_semantics/either.rb +18 -0
- data/lib/value_semantics/hash_coercer.rb +30 -0
- data/lib/value_semantics/hash_of.rb +20 -0
- data/lib/value_semantics/instance_methods.rb +170 -0
- data/lib/value_semantics/monkey_patched.rb +3 -0
- data/lib/value_semantics/range_of.rb +18 -0
- data/lib/value_semantics/recipe.rb +17 -0
- data/lib/value_semantics/struct.rb +19 -0
- data/lib/value_semantics/value_object_coercer.rb +44 -0
- data/lib/value_semantics/version.rb +1 -1
- metadata +85 -18
- data/.gitignore +0 -12
- data/.rspec +0 -2
- data/.travis.yml +0 -24
- data/Gemfile +0 -8
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/bin/test +0 -15
- data/value_semantics.gemspec +0 -34
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8d3795e585d271a6b0b784ef28931a16ae7f1846b13f7d307bf97d9d175c5384
|
4
|
+
data.tar.gz: 8289168c0be7e6cd00e24e59239a9ab8a2026fc42895007f33388d6ffa67add4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1b2520d80b112810e2b257920d591d4657cd9da26007307492cceac3321149b266bb310fba7c49e957d21a31e92bc041668bd907820b906570efa704d02c7948
|
7
|
+
data.tar.gz: 9605e0334ff2ff61dbb1925983b44f1275fb3768196bb62dbed6a2ccd9f6590b6e22ff3555101b6283181b18173657d363d397f8f10d329b9e4dea21ef1eb73c
|
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,87 @@ Notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
+
## [3.6.0] - 2020-09-01
|
9
|
+
### Added
|
10
|
+
- `RangeOf` built-in validator, for validating `Range` objects
|
11
|
+
- `HashCoercer` built-in coercer for homogeneous `Hash` objects
|
12
|
+
### Changed
|
13
|
+
- Optimised speed of value object initialization. It is now roughly 3x
|
14
|
+
slower than that of a hand-written class, which is 2-3x faster than
|
15
|
+
the previous version.
|
16
|
+
|
17
|
+
- Optimised memory allocation in object initialization. The happy path
|
18
|
+
(no exceptions raised) only allocates a single array object, under
|
19
|
+
normal circumstances. Extra allocations are likely caused by custom
|
20
|
+
validators, coercers, and default generators.
|
21
|
+
|
22
|
+
- Exceptions raised when initialising a value object are now
|
23
|
+
aggregated. Instead of telling you the problematic attributes one at
|
24
|
+
a time, you will get a list of all offending attributes in the
|
25
|
+
exception message. This applies to `MissingAttributes`,
|
26
|
+
`InvalidValue` and `UnrecognizedAttributes`. These will probably be
|
27
|
+
combined into a single exception in v4.0, so you can see all the
|
28
|
+
initialization problems at once.
|
29
|
+
|
30
|
+
- The exceptions `ValueSemantics::MissingAttributes` and
|
31
|
+
`ValueSemantics::InvalidValue` are now raised from inside
|
32
|
+
`initialize`. They were previously raised from inside of
|
33
|
+
`ValueSemantics::Attribute.determine_from!` which is an internal
|
34
|
+
implementation detail that is basically gibberish to any developer
|
35
|
+
reading it. The stack trace for this exception reads much better.
|
36
|
+
|
37
|
+
- The exception `ValueSemantics::UnrecognizedAttributes` is now raised
|
38
|
+
instead of `ValueSemantics::MissingAttributes` in the situation
|
39
|
+
where both exceptions would be raised. This makes it easier to debug
|
40
|
+
the problem where you attempt to initialize a value object using a
|
41
|
+
hash with string keys instead of symbol keys.
|
42
|
+
|
43
|
+
- The coercer returned from the `.coercer` class method is now
|
44
|
+
smarter. It handles string keys, handles objects that can be
|
45
|
+
converted to hashes.
|
46
|
+
### Deprecated
|
47
|
+
- `ValueSemantics::Attribute#determine_from!`. This was an internal
|
48
|
+
implementation detail, which is no longer used internally. Use the
|
49
|
+
`name`, `#coerce`, `#optional?`, `#default_generator` and
|
50
|
+
`#validate?` methods directly if you want to extract an attribute
|
51
|
+
from a hash.
|
52
|
+
- `ValueSemantics::NoDefaultError`. Use `Attribute#optional?` to check
|
53
|
+
whether there is a default.
|
54
|
+
|
55
|
+
|
56
|
+
|
57
|
+
## [3.5.0] - 2020-08-17
|
58
|
+
### Added
|
59
|
+
- Square bracket attr reader like `person[:name]`
|
60
|
+
- `HashOf` built-in validator, similar to `ArrayOf`
|
61
|
+
- `.coercer` class method, to help when composing value objects
|
62
|
+
- `ArrayCoercer` DSL method, to help when composing value objects
|
63
|
+
|
64
|
+
## [3.4.0] - 2020-08-01
|
65
|
+
### Added
|
66
|
+
- Value objects can be instantiated from any object that responds to `#to_h`.
|
67
|
+
Previously attributes were required to be given as a `Hash`.
|
68
|
+
|
69
|
+
- Added monkey patching for super-convenient attribute definitions. This is
|
70
|
+
**not** available by default, and needs to be explicitly enabled with
|
71
|
+
`ValueSemantics.monkey_patch!` or `require 'value_semantics/monkey_patched'`.
|
72
|
+
|
73
|
+
### Changed
|
74
|
+
- Improved exception messages for easier development experience
|
75
|
+
|
76
|
+
- Raises `ValueSemantics::InvalidValue` instead of `ArgumentError` when
|
77
|
+
attempting to initialize with an invalid value. `ValueSemantics::InvalidValue`
|
78
|
+
is a subclass of `ArgumentError`, so this change should be backward
|
79
|
+
compatible.
|
80
|
+
|
81
|
+
## [3.3.0] - 2020-07-17
|
82
|
+
### Added
|
83
|
+
- Added support for pattern matching in Ruby 2.7
|
84
|
+
|
85
|
+
## [3.2.1] - 2020-07-11
|
86
|
+
### Fixed
|
87
|
+
- Fix warnings new to Ruby 2.7 about keyword arguments
|
88
|
+
|
8
89
|
## [3.2.0] - 2019-09-30
|
9
90
|
### Added
|
10
91
|
- `ValueSemantics::Struct`, a convenience for creating a new class and mixing
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
[](https://badge.fury.io/rb/value_semantics)
|
2
2
|
[](https://travis-ci.org/tomdalling/value_semantics)
|
3
|
-

|
4
4
|
|
5
5
|
ValueSemantics
|
6
6
|
==============
|
@@ -15,10 +15,16 @@ These are intended for internal use, as opposed to validating user input like Ac
|
|
15
15
|
Invalid or missing attributes cause an exception for developers,
|
16
16
|
not an error message intended for application users.
|
17
17
|
|
18
|
-
See
|
18
|
+
See:
|
19
19
|
|
20
|
-
[announcement blog post]
|
21
|
-
[
|
20
|
+
- The [announcement blog post][blog post] for some of the rationale behind the gem
|
21
|
+
- [RubyTapas episode #584][rubytapas] for an example usage scenario
|
22
|
+
- The [API documentation](https://rubydoc.info/gems/value_semantics)
|
23
|
+
- Some [discussion on Reddit][reddit]
|
24
|
+
|
25
|
+
[blog post]: https://www.rubypigeon.com/posts/value-semantics-gem-for-making-value-classes/
|
26
|
+
[rubytapas]: https://www.rubytapas.com/2019/07/09/from-hash-to-value-object/
|
27
|
+
[reddit]: https://www.reddit.com/r/ruby/comments/akz4fs/valuesemanticsa_gem_for_making_value_classes/
|
22
28
|
|
23
29
|
|
24
30
|
Defining and Creating Value Objects
|
@@ -44,15 +50,19 @@ Person.new(name: "Tom", birthday: "2020-12-25")
|
|
44
50
|
#=> #<Person name="Tom" birthday=#<Date: 2020-12-25 ((2459209j,0s,0n),+0s,2299161j)>>
|
45
51
|
|
46
52
|
Person.new(birthday: Date.today)
|
47
|
-
#=> #<Person name="Anon Emous" birthday=#<Date:
|
53
|
+
#=> #<Person name="Anon Emous" birthday=#<Date: 2020-08-30 ((2459092j,0s,0n),+0s,2299161j)>>
|
48
54
|
|
49
55
|
Person.new(birthday: nil)
|
50
56
|
#=> #<Person name="Anon Emous" birthday=nil>
|
51
57
|
```
|
52
58
|
|
53
|
-
|
54
|
-
|
55
|
-
|
59
|
+
Value objects are typically initialized with keyword arguments or a `Hash`, but
|
60
|
+
will accept any object that responds to `#to_h`.
|
61
|
+
|
62
|
+
The curly bracket syntax used with `ValueSemantics.for_attributes` is,
|
63
|
+
unfortunately, mandatory due to Ruby's precedence rules. For a shorter
|
64
|
+
alternative method that works better with `do`/`end`, see [Convenience (Monkey
|
65
|
+
Patch)](#convenience-monkey-patch) below.
|
56
66
|
|
57
67
|
|
58
68
|
Using Value Objects
|
@@ -71,39 +81,69 @@ end
|
|
71
81
|
tom = Person.new(name: 'Tom')
|
72
82
|
|
73
83
|
|
74
|
-
#
|
75
84
|
# Read-only attributes
|
76
|
-
|
77
|
-
tom
|
78
|
-
tom.age #=> 31
|
79
|
-
|
85
|
+
tom.name #=> "Tom"
|
86
|
+
tom[:name] #=> "Tom"
|
80
87
|
|
81
|
-
#
|
82
88
|
# Convert to Hash
|
83
|
-
|
84
|
-
tom.to_h #=> { :name => "Tom", :age => 31 }
|
89
|
+
tom.to_h #=> {:name=>"Tom", :age=>31}
|
85
90
|
|
86
|
-
|
87
|
-
#
|
88
91
|
# Non-destructive updates
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
old_tom #=> #<Person name="Tom" age=99>
|
93
|
-
tom #=> #<Person name="Tom" age=31> (unchanged)
|
94
|
-
|
92
|
+
tom.with(age: 99) #=> #<Person name="Tom" age=99>
|
93
|
+
tom # (unchanged) #=> #<Person name="Tom" age=31>
|
95
94
|
|
96
|
-
#
|
97
95
|
# Equality
|
98
|
-
#
|
99
96
|
other_tom = Person.new(name: 'Tom', age: 31)
|
100
|
-
|
101
97
|
tom == other_tom #=> true
|
102
98
|
tom.eql?(other_tom) #=> true
|
103
99
|
tom.hash == other_tom.hash #=> true
|
100
|
+
|
101
|
+
# Ruby 2.7+ pattern matching
|
102
|
+
case tom
|
103
|
+
in name: "Tom", age:
|
104
|
+
puts age
|
105
|
+
end
|
106
|
+
# outputs: 31
|
104
107
|
```
|
105
108
|
|
106
109
|
|
110
|
+
Convenience (Monkey Patch)
|
111
|
+
--------------------------
|
112
|
+
|
113
|
+
There is a shorter way to define value attributes:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
require 'value_semantics/monkey_patched'
|
117
|
+
|
118
|
+
class Monkey
|
119
|
+
value_semantics do
|
120
|
+
name String
|
121
|
+
age Integer
|
122
|
+
end
|
123
|
+
end
|
124
|
+
```
|
125
|
+
|
126
|
+
**This is disabled by default**, to avoid polluting every class with an extra
|
127
|
+
class method.
|
128
|
+
|
129
|
+
This convenience method can be enabled in two ways:
|
130
|
+
|
131
|
+
1. Add a `require:` option to your `Gemfile` like this:
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
gem 'value_semantics', '~> 3.3', require: 'value_semantics/monkey_patched'
|
135
|
+
```
|
136
|
+
|
137
|
+
2. Alternatively, you can call `ValueSemantics.monkey_patch!` somewhere early
|
138
|
+
in the boot sequence of your code -- at the top of your script, for example,
|
139
|
+
or `config/boot.rb` if it's a Rails project.
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
require 'value_semantics'
|
143
|
+
ValueSemantics.monkey_patch!
|
144
|
+
```
|
145
|
+
|
146
|
+
|
107
147
|
Defaults
|
108
148
|
--------
|
109
149
|
|
@@ -119,7 +159,7 @@ class Cat
|
|
119
159
|
end
|
120
160
|
|
121
161
|
Cat.new
|
122
|
-
#=> #<Cat paws=4 born_at=
|
162
|
+
#=> #<Cat paws=4 born_at=2020-08-30 22:27:12.237812 +1000>
|
123
163
|
```
|
124
164
|
|
125
165
|
The `default` option is a single value.
|
@@ -148,15 +188,15 @@ class Person
|
|
148
188
|
}
|
149
189
|
end
|
150
190
|
|
151
|
-
Person.new(name: 'Tom',
|
152
|
-
Person.new(name: 5,
|
153
|
-
#=>
|
154
|
-
|
191
|
+
Person.new(name: 'Tom', birthday: '2000-01-01') # works
|
192
|
+
Person.new(name: 5, birthday: '2000-01-01')
|
193
|
+
#=> !!! ValueSemantics::InvalidValue: Some attributes of `Person` are invalid:
|
194
|
+
#=* - name: 5
|
155
195
|
|
156
|
-
Person.new(birthday: "1970-01-01"
|
157
|
-
Person.new(birthday: "hello"
|
158
|
-
#=>
|
159
|
-
|
196
|
+
Person.new(name: 'Tom', birthday: "1970-01-01") # works
|
197
|
+
Person.new(name: 'Tom', birthday: "hello")
|
198
|
+
#=> !!! ValueSemantics::InvalidValue: Some attributes of `Person` are invalid:
|
199
|
+
#=* - birthday: "hello"
|
160
200
|
```
|
161
201
|
|
162
202
|
|
@@ -168,13 +208,18 @@ for common situations:
|
|
168
208
|
```ruby
|
169
209
|
class LightSwitch
|
170
210
|
include ValueSemantics.for_attributes {
|
171
|
-
|
172
211
|
# Bool: only allows `true` or `false`
|
173
212
|
on? Bool()
|
174
213
|
|
175
214
|
# ArrayOf: validates elements in an array
|
176
215
|
light_ids ArrayOf(Integer)
|
177
216
|
|
217
|
+
# HashOf: validates keys/values of a homogeneous hash
|
218
|
+
toggle_stats HashOf(Symbol => Integer)
|
219
|
+
|
220
|
+
# RangeOf: validates ranges
|
221
|
+
levels RangeOf(Integer)
|
222
|
+
|
178
223
|
# Either: value must match at least one of a list of validators
|
179
224
|
color Either(Integer, String, nil)
|
180
225
|
|
@@ -186,9 +231,12 @@ end
|
|
186
231
|
LightSwitch.new(
|
187
232
|
on?: true,
|
188
233
|
light_ids: [11, 12, 13],
|
234
|
+
toggle_stats: { day: 42, night: 69 },
|
235
|
+
levels: (0..10),
|
189
236
|
color: "#FFAABB",
|
190
237
|
wierd_attr: [true, false, true, true],
|
191
238
|
)
|
239
|
+
#=> #<LightSwitch on?=true light_ids=[11, 12, 13] toggle_stats={:day=>42, :night=>69} levels=0..10 color="#FFAABB" wierd_attr=[true, false, true, true]>
|
192
240
|
```
|
193
241
|
|
194
242
|
|
@@ -197,22 +245,26 @@ LightSwitch.new(
|
|
197
245
|
A custom validator might look something like this:
|
198
246
|
|
199
247
|
```ruby
|
200
|
-
module
|
248
|
+
module DottedQuad
|
201
249
|
def self.===(value)
|
202
|
-
value.
|
250
|
+
value.split('.').all? do |part|
|
251
|
+
('0'..'255').cover?(part)
|
252
|
+
end
|
203
253
|
end
|
204
254
|
end
|
205
255
|
|
206
|
-
class
|
256
|
+
class Server
|
207
257
|
include ValueSemantics.for_attributes {
|
208
|
-
|
258
|
+
address DottedQuad
|
209
259
|
}
|
210
260
|
end
|
211
261
|
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
262
|
+
Server.new(address: '127.0.0.1')
|
263
|
+
#=> #<Server address="127.0.0.1">
|
264
|
+
|
265
|
+
Server.new(address: '127.0.0.999')
|
266
|
+
#=> !!! ValueSemantics::InvalidValue: Some attributes of `Server` are invalid:
|
267
|
+
#=* - address: "127.0.0.999"
|
216
268
|
```
|
217
269
|
|
218
270
|
Default attribute values also pass through validation.
|
@@ -224,46 +276,48 @@ Coercion
|
|
224
276
|
Coercion allows non-standard or "convenience" values to be converted into
|
225
277
|
proper, valid values, where possible.
|
226
278
|
|
227
|
-
For example, an object with an `
|
228
|
-
which are then coerced into `
|
279
|
+
For example, an object with an `Pathname` attribute may allow string values,
|
280
|
+
which are then coerced into `Pathname` objects.
|
229
281
|
|
230
282
|
Using the option `coerce: true`,
|
231
283
|
coercion happens through a custom class method called `coerce_#{attr}`,
|
232
284
|
which takes the raw value as an argument, and returns the coerced value.
|
233
285
|
|
234
286
|
```ruby
|
235
|
-
|
287
|
+
require 'pathname'
|
288
|
+
|
289
|
+
class Document
|
236
290
|
include ValueSemantics.for_attributes {
|
237
|
-
|
291
|
+
path Pathname, coerce: true
|
238
292
|
}
|
239
293
|
|
240
|
-
def self.
|
294
|
+
def self.coerce_path(value)
|
241
295
|
if value.is_a?(String)
|
242
|
-
|
296
|
+
Pathname.new(value)
|
243
297
|
else
|
244
298
|
value
|
245
299
|
end
|
246
300
|
end
|
247
301
|
end
|
248
302
|
|
249
|
-
|
250
|
-
#=> #<
|
303
|
+
Document.new(path: '~/Documents/whatever.doc')
|
304
|
+
#=> #<Document path=#<Pathname:~/Documents/whatever.doc>>
|
251
305
|
|
252
|
-
|
253
|
-
#=> #<
|
306
|
+
Document.new(path: Pathname.new('~/Documents/whatever.doc'))
|
307
|
+
#=> #<Document path=#<Pathname:~/Documents/whatever.doc>>
|
254
308
|
|
255
|
-
|
256
|
-
#=>
|
257
|
-
|
309
|
+
Document.new(path: 42)
|
310
|
+
#=> !!! ValueSemantics::InvalidValue: Some attributes of `Document` are invalid:
|
311
|
+
#=* - path: 42
|
258
312
|
```
|
259
313
|
|
260
314
|
You can also use any callable object as a coercer.
|
261
315
|
That means, you could use a lambda:
|
262
316
|
|
263
317
|
```ruby
|
264
|
-
class
|
318
|
+
class Document
|
265
319
|
include ValueSemantics.for_attributes {
|
266
|
-
|
320
|
+
path Pathname, coerce: ->(value) { Pathname.new(value) }
|
267
321
|
}
|
268
322
|
end
|
269
323
|
```
|
@@ -271,25 +325,25 @@ end
|
|
271
325
|
Or a custom class:
|
272
326
|
|
273
327
|
```ruby
|
274
|
-
class
|
328
|
+
class MyPathCoercer
|
275
329
|
def call(value)
|
276
|
-
|
330
|
+
Pathname.new(value)
|
277
331
|
end
|
278
332
|
end
|
279
333
|
|
280
|
-
class
|
334
|
+
class Document
|
281
335
|
include ValueSemantics.for_attributes {
|
282
|
-
|
336
|
+
path Pathname, coerce: MyPathCoercer.new
|
283
337
|
}
|
284
338
|
end
|
285
339
|
```
|
286
340
|
|
287
|
-
Or reuse an existing
|
341
|
+
Or reuse an existing method:
|
288
342
|
|
289
343
|
```ruby
|
290
|
-
class
|
344
|
+
class Document
|
291
345
|
include ValueSemantics.for_attributes {
|
292
|
-
|
346
|
+
path Pathname, coerce: Pathname.method(:new)
|
293
347
|
}
|
294
348
|
end
|
295
349
|
```
|
@@ -301,7 +355,85 @@ Another option is to raise an error within the coercion method.
|
|
301
355
|
|
302
356
|
Default attribute values also pass through coercion.
|
303
357
|
For example, the default value could be a string,
|
304
|
-
which would then be coerced into an `
|
358
|
+
which would then be coerced into an `Pathname` object.
|
359
|
+
|
360
|
+
|
361
|
+
## Built-in Coercers
|
362
|
+
|
363
|
+
ValueSemantics provides a few built-in coercer objects via the DSL.
|
364
|
+
|
365
|
+
```ruby
|
366
|
+
class Config
|
367
|
+
include ValueSemantics.for_attributes {
|
368
|
+
# ArrayCoercer: takes an element coercer
|
369
|
+
paths coerce: ArrayCoercer(Pathname.method(:new))
|
370
|
+
|
371
|
+
# HashCoercer: takes a key and value coercer
|
372
|
+
env coerce: HashCoercer(
|
373
|
+
keys: :to_sym.to_proc,
|
374
|
+
values: :to_i.to_proc,
|
375
|
+
)
|
376
|
+
}
|
377
|
+
end
|
378
|
+
|
379
|
+
config = Config.new(
|
380
|
+
paths: ['/a', '/b'],
|
381
|
+
env: { 'AAAA' => '1', 'BBBB' => '2' },
|
382
|
+
)
|
383
|
+
|
384
|
+
config.paths #=> [#<Pathname:/a>, #<Pathname:/b>]
|
385
|
+
config.env #=> {:AAAA=>1, :BBBB=>2}
|
386
|
+
```
|
387
|
+
|
388
|
+
|
389
|
+
## Nesting
|
390
|
+
|
391
|
+
It is fairly common to nest value objects inside each other. This
|
392
|
+
works as expected, but coercion is not automatic.
|
393
|
+
|
394
|
+
For nested coercion, use the `.coercer` class method that
|
395
|
+
ValueSemantics provides. It returns a coercer object that accepts
|
396
|
+
strings for attribute names, and will ignore attributes that the value
|
397
|
+
class does not define, instead of raising an error.
|
398
|
+
|
399
|
+
This works well in combination with `ArrayCoercer`.
|
400
|
+
|
401
|
+
```ruby
|
402
|
+
class CrabClaw
|
403
|
+
include ValueSemantics.for_attributes {
|
404
|
+
size Either(:big, :small)
|
405
|
+
}
|
406
|
+
end
|
407
|
+
|
408
|
+
class Crab
|
409
|
+
include ValueSemantics.for_attributes {
|
410
|
+
left_claw CrabClaw, coerce: CrabClaw.coercer
|
411
|
+
right_claw CrabClaw, coerce: CrabClaw.coercer
|
412
|
+
}
|
413
|
+
end
|
414
|
+
|
415
|
+
class Ocean
|
416
|
+
include ValueSemantics.for_attributes {
|
417
|
+
crabs ArrayOf(Crab), coerce: ArrayCoercer(Crab.coercer)
|
418
|
+
}
|
419
|
+
end
|
420
|
+
|
421
|
+
ocean = Ocean.new(
|
422
|
+
crabs: [
|
423
|
+
{
|
424
|
+
'left_claw' => { 'size' => :small },
|
425
|
+
'right_claw' => { 'size' => :small },
|
426
|
+
voiced_by: 'Samuel E. Wright', # this attr will be ignored
|
427
|
+
}, {
|
428
|
+
'left_claw' => { 'size' => :big },
|
429
|
+
'right_claw' => { 'size' => :big },
|
430
|
+
}
|
431
|
+
]
|
432
|
+
)
|
433
|
+
|
434
|
+
ocean.crabs.first #=> #<Crab left_claw=#<CrabClaw size=:small> right_claw=#<CrabClaw size=:small>>
|
435
|
+
ocean.crabs.first.right_claw.size #=> :small
|
436
|
+
```
|
305
437
|
|
306
438
|
|
307
439
|
## ValueSemantics::Struct
|
@@ -311,11 +443,41 @@ one step, similar to how `Struct` works from the Ruby standard library. For
|
|
311
443
|
example:
|
312
444
|
|
313
445
|
```ruby
|
314
|
-
|
315
|
-
name String, default: "
|
446
|
+
Pigeon = ValueSemantics::Struct.new do
|
447
|
+
name String, default: "Jannie"
|
448
|
+
end
|
449
|
+
|
450
|
+
Pigeon.new.name #=> "Jannie"
|
451
|
+
```
|
452
|
+
|
453
|
+
|
454
|
+
## Known Issues
|
455
|
+
|
456
|
+
Some valid attribute names result in invalid Ruby syntax when using the DSL.
|
457
|
+
In these situations, you can use the DSL method `def_attr` instead.
|
458
|
+
|
459
|
+
For example, if you want an attribute named `then`:
|
460
|
+
|
461
|
+
```ruby
|
462
|
+
# Can't do this:
|
463
|
+
class Conditional
|
464
|
+
include ValueSemantics.for_attributes {
|
465
|
+
then String
|
466
|
+
else String
|
467
|
+
}
|
316
468
|
end
|
469
|
+
#=> !!! SyntaxError: README.md:461: syntax error, unexpected `then'
|
470
|
+
#=* then String
|
471
|
+
#=* ^~~~
|
472
|
+
|
317
473
|
|
318
|
-
|
474
|
+
# This will work
|
475
|
+
class Conditional
|
476
|
+
include ValueSemantics.for_attributes {
|
477
|
+
def_attr :then, String
|
478
|
+
def_attr :else, String
|
479
|
+
}
|
480
|
+
end
|
319
481
|
```
|
320
482
|
|
321
483
|
|
@@ -341,7 +503,15 @@ Or install it yourself as:
|
|
341
503
|
Bug reports and pull requests are welcome on GitHub at:
|
342
504
|
https://github.com/tomdalling/value_semantics
|
343
505
|
|
344
|
-
Keep in mind that this gem aims to be as close to 100% backwards compatible as
|
506
|
+
Keep in mind that this gem aims to be as close to 100% backwards compatible as
|
507
|
+
possible.
|
508
|
+
|
509
|
+
I'm happy to accept PRs that:
|
510
|
+
|
511
|
+
- Improve error messages for a better developer experience, especially those
|
512
|
+
that support a TDD workflow.
|
513
|
+
- Add new and helpful built-in validators and coercers
|
514
|
+
- Implement automatic freezing of value objects (must be opt-in)
|
345
515
|
|
346
516
|
## License
|
347
517
|
|