value_semantics 3.3.0 → 3.4.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 +17 -0
- data/README.md +91 -49
- data/lib/value_semantics.rb +73 -12
- data/lib/value_semantics/monkey_patched.rb +3 -0
- data/lib/value_semantics/version.rb +1 -1
- metadata +19 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e86c0e4467ff36d89870545718de723b0a0b62ff21dc50d30a3f6e1c0766c4c1
|
4
|
+
data.tar.gz: '011313548dfe90ee7b686bc91338b5e0f403ce2dd26f98fd607fdf16ef2c5ce7'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ecd428749dd01e806df9bbd4a361084f1b066298d464fea3a04ff7f66214ba224b99b93c7902f513c1c4f89fc52690779f4a14bb2c7e78106a212456a8f2c14d
|
7
|
+
data.tar.gz: 86c31d6565594493891581dde7feac4f9a9a5c1ff39b86874fd1be74d6bf3827b9a3311369dcaf9a76d9c41ca03be9abc464736ac86d09d316125e9472534554
|
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,23 @@ 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.4.0] - 2020-08-01
|
9
|
+
### Added
|
10
|
+
- Value objects can be instantiated from any object that responds to `#to_h`.
|
11
|
+
Previously attributes were required to be given as a `Hash`.
|
12
|
+
|
13
|
+
- Added monkey patching for super-convenient attribute definitions. This is
|
14
|
+
**not** available by default, and needs to be explicitly enabled with
|
15
|
+
`ValueSemantics.monkey_patch!` or `require 'value_semantics/monkey_patched'`.
|
16
|
+
|
17
|
+
### Changed
|
18
|
+
- Improved exception messages for easier development experience
|
19
|
+
|
20
|
+
- Raises `ValueSemantics::InvalidValue` instead of `ArgumentError` when
|
21
|
+
attempting to initialize with an invalid value. `ValueSemantics::InvalidValue`
|
22
|
+
is a subclass of `ArgumentError`, so this change should be backward
|
23
|
+
compatible.
|
24
|
+
|
8
25
|
## [3.3.0] - 2020-07-17
|
9
26
|
### Added
|
10
27
|
- Added support for pattern matching in Ruby 2.7
|
data/README.md
CHANGED
@@ -15,10 +15,15 @@ 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
|
+
- Some [discussion on Reddit][reddit]
|
23
|
+
|
24
|
+
[blog post]: https://www.rubypigeon.com/posts/value-semantics-gem-for-making-value-classes/
|
25
|
+
[rubytapas]: https://www.rubytapas.com/2019/07/09/from-hash-to-value-object/
|
26
|
+
[reddit]: https://www.reddit.com/r/ruby/comments/akz4fs/valuesemanticsa_gem_for_making_value_classes/
|
22
27
|
|
23
28
|
|
24
29
|
Defining and Creating Value Objects
|
@@ -50,9 +55,13 @@ Person.new(birthday: nil)
|
|
50
55
|
#=> #<Person name="Anon Emous" birthday=nil>
|
51
56
|
```
|
52
57
|
|
53
|
-
|
54
|
-
|
55
|
-
|
58
|
+
Value objects are typically initialized with keyword arguments or a `Hash`, but
|
59
|
+
will accept any object that responds to `#to_h`.
|
60
|
+
|
61
|
+
The curly bracket syntax used with `ValueSemantics.for_attributes` is,
|
62
|
+
unfortunately, mandatory due to Ruby's precedence rules. For a shorter
|
63
|
+
alternative method that works better with `do`/`end`, see [Convenience (Monkey
|
64
|
+
Patch)](#convenience-monkey-patch) below.
|
56
65
|
|
57
66
|
|
58
67
|
Using Value Objects
|
@@ -71,48 +80,71 @@ end
|
|
71
80
|
tom = Person.new(name: 'Tom')
|
72
81
|
|
73
82
|
|
74
|
-
#
|
75
83
|
# Read-only attributes
|
76
|
-
#
|
77
84
|
tom.name #=> "Tom"
|
78
85
|
tom.age #=> 31
|
79
86
|
|
80
87
|
|
81
|
-
#
|
82
88
|
# Convert to Hash
|
83
|
-
#
|
84
89
|
tom.to_h #=> { :name => "Tom", :age => 31 }
|
85
90
|
|
86
91
|
|
87
|
-
#
|
88
92
|
# Non-destructive updates
|
89
|
-
#
|
90
93
|
old_tom = tom.with(age: 99)
|
91
|
-
|
92
94
|
old_tom #=> #<Person name="Tom" age=99>
|
93
95
|
tom #=> #<Person name="Tom" age=31> (unchanged)
|
94
96
|
|
95
97
|
|
96
|
-
#
|
97
98
|
# Equality
|
98
|
-
#
|
99
99
|
other_tom = Person.new(name: 'Tom', age: 31)
|
100
|
-
|
101
100
|
tom == other_tom #=> true
|
102
101
|
tom.eql?(other_tom) #=> true
|
103
102
|
tom.hash == other_tom.hash #=> true
|
104
103
|
|
105
104
|
|
106
|
-
#
|
107
105
|
# Ruby 2.7+ pattern matching
|
108
|
-
#
|
109
106
|
case tom
|
110
|
-
in
|
107
|
+
in name: "Tom", age:
|
111
108
|
puts age # outputs: 31
|
112
109
|
end
|
113
110
|
```
|
114
111
|
|
115
112
|
|
113
|
+
Convenience (Monkey Patch)
|
114
|
+
--------------------------
|
115
|
+
|
116
|
+
There is a shorter way to define value attributes:
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
class Person
|
120
|
+
value_semantics do
|
121
|
+
name String
|
122
|
+
age Integer
|
123
|
+
end
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
**This is disabled by default**, to avoid polluting every class with an extra
|
128
|
+
class method.
|
129
|
+
|
130
|
+
This convenience method can be enabled in two ways:
|
131
|
+
|
132
|
+
1. Add a `require:` option to your `Gemfile` like this:
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
gem 'value_semantics', '~> 3.3', require: 'value_semantics/monkey_patched'
|
136
|
+
```
|
137
|
+
|
138
|
+
2. Alternatively, you can call `ValueSemantics.monkey_patch!` somewhere early
|
139
|
+
in the boot sequence of your code -- at the top of your script, for example,
|
140
|
+
or `config/boot.rb` if it's a Rails project.
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
require 'value_semantics'
|
144
|
+
ValueSemantics.monkey_patch!
|
145
|
+
```
|
146
|
+
|
147
|
+
|
116
148
|
Defaults
|
117
149
|
--------
|
118
150
|
|
@@ -159,13 +191,13 @@ end
|
|
159
191
|
|
160
192
|
Person.new(name: 'Tom', ...) # works
|
161
193
|
Person.new(name: 5, ...)
|
162
|
-
#=>
|
163
|
-
#=>
|
194
|
+
#=> ValueSemantics::InvalidValue:
|
195
|
+
#=> Attribute `Person#name` is invalid: 5
|
164
196
|
|
165
197
|
Person.new(birthday: "1970-01-01", ...) # works
|
166
198
|
Person.new(birthday: "hello", ...)
|
167
|
-
#=>
|
168
|
-
#=>
|
199
|
+
#=> ValueSemantics::InvalidValue:
|
200
|
+
#=> Attribute 'Person#birthday' is invalid: "hello"
|
169
201
|
```
|
170
202
|
|
171
203
|
|
@@ -220,8 +252,8 @@ end
|
|
220
252
|
|
221
253
|
Person.new(age: 9) # works
|
222
254
|
Person.new(age: 8)
|
223
|
-
#=>
|
224
|
-
#=>
|
255
|
+
#=> ValueSemantics::InvalidValue:
|
256
|
+
#=> Attribute 'Person#age' is invalid: 8
|
225
257
|
```
|
226
258
|
|
227
259
|
Default attribute values also pass through validation.
|
@@ -233,46 +265,48 @@ Coercion
|
|
233
265
|
Coercion allows non-standard or "convenience" values to be converted into
|
234
266
|
proper, valid values, where possible.
|
235
267
|
|
236
|
-
For example, an object with an `
|
237
|
-
which are then coerced into `
|
268
|
+
For example, an object with an `Pathname` attribute may allow string values,
|
269
|
+
which are then coerced into `Pathname` objects.
|
238
270
|
|
239
271
|
Using the option `coerce: true`,
|
240
272
|
coercion happens through a custom class method called `coerce_#{attr}`,
|
241
273
|
which takes the raw value as an argument, and returns the coerced value.
|
242
274
|
|
243
275
|
```ruby
|
244
|
-
|
276
|
+
require 'pathname'
|
277
|
+
|
278
|
+
class Document
|
245
279
|
include ValueSemantics.for_attributes {
|
246
|
-
|
280
|
+
path Pathname, coerce: true
|
247
281
|
}
|
248
282
|
|
249
|
-
def self.
|
283
|
+
def self.coerce_path(value)
|
250
284
|
if value.is_a?(String)
|
251
|
-
|
285
|
+
Pathname.new(value)
|
252
286
|
else
|
253
287
|
value
|
254
288
|
end
|
255
289
|
end
|
256
290
|
end
|
257
291
|
|
258
|
-
|
259
|
-
#=> #<
|
292
|
+
Document.new(path: '~/Documents/whatever.doc')
|
293
|
+
#=> #<Document path=#<Pathname:~/Documents/whatever.doc>>
|
260
294
|
|
261
|
-
|
262
|
-
#=> #<
|
295
|
+
Document.new(path: Pathname.new('~/Documents/whatever.doc'))
|
296
|
+
#=> #<Document path=#<Pathname:~/Documents/whatever.doc>>
|
263
297
|
|
264
|
-
|
265
|
-
#=>
|
266
|
-
#=>
|
298
|
+
Document.new(path: 42)
|
299
|
+
#=> ValueSemantics::InvalidValue:
|
300
|
+
#=> Attribute 'Document#path' is invalid: 42
|
267
301
|
```
|
268
302
|
|
269
303
|
You can also use any callable object as a coercer.
|
270
304
|
That means, you could use a lambda:
|
271
305
|
|
272
306
|
```ruby
|
273
|
-
class
|
307
|
+
class Document
|
274
308
|
include ValueSemantics.for_attributes {
|
275
|
-
|
309
|
+
path Pathname, coerce: ->(value) { Pathname.new(value) }
|
276
310
|
}
|
277
311
|
end
|
278
312
|
```
|
@@ -280,25 +314,25 @@ end
|
|
280
314
|
Or a custom class:
|
281
315
|
|
282
316
|
```ruby
|
283
|
-
class
|
317
|
+
class MyPathCoercer
|
284
318
|
def call(value)
|
285
|
-
|
319
|
+
Pathname.new(value)
|
286
320
|
end
|
287
321
|
end
|
288
322
|
|
289
|
-
class
|
323
|
+
class Document
|
290
324
|
include ValueSemantics.for_attributes {
|
291
|
-
|
325
|
+
path Pathname, coerce: MyPathCoercer.new
|
292
326
|
}
|
293
327
|
end
|
294
328
|
```
|
295
329
|
|
296
|
-
Or reuse an existing
|
330
|
+
Or reuse an existing method:
|
297
331
|
|
298
332
|
```ruby
|
299
|
-
class
|
333
|
+
class Document
|
300
334
|
include ValueSemantics.for_attributes {
|
301
|
-
|
335
|
+
path Pathname, coerce: Pathname.method(:new)
|
302
336
|
}
|
303
337
|
end
|
304
338
|
```
|
@@ -310,7 +344,7 @@ Another option is to raise an error within the coercion method.
|
|
310
344
|
|
311
345
|
Default attribute values also pass through coercion.
|
312
346
|
For example, the default value could be a string,
|
313
|
-
which would then be coerced into an `
|
347
|
+
which would then be coerced into an `Pathname` object.
|
314
348
|
|
315
349
|
|
316
350
|
## ValueSemantics::Struct
|
@@ -350,7 +384,15 @@ Or install it yourself as:
|
|
350
384
|
Bug reports and pull requests are welcome on GitHub at:
|
351
385
|
https://github.com/tomdalling/value_semantics
|
352
386
|
|
353
|
-
Keep in mind that this gem aims to be as close to 100% backwards compatible as
|
387
|
+
Keep in mind that this gem aims to be as close to 100% backwards compatible as
|
388
|
+
possible.
|
389
|
+
|
390
|
+
I'm happy to accept PRs that:
|
391
|
+
|
392
|
+
- Improve error messages for a better developer experience, especially those
|
393
|
+
that support a TDD workflow.
|
394
|
+
- Add new, helpful validators
|
395
|
+
- Implement automatic freezing of value objects (must be opt-in)
|
354
396
|
|
355
397
|
## License
|
356
398
|
|
data/lib/value_semantics.rb
CHANGED
@@ -3,6 +3,7 @@ module ValueSemantics
|
|
3
3
|
class UnrecognizedAttributes < Error; end
|
4
4
|
class NoDefaultValue < Error; end
|
5
5
|
class MissingAttributes < Error; end
|
6
|
+
class InvalidValue < ArgumentError; end
|
6
7
|
|
7
8
|
NOT_SPECIFIED = Object.new.freeze
|
8
9
|
|
@@ -43,6 +44,39 @@ module ValueSemantics
|
|
43
44
|
end
|
44
45
|
end
|
45
46
|
|
47
|
+
#
|
48
|
+
# Makes the `.value_semantics` convenience method available to all classes
|
49
|
+
#
|
50
|
+
# `.value_semantics` is a shortcut for `include ValueSemantics.for_attributes`.
|
51
|
+
# Instead of:
|
52
|
+
#
|
53
|
+
# class Person
|
54
|
+
# include ValueSemantics.for_attributes {
|
55
|
+
# name String
|
56
|
+
# }
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# You can just write:
|
60
|
+
#
|
61
|
+
# class Person
|
62
|
+
# value_semantics do
|
63
|
+
# name String
|
64
|
+
# end
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# Alternatively, you can `require 'value_semantics/monkey_patched'`, which
|
68
|
+
# will call this method automatically.
|
69
|
+
#
|
70
|
+
def self.monkey_patch!
|
71
|
+
Class.class_eval do
|
72
|
+
# @!visibility private
|
73
|
+
def value_semantics(&block)
|
74
|
+
include ValueSemantics.for_attributes(&block)
|
75
|
+
end
|
76
|
+
private :value_semantics
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
46
80
|
#
|
47
81
|
# All the class methods available on ValueSemantics classes
|
48
82
|
#
|
@@ -55,6 +89,11 @@ module ValueSemantics
|
|
55
89
|
# was included into this class.
|
56
90
|
#
|
57
91
|
def value_semantics
|
92
|
+
if block_given?
|
93
|
+
# caller is trying to use the monkey-patched Class method
|
94
|
+
raise "`#{self}` has already included ValueSemantics"
|
95
|
+
end
|
96
|
+
|
58
97
|
self::VALUE_SEMANTICS_RECIPE__
|
59
98
|
end
|
60
99
|
end
|
@@ -64,15 +103,31 @@ module ValueSemantics
|
|
64
103
|
#
|
65
104
|
module InstanceMethods
|
66
105
|
#
|
67
|
-
# Creates a value object based on a
|
106
|
+
# Creates a value object based on a hash of attributes
|
68
107
|
#
|
69
|
-
# @param
|
70
|
-
#
|
71
|
-
# @raise [MissingAttributes] if given_attrs is missing any attributes that do not have defaults
|
72
|
-
# @raise [ArgumentError] if any attribute values do no pass their validators
|
108
|
+
# @param attributes [#to_h] A hash of attribute values by name. Typically a
|
109
|
+
# `Hash`, but can be any object that responds to `#to_h`.
|
73
110
|
#
|
74
|
-
|
75
|
-
|
111
|
+
# @raise [UnrecognizedAttributes] if given_attrs contains keys that are not
|
112
|
+
# attributes
|
113
|
+
# @raise [MissingAttributes] if given_attrs is missing any attributes that
|
114
|
+
# do not have defaults
|
115
|
+
# @raise [InvalidValue] if any attribute values do no pass their validators
|
116
|
+
# @raise [TypeError] if the argument does not respond to `#to_h`
|
117
|
+
#
|
118
|
+
def initialize(attributes = nil)
|
119
|
+
attributes_hash =
|
120
|
+
if attributes.respond_to?(:to_h)
|
121
|
+
attributes.to_h
|
122
|
+
else
|
123
|
+
raise TypeError, <<-END_MESSAGE.strip.gsub(/\s+/, ' ')
|
124
|
+
Can not initialize a `#{self.class}` with a `#{attributes.class}`
|
125
|
+
object. This argument is typically a `Hash` of attributes, but can
|
126
|
+
be any object that responds to `#to_h`.
|
127
|
+
END_MESSAGE
|
128
|
+
end
|
129
|
+
|
130
|
+
remaining_attrs = attributes_hash.dup
|
76
131
|
|
77
132
|
self.class.value_semantics.attributes.each do |attr|
|
78
133
|
key, value = attr.determine_from!(remaining_attrs, self.class)
|
@@ -81,8 +136,14 @@ module ValueSemantics
|
|
81
136
|
end
|
82
137
|
|
83
138
|
unless remaining_attrs.empty?
|
84
|
-
|
85
|
-
|
139
|
+
raise(
|
140
|
+
UnrecognizedAttributes,
|
141
|
+
"`#{self.class}` does not define attributes: " +
|
142
|
+
remaining_attrs
|
143
|
+
.keys
|
144
|
+
.map { |k| '`' + k.inspect + '`' }
|
145
|
+
.join(', ')
|
146
|
+
)
|
86
147
|
end
|
87
148
|
end
|
88
149
|
|
@@ -183,7 +244,7 @@ module ValueSemantics
|
|
183
244
|
coerce: nil)
|
184
245
|
generator = begin
|
185
246
|
if default_generator && !default.equal?(NOT_SPECIFIED)
|
186
|
-
raise ArgumentError, "Attribute
|
247
|
+
raise ArgumentError, "Attribute `#{name}` can not have both a `:default` and a `:default_generator`"
|
187
248
|
elsif default_generator
|
188
249
|
default_generator
|
189
250
|
elsif !default.equal?(NOT_SPECIFIED)
|
@@ -204,7 +265,7 @@ module ValueSemantics
|
|
204
265
|
def determine_from!(attr_hash, klass)
|
205
266
|
raw_value = attr_hash.fetch(name) do
|
206
267
|
if default_generator.equal?(NO_DEFAULT_GENERATOR)
|
207
|
-
raise MissingAttributes, "
|
268
|
+
raise MissingAttributes, "Attribute `#{klass}\##{name}` has no value"
|
208
269
|
else
|
209
270
|
default_generator.call
|
210
271
|
end
|
@@ -215,7 +276,7 @@ module ValueSemantics
|
|
215
276
|
if validate?(coerced_value)
|
216
277
|
[name, coerced_value]
|
217
278
|
else
|
218
|
-
raise
|
279
|
+
raise InvalidValue, "Attribute `#{klass}\##{name}` is invalid: #{coerced_value.inspect}"
|
219
280
|
end
|
220
281
|
end
|
221
282
|
|
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: 3.
|
4
|
+
version: 3.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tom Dalling
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-08-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '3.7'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: super_diff
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: mutant-rspec
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -107,6 +121,7 @@ files:
|
|
107
121
|
- LICENSE.txt
|
108
122
|
- README.md
|
109
123
|
- lib/value_semantics.rb
|
124
|
+
- lib/value_semantics/monkey_patched.rb
|
110
125
|
- lib/value_semantics/version.rb
|
111
126
|
homepage: https://github.com/tomdalling/value_semantics
|
112
127
|
licenses:
|
@@ -114,7 +129,7 @@ licenses:
|
|
114
129
|
metadata:
|
115
130
|
bug_tracker_uri: https://github.com/tomdalling/value_semantics/issues
|
116
131
|
changelog_uri: https://github.com/tomdalling/value_semantics/blob/master/CHANGELOG.md
|
117
|
-
documentation_uri: https://github.com/tomdalling/value_semantics/blob/v3.
|
132
|
+
documentation_uri: https://github.com/tomdalling/value_semantics/blob/v3.4.0/README.md
|
118
133
|
source_code_uri: https://github.com/tomdalling/value_semantics
|
119
134
|
post_install_message:
|
120
135
|
rdoc_options: []
|
@@ -131,8 +146,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
131
146
|
- !ruby/object:Gem::Version
|
132
147
|
version: '0'
|
133
148
|
requirements: []
|
134
|
-
|
135
|
-
rubygems_version: 2.7.7
|
149
|
+
rubygems_version: 3.0.8
|
136
150
|
signing_key:
|
137
151
|
specification_version: 4
|
138
152
|
summary: Makes value classes, with lightweight validation and coercion.
|