wholeable 0.0.0 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/README.adoc +206 -10
- data/lib/wholeable/builder.rb +46 -22
- data/lib/wholeable.rb +1 -1
- data/wholeable.gemspec +1 -1
- data.tar.gz.sig +0 -0
- metadata +2 -2
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 40664a3b6be45a6f4451814d7640f0af87855b7ddb347403664e2ab7932e339f
|
4
|
+
data.tar.gz: c67f512e1fd98157d4eb09c2fe6b53ae5b8336db543082c71649a773740f3999
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 86bcedb5107abbd424d76f282b98bb981093abda67b025d7ba10f8737c1964b0fd4380b5ee55881c1b2a95f107311347ffc247307cbe42fbb582328a1159452a
|
7
|
+
data.tar.gz: 9ca644f01317034aa2311ecdb1a68f993edc2d09914217f4ba811222e9382f21cc8b4074c3f47c81f4788ca4cfc20eb16a607f290b623469c1bc69d5a2d6781e
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/README.adoc
CHANGED
@@ -23,9 +23,10 @@ toc::[]
|
|
23
23
|
* Ensures equality (i.e. `#==` and `#eql?`) is determined by attribute values and not object identity (i.e. `#equal?`).
|
24
24
|
* Allows you to compare two objects of same or different types and see their differences.
|
25
25
|
* Provides {pattern_matching_link}.
|
26
|
-
*
|
26
|
+
* Provides inheritance so you can subclass and add attributes or provide additional behavior.
|
27
|
+
* Automatically defines public attribute readers (i.e. `.attr_reader`) if _immutable_ (default) or public attribute readers and writers (i.e. `.attr_accessor`) if _mutable_.
|
27
28
|
* Ensures object inspection (i.e. `#inspect`) shows all registered attributes.
|
28
|
-
* Ensures object is frozen upon initialization.
|
29
|
+
* Ensures object is frozen upon initialization by default.
|
29
30
|
|
30
31
|
== Requirements
|
31
32
|
|
@@ -78,9 +79,12 @@ class Person
|
|
78
79
|
end
|
79
80
|
end
|
80
81
|
|
81
|
-
jill = Person
|
82
|
-
jill_two = Person
|
83
|
-
jack = Person
|
82
|
+
jill = Person[name: "Jill Smith", email: "jill@example.com"]
|
83
|
+
jill_two = Person[name: "Jill Smith", email: "jill@example.com"]
|
84
|
+
jack = Person[name: "Jack Smith", email: "jack@example.com"]
|
85
|
+
|
86
|
+
Person.members # [:name, :email]
|
87
|
+
jill.members # [:name, :email]
|
84
88
|
|
85
89
|
jill.name # "Jill Smith"
|
86
90
|
jill.email # "jill@example.com"
|
@@ -122,25 +126,217 @@ jack.to_a # ["Jack Smith", "jack@example.com"]
|
|
122
126
|
jill.to_h # {:name=>"Jill Smith", :email=>"jill@example.com"}
|
123
127
|
jack.to_h # {:name=>"Jack Smith", :email=>"jack@example.com"}
|
124
128
|
|
129
|
+
jill.to_s # "#<Person @name=\"Jill Smith\", @email=\"jill@example.com\">"
|
130
|
+
jill_two.to_s # "#<Person @name=\"Jill Smith\", @email=\"jill@example.com\">"
|
131
|
+
jack.to_s # "#<Person @name=\"Jack Smith\", @email=\"jack@example.com\">"
|
132
|
+
|
125
133
|
jill.with name: "Sue" # #<Person @name="Sue", @email="jill@example.com">
|
126
134
|
jill.with bad: "!" # unknown keyword: :bad (ArgumentError)
|
127
135
|
----
|
128
136
|
|
129
137
|
As you can see, object equality is determined by the object's values and _not_ by the object's identity. When you include `Wholeable` along with a list of keys, the following happens:
|
130
138
|
|
131
|
-
. The corresponding _public_ `attr_reader` for each key is created which saves you time and reduces double entry when implementing your whole value object.
|
132
|
-
. The `#to_a
|
139
|
+
. The corresponding _public_ `attr_reader` (or `attr_accessor` if mutable) for each key is created which saves you time and reduces double entry when implementing your whole value object.
|
140
|
+
. The `#to_a`, `#to_h`, and `#to_s` methods are added for convenience and to be compatible with {data_link} and {structs_link}.
|
133
141
|
. The `#deconstruct` and `#deconstruct_keys` aliases are created so you can leverage {pattern_matching_link}.
|
134
142
|
. The `#==`, `#eql?`, `#hash`, `#inspect`, and `#with` methods are added to provide whole value behavior.
|
135
143
|
. The object is immediately frozen after initialization to ensure your instance is _immutable_ by default.
|
136
144
|
|
145
|
+
=== Initialization
|
146
|
+
|
147
|
+
As shown above, you can create an instance of your whole value object by using `.[]`. Example:
|
148
|
+
|
149
|
+
[source,ruby]
|
150
|
+
----
|
151
|
+
Person[name: "Jill Smith", email: "jill@example.com"]
|
152
|
+
----
|
153
|
+
|
154
|
+
Alternatively, you can create new instances using `.new`. Example:
|
155
|
+
|
156
|
+
[source,ruby]
|
157
|
+
----
|
158
|
+
Person.new name: "Jill Smith", email: "jill@example.com"
|
159
|
+
----
|
160
|
+
|
161
|
+
Both methods work but use `.[]` when supplying arguments and `.new` when you don't have any arguments.
|
162
|
+
|
163
|
+
=== Mutability
|
164
|
+
|
165
|
+
All whole value objects are frozen by default. You can change behavior by specifying whether instances should be mutable by passing `kind: :mutable` as a keyword argument. Example:
|
166
|
+
|
167
|
+
[source,ruby]
|
168
|
+
----
|
169
|
+
class Person
|
170
|
+
include Wholeable[:name, :email, kind: :mutable]
|
171
|
+
|
172
|
+
def initialize name: "Jill", email: "jill@example.com"
|
173
|
+
@name = name
|
174
|
+
@email = email
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
jill = Person.new
|
179
|
+
jill.frozen? # false
|
180
|
+
----
|
181
|
+
|
182
|
+
When your object is mutable, you'll also have access to setter methods in addition to the normal getter methods. Example:
|
183
|
+
|
184
|
+
[source,ruby]
|
185
|
+
----
|
186
|
+
jill.name # "Jill"
|
187
|
+
jill.name = "Jayne"
|
188
|
+
jill.name # "Jayne"
|
189
|
+
----
|
190
|
+
|
191
|
+
You can also make your object immutable by using `kind: :immutable` but this is default behavior and redundant. Any invalid kind (example: `kind: :bogus`) will be ignored and default to being immutable.
|
192
|
+
|
193
|
+
=== Inheritance
|
194
|
+
|
195
|
+
Unlike {data_link} or {structs_link}, you can subclass a whole value object. Example:
|
196
|
+
|
197
|
+
[source,ruby]
|
198
|
+
----
|
199
|
+
class Person
|
200
|
+
include Wholeable[:name]
|
201
|
+
|
202
|
+
def initialize name:
|
203
|
+
@name = name
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
class Contact < Person
|
208
|
+
include Wholeable[:email]
|
209
|
+
|
210
|
+
def initialize(email:, **)
|
211
|
+
super(**)
|
212
|
+
@email = email
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
contact = Contact[name: "Jill Smith", email: "jill@example.com"]
|
217
|
+
|
218
|
+
contact.to_h # {name: "Jill Smith", email: "jill@example.com"}
|
219
|
+
contact.frozen? # true
|
220
|
+
----
|
221
|
+
|
222
|
+
Notice `Contact` inherits from `Person` while only defining the attributes that make it unique. You don't need to redefine the same attributes found in the superclass as that would be redundant and defeat the purpose of subclassing in the first place.
|
223
|
+
|
224
|
+
When subclassing, each subclass has access to the same attributes defined by the superclass no matter how deep your ancestry is. This does mean you must pass the remaining attributes to the superclass via the double splat.
|
225
|
+
|
226
|
+
Mutability is honored but is specific to each object in the ancestry. In other words, if the entire ancestry is immutable then no object can mutate an attribute defined in the ancestry. The same applies if the entire ancestry is mutable except, now, any child can mutate any attribute previously defined by the ancestry. Any attribute that is mutated is only mutated specific to the subclass as is standard inheritance behavior.
|
227
|
+
|
228
|
+
If your ancestry is a mixed (immutable and mutable) then behavior is specific to each child in the ancestry. This means a mutable child won't make the entire ancestry mutable, only the child will be mutable. Best practice is to architect your ancestry so immutability or mutability is the same across all objects. To illustrate, here's an example with an immutable parent and mutable child:
|
229
|
+
|
230
|
+
[source,ruby]
|
231
|
+
----
|
232
|
+
class Parent
|
233
|
+
include Wholeable[:one]
|
234
|
+
|
235
|
+
def initialize one: 1
|
236
|
+
@one = one
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
class Child < Parent
|
241
|
+
include Wholeable[:two, kind: :mutable]
|
242
|
+
|
243
|
+
def initialize(two: 2, **)
|
244
|
+
super(**)
|
245
|
+
@two = two
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
child = Child.new
|
250
|
+
|
251
|
+
child.one = 100 # NoMethodError
|
252
|
+
child.two = 200 # 200
|
253
|
+
child.frozen? # false
|
254
|
+
----
|
255
|
+
|
256
|
+
Notice, when attempting to mutate the `one` attribute, you get a `NoMethodError`. This is because `#one=` is defined by the _immutable_ parent while `#two=` is defined on the _mutable_ child.
|
257
|
+
|
258
|
+
If you the flip mutability of your ancestry, you can make your parent mutable while the child immutable for different behavior. Example:
|
259
|
+
|
260
|
+
[source,ruby]
|
261
|
+
----
|
262
|
+
class Parent
|
263
|
+
include Wholeable[:one, kind: :mutable]
|
264
|
+
|
265
|
+
def initialize one: 1
|
266
|
+
@one = one
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
class Child < Parent
|
271
|
+
include Wholeable[:two]
|
272
|
+
|
273
|
+
def initialize(two: 2, **)
|
274
|
+
super(**)
|
275
|
+
@two = two
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
child = Child.new
|
280
|
+
|
281
|
+
child.one = 100 # FrozenError
|
282
|
+
child.two = 200 # NoMethodError
|
283
|
+
child.frozen? # true
|
284
|
+
----
|
285
|
+
|
286
|
+
In this case, you get a `FrozenError` for `#one=` because the parent is _mutable_ and defined the `#one=` method but the child is _immutable_ which caused the associated attribute to be frozen. On the other hand, the `#two=` method is never defined by the subclass due to being immutable and so you you get a: `NoMethodError`.
|
287
|
+
|
288
|
+
_Again, if using inheritance, ensure immutability or mutability remains consistent throughout the entire ancestry._
|
289
|
+
|
137
290
|
== Caveats
|
138
291
|
|
139
|
-
Whole values can be broken via the following:
|
292
|
+
Whole values can be broken via the following situations:
|
140
293
|
|
141
|
-
* *Duplication*: Sending the `#dup` message will cause your whole value object to be unfrozen. This might be desired in certain situations but make sure to refreeze afterwards.
|
142
294
|
* *Post Attributes*: Adding additional attributes after what is defined when including `Wholeable` will break your whole value object. To prevent this, let Wholeable manage this for you (easiest). Otherwise (harder), you can manually override `#==`, `#eql?`, `#hash`, `#inspect`, `#to_a`, and `#to_h` behavior at which point you don't need Wholeable anymore.
|
143
|
-
* *Deep Freezing*: The automatic freezing of your instances is shallow and will not
|
295
|
+
* *Deep Freezing*: The automatic freezing of your instances is shallow and will not deep freeze nested attributes. This behavior mimics the behavior of {data_link} objects.
|
296
|
+
|
297
|
+
== Performance
|
298
|
+
|
299
|
+
The performance of this gem is good but definitely slower than native support for {data_link} and {structs_link} because they are written in C. To illustrate, here's a micro benchmark for comparison:
|
300
|
+
|
301
|
+
----
|
302
|
+
INITIALIZATION
|
303
|
+
|
304
|
+
ruby 3.3.5 (2024-09-03 revision ef084cc8f4) +YJIT [arm64-darwin23.6.0]
|
305
|
+
Warming up --------------------------------------
|
306
|
+
Data 470.027k i/100ms
|
307
|
+
Struct 422.010k i/100ms
|
308
|
+
Whole 805.945k i/100ms
|
309
|
+
Calculating -------------------------------------
|
310
|
+
Data 4.750M (± 1.1%) i/s (210.53 ns/i) - 23.971M in 5.047225s
|
311
|
+
Struct 4.579M (± 1.1%) i/s (218.38 ns/i) - 23.211M in 5.069228s
|
312
|
+
Whole 9.408M (± 1.2%) i/s (106.29 ns/i) - 47.551M in 5.055033s
|
313
|
+
|
314
|
+
Comparison:
|
315
|
+
Whole: 9407938.7 i/s - 1.60x slower
|
316
|
+
Data: 4750013.8 i/s - 3.17x slower
|
317
|
+
Struct: 4579253.1 i/s - 3.28x slower
|
318
|
+
|
319
|
+
BEHAVIOR
|
320
|
+
|
321
|
+
ruby 3.3.5 (2024-09-03 revision ef084cc8f4) +YJIT [arm64-darwin23.6.0]
|
322
|
+
Warming up --------------------------------------
|
323
|
+
Data 129.006k i/100ms
|
324
|
+
Struct 129.832k i/100ms
|
325
|
+
Wholeable 78.861k i/100ms
|
326
|
+
Calculating -------------------------------------
|
327
|
+
Data 1.336M (± 3.6%) i/s (748.33 ns/i) - 6.708M in 5.027517s
|
328
|
+
Struct 1.341M (± 1.7%) i/s (745.89 ns/i) - 6.751M in 5.037050s
|
329
|
+
Wholeable 816.232k (± 1.9%) i/s (1.23 μs/i) - 4.101M in 5.025751s
|
330
|
+
|
331
|
+
Comparison:
|
332
|
+
Struct: 1340687.5 i/s
|
333
|
+
Data: 1336304.1 i/s - same-ish: difference falls within error
|
334
|
+
Wholeable: 816232.0 i/s - 1.64x slower
|
335
|
+
----
|
336
|
+
|
337
|
+
While the above isn't bad, you can definitely see this gem is slower than Ruby's own native objects when interacting with it despite being faster upon initialization.
|
338
|
+
|
339
|
+
Default to using {data_link} or {structs_link} but, if you find yourself needing a whole value object with more behavior than what a `Data` or `Struct` can provide, then this gem is a good solution.
|
144
340
|
|
145
341
|
== Development
|
146
342
|
|
data/lib/wholeable/builder.rb
CHANGED
@@ -3,29 +3,51 @@
|
|
3
3
|
module Wholeable
|
4
4
|
# Provides core equality behavior.
|
5
5
|
class Builder < Module
|
6
|
-
def
|
6
|
+
def self.add_aliases descendant
|
7
|
+
descendant.alias_method :deconstruct, :to_a
|
8
|
+
descendant.alias_method :deconstruct_keys, :to_h
|
9
|
+
descendant.alias_method :to_s, :inspect
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize *keys, kind: :immutable
|
7
13
|
super()
|
8
14
|
@keys = keys.uniq
|
9
|
-
|
10
|
-
|
15
|
+
@kind = kind
|
16
|
+
@members = []
|
17
|
+
setup
|
11
18
|
end
|
12
19
|
|
13
20
|
def included descendant
|
14
21
|
super
|
22
|
+
coalesce_members descendant
|
15
23
|
|
16
|
-
descendant.class_eval <<-
|
17
|
-
def self.
|
24
|
+
descendant.class_eval <<-METHODS, __FILE__, __LINE__ + 1
|
25
|
+
def self.[](...) = new(...)
|
18
26
|
|
19
|
-
|
20
|
-
READER
|
27
|
+
def self.new(...) = #{mutable?} ? super.dup : super.freeze
|
21
28
|
|
22
|
-
|
23
|
-
|
29
|
+
def self.members = #{members}
|
30
|
+
|
31
|
+
#{mutable? ? :attr_accessor : :attr_reader} #{keys.map(&:inspect).join ", "}
|
32
|
+
METHODS
|
33
|
+
|
34
|
+
self.class.add_aliases descendant
|
24
35
|
end
|
25
36
|
|
26
37
|
private
|
27
38
|
|
28
|
-
attr_reader :keys
|
39
|
+
attr_reader :keys, :kind, :members
|
40
|
+
|
41
|
+
def setup
|
42
|
+
private_methods.grep(/\A(define)_/).sort.each { |method| __send__ method }
|
43
|
+
freeze
|
44
|
+
end
|
45
|
+
|
46
|
+
def coalesce_members descendant
|
47
|
+
members.replace(descendant.respond_to?(:members) ? (descendant.members + keys).uniq : keys)
|
48
|
+
end
|
49
|
+
|
50
|
+
def mutable? = kind == :mutable
|
29
51
|
|
30
52
|
def define_diff
|
31
53
|
define_method :diff do |other|
|
@@ -46,34 +68,36 @@ module Wholeable
|
|
46
68
|
define_method(:==) { |other| other.is_a?(self.class) && hash == other.hash }
|
47
69
|
end
|
48
70
|
|
49
|
-
def define_hash
|
71
|
+
def define_hash
|
50
72
|
define_method :hash do
|
51
|
-
|
52
|
-
|
53
|
-
|
73
|
+
members.map { |key| public_send key }
|
74
|
+
.prepend(self.class)
|
75
|
+
.hash
|
54
76
|
end
|
55
77
|
end
|
56
78
|
|
57
|
-
def define_inspect
|
79
|
+
def define_inspect
|
58
80
|
define_method :inspect do
|
59
81
|
klass = self.class
|
60
82
|
name = klass.name || klass.inspect
|
61
83
|
|
62
|
-
|
63
|
-
|
64
|
-
|
84
|
+
members.map { |key| "@#{key}=#{public_send(key).inspect}" }
|
85
|
+
.join(", ")
|
86
|
+
.then { |pairs| "#<#{name} #{pairs}>" }
|
65
87
|
end
|
66
88
|
end
|
67
89
|
|
68
|
-
def
|
90
|
+
def define_members(local_members = members) = define_method(:members) { local_members }
|
91
|
+
|
92
|
+
def define_to_a
|
69
93
|
define_method :to_a do
|
70
|
-
|
94
|
+
members.reduce([]) { |collection, key| collection.append public_send(key) }
|
71
95
|
end
|
72
96
|
end
|
73
97
|
|
74
|
-
def define_to_h
|
98
|
+
def define_to_h
|
75
99
|
define_method :to_h do
|
76
|
-
|
100
|
+
members.each.with_object({}) { |key, attributes| attributes[key] = public_send key }
|
77
101
|
end
|
78
102
|
end
|
79
103
|
|
data/lib/wholeable.rb
CHANGED
data/wholeable.gemspec
CHANGED
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: wholeable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brooke Kuhlmann
|
@@ -35,7 +35,7 @@ cert_chain:
|
|
35
35
|
3n5C8/6Zh9DYTkpcwPSuIfAga6wf4nXc9m6JAw8AuMLaiWN/r/2s4zJsUHYERJEu
|
36
36
|
gZGm4JqtuSg8pYjPeIJxS960owq+SfuC+jxqmRA54BisFCv/0VOJi7tiJVY=
|
37
37
|
-----END CERTIFICATE-----
|
38
|
-
date: 2024-10-
|
38
|
+
date: 2024-10-06 00:00:00.000000000 Z
|
39
39
|
dependencies: []
|
40
40
|
description:
|
41
41
|
email:
|
metadata.gz.sig
CHANGED
Binary file
|