value_semantics 3.0.1 → 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 +44 -0
- data/README.md +115 -44
- data/lib/value_semantics.rb +109 -16
- data/lib/value_semantics/monkey_patched.rb +3 -0
- data/lib/value_semantics/version.rb +1 -1
- metadata +29 -18
- data/.gitignore +0 -12
- data/.rspec +0 -2
- data/.travis.yml +0 -24
- data/Gemfile +0 -6
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/bin/test +0 -19
- 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: 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
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
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
|
+
|
|
25
|
+
## [3.3.0] - 2020-07-17
|
|
26
|
+
### Added
|
|
27
|
+
- Added support for pattern matching in Ruby 2.7
|
|
28
|
+
|
|
29
|
+
## [3.2.1] - 2020-07-11
|
|
30
|
+
### Fixed
|
|
31
|
+
- Fix warnings new to Ruby 2.7 about keyword arguments
|
|
32
|
+
|
|
33
|
+
## [3.2.0] - 2019-09-30
|
|
34
|
+
### Added
|
|
35
|
+
- `ValueSemantics::Struct`, a convenience for creating a new class and mixing
|
|
36
|
+
in ValueSemantics in a single step.
|
|
37
|
+
|
|
38
|
+
## [3.1.0] - 2019-06-30
|
|
39
|
+
### Added
|
|
40
|
+
- Built-in PP support for value classes
|
|
41
|
+
|
|
42
|
+
## [3.0.0] - 2019-01-27
|
|
43
|
+
|
|
44
|
+
First public release
|
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,6 +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:
|
|
19
|
+
|
|
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/
|
|
27
|
+
|
|
18
28
|
|
|
19
29
|
Defining and Creating Value Objects
|
|
20
30
|
-----------------------------------
|
|
@@ -45,9 +55,13 @@ Person.new(birthday: nil)
|
|
|
45
55
|
#=> #<Person name="Anon Emous" birthday=nil>
|
|
46
56
|
```
|
|
47
57
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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.
|
|
51
65
|
|
|
52
66
|
|
|
53
67
|
Using Value Objects
|
|
@@ -66,38 +80,70 @@ end
|
|
|
66
80
|
tom = Person.new(name: 'Tom')
|
|
67
81
|
|
|
68
82
|
|
|
69
|
-
#
|
|
70
83
|
# Read-only attributes
|
|
71
|
-
#
|
|
72
84
|
tom.name #=> "Tom"
|
|
73
85
|
tom.age #=> 31
|
|
74
86
|
|
|
75
87
|
|
|
76
|
-
#
|
|
77
88
|
# Convert to Hash
|
|
78
|
-
#
|
|
79
89
|
tom.to_h #=> { :name => "Tom", :age => 31 }
|
|
80
90
|
|
|
81
91
|
|
|
82
|
-
#
|
|
83
92
|
# Non-destructive updates
|
|
84
|
-
#
|
|
85
93
|
old_tom = tom.with(age: 99)
|
|
86
|
-
|
|
87
94
|
old_tom #=> #<Person name="Tom" age=99>
|
|
88
95
|
tom #=> #<Person name="Tom" age=31> (unchanged)
|
|
89
96
|
|
|
90
97
|
|
|
91
|
-
#
|
|
92
98
|
# Equality
|
|
93
|
-
#
|
|
94
99
|
other_tom = Person.new(name: 'Tom', age: 31)
|
|
95
|
-
|
|
96
100
|
tom == other_tom #=> true
|
|
97
101
|
tom.eql?(other_tom) #=> true
|
|
98
102
|
tom.hash == other_tom.hash #=> true
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# Ruby 2.7+ pattern matching
|
|
106
|
+
case tom
|
|
107
|
+
in name: "Tom", age:
|
|
108
|
+
puts age # outputs: 31
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
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
|
|
99
125
|
```
|
|
100
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
|
+
|
|
101
147
|
|
|
102
148
|
Defaults
|
|
103
149
|
--------
|
|
@@ -145,13 +191,13 @@ end
|
|
|
145
191
|
|
|
146
192
|
Person.new(name: 'Tom', ...) # works
|
|
147
193
|
Person.new(name: 5, ...)
|
|
148
|
-
#=>
|
|
149
|
-
#=>
|
|
194
|
+
#=> ValueSemantics::InvalidValue:
|
|
195
|
+
#=> Attribute `Person#name` is invalid: 5
|
|
150
196
|
|
|
151
197
|
Person.new(birthday: "1970-01-01", ...) # works
|
|
152
198
|
Person.new(birthday: "hello", ...)
|
|
153
|
-
#=>
|
|
154
|
-
#=>
|
|
199
|
+
#=> ValueSemantics::InvalidValue:
|
|
200
|
+
#=> Attribute 'Person#birthday' is invalid: "hello"
|
|
155
201
|
```
|
|
156
202
|
|
|
157
203
|
|
|
@@ -206,8 +252,8 @@ end
|
|
|
206
252
|
|
|
207
253
|
Person.new(age: 9) # works
|
|
208
254
|
Person.new(age: 8)
|
|
209
|
-
#=>
|
|
210
|
-
#=>
|
|
255
|
+
#=> ValueSemantics::InvalidValue:
|
|
256
|
+
#=> Attribute 'Person#age' is invalid: 8
|
|
211
257
|
```
|
|
212
258
|
|
|
213
259
|
Default attribute values also pass through validation.
|
|
@@ -219,46 +265,48 @@ Coercion
|
|
|
219
265
|
Coercion allows non-standard or "convenience" values to be converted into
|
|
220
266
|
proper, valid values, where possible.
|
|
221
267
|
|
|
222
|
-
For example, an object with an `
|
|
223
|
-
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.
|
|
224
270
|
|
|
225
271
|
Using the option `coerce: true`,
|
|
226
272
|
coercion happens through a custom class method called `coerce_#{attr}`,
|
|
227
273
|
which takes the raw value as an argument, and returns the coerced value.
|
|
228
274
|
|
|
229
275
|
```ruby
|
|
230
|
-
|
|
276
|
+
require 'pathname'
|
|
277
|
+
|
|
278
|
+
class Document
|
|
231
279
|
include ValueSemantics.for_attributes {
|
|
232
|
-
|
|
280
|
+
path Pathname, coerce: true
|
|
233
281
|
}
|
|
234
282
|
|
|
235
|
-
def self.
|
|
283
|
+
def self.coerce_path(value)
|
|
236
284
|
if value.is_a?(String)
|
|
237
|
-
|
|
285
|
+
Pathname.new(value)
|
|
238
286
|
else
|
|
239
287
|
value
|
|
240
288
|
end
|
|
241
289
|
end
|
|
242
290
|
end
|
|
243
291
|
|
|
244
|
-
|
|
245
|
-
#=> #<
|
|
292
|
+
Document.new(path: '~/Documents/whatever.doc')
|
|
293
|
+
#=> #<Document path=#<Pathname:~/Documents/whatever.doc>>
|
|
246
294
|
|
|
247
|
-
|
|
248
|
-
#=> #<
|
|
295
|
+
Document.new(path: Pathname.new('~/Documents/whatever.doc'))
|
|
296
|
+
#=> #<Document path=#<Pathname:~/Documents/whatever.doc>>
|
|
249
297
|
|
|
250
|
-
|
|
251
|
-
#=>
|
|
252
|
-
#=>
|
|
298
|
+
Document.new(path: 42)
|
|
299
|
+
#=> ValueSemantics::InvalidValue:
|
|
300
|
+
#=> Attribute 'Document#path' is invalid: 42
|
|
253
301
|
```
|
|
254
302
|
|
|
255
303
|
You can also use any callable object as a coercer.
|
|
256
304
|
That means, you could use a lambda:
|
|
257
305
|
|
|
258
306
|
```ruby
|
|
259
|
-
class
|
|
307
|
+
class Document
|
|
260
308
|
include ValueSemantics.for_attributes {
|
|
261
|
-
|
|
309
|
+
path Pathname, coerce: ->(value) { Pathname.new(value) }
|
|
262
310
|
}
|
|
263
311
|
end
|
|
264
312
|
```
|
|
@@ -266,25 +314,25 @@ end
|
|
|
266
314
|
Or a custom class:
|
|
267
315
|
|
|
268
316
|
```ruby
|
|
269
|
-
class
|
|
317
|
+
class MyPathCoercer
|
|
270
318
|
def call(value)
|
|
271
|
-
|
|
319
|
+
Pathname.new(value)
|
|
272
320
|
end
|
|
273
321
|
end
|
|
274
322
|
|
|
275
|
-
class
|
|
323
|
+
class Document
|
|
276
324
|
include ValueSemantics.for_attributes {
|
|
277
|
-
|
|
325
|
+
path Pathname, coerce: MyPathCoercer.new
|
|
278
326
|
}
|
|
279
327
|
end
|
|
280
328
|
```
|
|
281
329
|
|
|
282
|
-
Or reuse an existing
|
|
330
|
+
Or reuse an existing method:
|
|
283
331
|
|
|
284
332
|
```ruby
|
|
285
|
-
class
|
|
333
|
+
class Document
|
|
286
334
|
include ValueSemantics.for_attributes {
|
|
287
|
-
|
|
335
|
+
path Pathname, coerce: Pathname.method(:new)
|
|
288
336
|
}
|
|
289
337
|
end
|
|
290
338
|
```
|
|
@@ -296,7 +344,22 @@ Another option is to raise an error within the coercion method.
|
|
|
296
344
|
|
|
297
345
|
Default attribute values also pass through coercion.
|
|
298
346
|
For example, the default value could be a string,
|
|
299
|
-
which would then be coerced into an `
|
|
347
|
+
which would then be coerced into an `Pathname` object.
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
## ValueSemantics::Struct
|
|
351
|
+
|
|
352
|
+
This is a convenience for making a new class and including ValueSemantics in
|
|
353
|
+
one step, similar to how `Struct` works from the Ruby standard library. For
|
|
354
|
+
example:
|
|
355
|
+
|
|
356
|
+
```ruby
|
|
357
|
+
Cat = ValueSemantics::Struct.new do
|
|
358
|
+
name String, default: "Mittens"
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
Cat.new.name #=> "Mittens"
|
|
362
|
+
```
|
|
300
363
|
|
|
301
364
|
|
|
302
365
|
## Installation
|
|
@@ -321,7 +384,15 @@ Or install it yourself as:
|
|
|
321
384
|
Bug reports and pull requests are welcome on GitHub at:
|
|
322
385
|
https://github.com/tomdalling/value_semantics
|
|
323
386
|
|
|
324
|
-
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)
|
|
325
396
|
|
|
326
397
|
## License
|
|
327
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
|
|
107
|
+
#
|
|
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`.
|
|
68
110
|
#
|
|
69
|
-
# @
|
|
70
|
-
#
|
|
71
|
-
# @raise [MissingAttributes] if given_attrs is missing any attributes that
|
|
72
|
-
#
|
|
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`
|
|
73
117
|
#
|
|
74
|
-
def initialize(
|
|
75
|
-
|
|
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
|
|
|
@@ -139,6 +200,20 @@ module ValueSemantics
|
|
|
139
200
|
|
|
140
201
|
"#<#{self.class} #{attrs}>"
|
|
141
202
|
end
|
|
203
|
+
|
|
204
|
+
def pretty_print(pp)
|
|
205
|
+
pp.object_group(self) do
|
|
206
|
+
to_h.each do |attr, value|
|
|
207
|
+
pp.breakable
|
|
208
|
+
pp.text("#{attr}=")
|
|
209
|
+
pp.pp(value)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def deconstruct_keys(_)
|
|
215
|
+
to_h
|
|
216
|
+
end
|
|
142
217
|
end
|
|
143
218
|
|
|
144
219
|
#
|
|
@@ -169,7 +244,7 @@ module ValueSemantics
|
|
|
169
244
|
coerce: nil)
|
|
170
245
|
generator = begin
|
|
171
246
|
if default_generator && !default.equal?(NOT_SPECIFIED)
|
|
172
|
-
raise ArgumentError, "Attribute
|
|
247
|
+
raise ArgumentError, "Attribute `#{name}` can not have both a `:default` and a `:default_generator`"
|
|
173
248
|
elsif default_generator
|
|
174
249
|
default_generator
|
|
175
250
|
elsif !default.equal?(NOT_SPECIFIED)
|
|
@@ -190,7 +265,7 @@ module ValueSemantics
|
|
|
190
265
|
def determine_from!(attr_hash, klass)
|
|
191
266
|
raw_value = attr_hash.fetch(name) do
|
|
192
267
|
if default_generator.equal?(NO_DEFAULT_GENERATOR)
|
|
193
|
-
raise MissingAttributes, "
|
|
268
|
+
raise MissingAttributes, "Attribute `#{klass}\##{name}` has no value"
|
|
194
269
|
else
|
|
195
270
|
default_generator.call
|
|
196
271
|
end
|
|
@@ -201,7 +276,7 @@ module ValueSemantics
|
|
|
201
276
|
if validate?(coerced_value)
|
|
202
277
|
[name, coerced_value]
|
|
203
278
|
else
|
|
204
|
-
raise
|
|
279
|
+
raise InvalidValue, "Attribute `#{klass}\##{name}` is invalid: #{coerced_value.inspect}"
|
|
205
280
|
end
|
|
206
281
|
end
|
|
207
282
|
|
|
@@ -284,13 +359,13 @@ module ValueSemantics
|
|
|
284
359
|
ArrayOf.new(element_validator)
|
|
285
360
|
end
|
|
286
361
|
|
|
287
|
-
def def_attr(*args)
|
|
288
|
-
__attributes << Attribute.define(*args)
|
|
362
|
+
def def_attr(*args, **kwargs)
|
|
363
|
+
__attributes << Attribute.define(*args, **kwargs)
|
|
289
364
|
end
|
|
290
365
|
|
|
291
|
-
def method_missing(name, *args)
|
|
366
|
+
def method_missing(name, *args, **kwargs)
|
|
292
367
|
if respond_to_missing?(name)
|
|
293
|
-
def_attr(name, *args)
|
|
368
|
+
def_attr(name, *args, **kwargs)
|
|
294
369
|
else
|
|
295
370
|
super
|
|
296
371
|
end
|
|
@@ -356,4 +431,22 @@ module ValueSemantics
|
|
|
356
431
|
end
|
|
357
432
|
end
|
|
358
433
|
|
|
434
|
+
#
|
|
435
|
+
# ValueSemantics equivalent of the Struct class from the Ruby standard
|
|
436
|
+
# library
|
|
437
|
+
#
|
|
438
|
+
class Struct
|
|
439
|
+
#
|
|
440
|
+
# Creates a new Class with ValueSemantics mixed in
|
|
441
|
+
#
|
|
442
|
+
# @yield a block containing ValueSemantics DSL
|
|
443
|
+
# @return [Class] the newly created class
|
|
444
|
+
#
|
|
445
|
+
def self.new(&block)
|
|
446
|
+
klass = Class.new
|
|
447
|
+
klass.include(ValueSemantics.for_attributes(&block))
|
|
448
|
+
klass
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
|
|
359
452
|
end
|
metadata
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: value_semantics
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.0
|
|
4
|
+
version: 3.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tom Dalling
|
|
8
8
|
autorequire:
|
|
9
|
-
bindir:
|
|
9
|
+
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2020-08-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
|
-
- - "
|
|
17
|
+
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
19
|
version: '1.15'
|
|
20
20
|
type: :development
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
|
-
- - "
|
|
24
|
+
- - ">="
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '1.15'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
@@ -30,14 +30,28 @@ dependencies:
|
|
|
30
30
|
requirements:
|
|
31
31
|
- - "~>"
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: 3.7
|
|
33
|
+
version: '3.7'
|
|
34
34
|
type: :development
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: 3.7
|
|
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
|
|
@@ -103,22 +117,20 @@ executables: []
|
|
|
103
117
|
extensions: []
|
|
104
118
|
extra_rdoc_files: []
|
|
105
119
|
files:
|
|
106
|
-
-
|
|
107
|
-
- ".rspec"
|
|
108
|
-
- ".travis.yml"
|
|
109
|
-
- Gemfile
|
|
120
|
+
- CHANGELOG.md
|
|
110
121
|
- LICENSE.txt
|
|
111
122
|
- README.md
|
|
112
|
-
- bin/console
|
|
113
|
-
- bin/setup
|
|
114
|
-
- bin/test
|
|
115
123
|
- lib/value_semantics.rb
|
|
124
|
+
- lib/value_semantics/monkey_patched.rb
|
|
116
125
|
- lib/value_semantics/version.rb
|
|
117
|
-
- value_semantics.gemspec
|
|
118
126
|
homepage: https://github.com/tomdalling/value_semantics
|
|
119
127
|
licenses:
|
|
120
128
|
- MIT
|
|
121
|
-
metadata:
|
|
129
|
+
metadata:
|
|
130
|
+
bug_tracker_uri: https://github.com/tomdalling/value_semantics/issues
|
|
131
|
+
changelog_uri: https://github.com/tomdalling/value_semantics/blob/master/CHANGELOG.md
|
|
132
|
+
documentation_uri: https://github.com/tomdalling/value_semantics/blob/v3.4.0/README.md
|
|
133
|
+
source_code_uri: https://github.com/tomdalling/value_semantics
|
|
122
134
|
post_install_message:
|
|
123
135
|
rdoc_options: []
|
|
124
136
|
require_paths:
|
|
@@ -134,8 +146,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
134
146
|
- !ruby/object:Gem::Version
|
|
135
147
|
version: '0'
|
|
136
148
|
requirements: []
|
|
137
|
-
|
|
138
|
-
rubygems_version: 2.7.7
|
|
149
|
+
rubygems_version: 3.0.8
|
|
139
150
|
signing_key:
|
|
140
151
|
specification_version: 4
|
|
141
152
|
summary: Makes value classes, with lightweight validation and coercion.
|
data/.gitignore
DELETED
data/.rspec
DELETED
data/.travis.yml
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
language: ruby
|
|
2
|
-
script: bin/test
|
|
3
|
-
|
|
4
|
-
# test old Ruby versions WITHOUT mutation testing
|
|
5
|
-
rvm:
|
|
6
|
-
- 2.3.8
|
|
7
|
-
- 2.4.5
|
|
8
|
-
- 2.5.3
|
|
9
|
-
env: MUTATION_TEST=false
|
|
10
|
-
|
|
11
|
-
# test the latest Ruby version WITH mutation testing
|
|
12
|
-
matrix:
|
|
13
|
-
include:
|
|
14
|
-
- rvm: 2.6.0
|
|
15
|
-
env: MUTATION_TEST=true
|
|
16
|
-
|
|
17
|
-
# deploy gem on tagged commits, on the latest Ruby version only
|
|
18
|
-
deploy:
|
|
19
|
-
provider: rubygems
|
|
20
|
-
on:
|
|
21
|
-
tags: true
|
|
22
|
-
env: MUTATION_TEST=true
|
|
23
|
-
api_key:
|
|
24
|
-
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/Gemfile
DELETED
data/bin/console
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
|
|
3
|
-
require "bundler/setup"
|
|
4
|
-
require "value_semantics"
|
|
5
|
-
|
|
6
|
-
# You can add fixtures and/or initialization code here to make experimenting
|
|
7
|
-
# with your gem easier. You can also use a different console, if you like.
|
|
8
|
-
|
|
9
|
-
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
10
|
-
# require "pry"
|
|
11
|
-
# Pry.start
|
|
12
|
-
|
|
13
|
-
require "irb"
|
|
14
|
-
IRB.start(__FILE__)
|
data/bin/setup
DELETED
data/bin/test
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
set -ue
|
|
3
|
-
|
|
4
|
-
MUTANT_PATTERN=${1:-ValueSemantics*}
|
|
5
|
-
|
|
6
|
-
# if $MUTATION_TEST is false, just run RSpec
|
|
7
|
-
if [[ "${MUTATION_TEST:-true}" == "false" ]] ; then
|
|
8
|
-
bundle exec rspec
|
|
9
|
-
else
|
|
10
|
-
bundle exec mutant \
|
|
11
|
-
--include lib \
|
|
12
|
-
--require value_semantics \
|
|
13
|
-
--use rspec "$MUTANT_PATTERN" \
|
|
14
|
-
# Mutant 0.8.24 introduces new mutations that cause infinite recursion inside
|
|
15
|
-
# #method_missing. These --ignore-subject lines prevent that from happening
|
|
16
|
-
#--ignore-subject "ValueSemantics::DSL#method_missing" \
|
|
17
|
-
#--ignore-subject "ValueSemantics::DSL#respond_to_missing?" \
|
|
18
|
-
#--ignore-subject "ValueSemantics::DSL#def_attr" \
|
|
19
|
-
fi
|
data/value_semantics.gemspec
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# coding: utf-8
|
|
2
|
-
lib = File.expand_path("../lib", __FILE__)
|
|
3
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
-
require "value_semantics/version"
|
|
5
|
-
|
|
6
|
-
Gem::Specification.new do |spec|
|
|
7
|
-
spec.name = "value_semantics"
|
|
8
|
-
spec.version = ValueSemantics::VERSION
|
|
9
|
-
spec.authors = ["Tom Dalling"]
|
|
10
|
-
spec.email = [["tom", "@", "tomdalling.com"].join]
|
|
11
|
-
|
|
12
|
-
spec.summary = %q{Makes value classes, with lightweight validation and coercion.}
|
|
13
|
-
spec.description = %q{
|
|
14
|
-
Generates modules that provide conventional value semantics for a given set of attributes.
|
|
15
|
-
The behaviour is similar to an immutable `Struct` class,
|
|
16
|
-
plus extensible, lightweight validation and coercion.
|
|
17
|
-
}
|
|
18
|
-
spec.homepage = "https://github.com/tomdalling/value_semantics"
|
|
19
|
-
spec.license = "MIT"
|
|
20
|
-
|
|
21
|
-
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
|
22
|
-
f.match(%r{^(test|spec|features)/})
|
|
23
|
-
end
|
|
24
|
-
spec.bindir = "exe"
|
|
25
|
-
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
26
|
-
spec.require_paths = ["lib"]
|
|
27
|
-
|
|
28
|
-
spec.add_development_dependency "bundler", "~> 1.15"
|
|
29
|
-
spec.add_development_dependency "rspec", "~> 3.7.0"
|
|
30
|
-
spec.add_development_dependency "mutant-rspec"
|
|
31
|
-
spec.add_development_dependency "yard"
|
|
32
|
-
spec.add_development_dependency "byebug"
|
|
33
|
-
spec.add_development_dependency "gem-release"
|
|
34
|
-
end
|