value_semantics 3.2.0 → 3.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Gem Version](https://badge.fury.io/rb/value_semantics.svg)](https://badge.fury.io/rb/value_semantics)
|
2
2
|
[![Build Status](https://travis-ci.org/tomdalling/value_semantics.svg?branch=master)](https://travis-ci.org/tomdalling/value_semantics)
|
3
|
-
![Mutation Coverage](https://img.shields.io/badge/mutation%20coverage-
|
3
|
+
![Mutation Coverage](https://img.shields.io/badge/mutation%20coverage-to%20the%20MAX-brightgreen.svg)
|
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
|
|