lab42_data_class 0.6.0 → 0.7.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/README.md +86 -400
- data/lib/lab42/data_class/duplicate_definition_error.rb +9 -0
- data/lib/lab42/data_class/proxy/constraints/maker.rb +2 -0
- data/lib/lab42/data_class/proxy/derived.rb +21 -0
- data/lib/lab42/data_class/proxy/memos.rb +8 -0
- data/lib/lab42/data_class/proxy.rb +55 -9
- data/lib/lab42/data_class/version.rb +1 -1
- data/lib/lab42/data_class.rb +18 -3
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2ae30c490e9b798403214f49c4cbc43564bf47b741878f9aa174e27dc17a85b8
|
4
|
+
data.tar.gz: 2e7ab04142e8eb38206ba14bd03464465844b307087708d382e253fc40dd8f1b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f40faa136f73115809cea4f73d722188ea62e9299319313b1ffcbcc852c710de0c399c3d1c54dcdb1ae1de57173baabfb66607f636ff62b747f133774e4c205a
|
7
|
+
data.tar.gz: 294c975087b12a3cab2731eb78840d735dbdeac923f1c2edb8d5beaf598848caf18d571e3129b42dac0ad2d276657507f4df10268a41863ffb2150ea11df5d8a
|
data/README.md
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
|
2
|
+
|
2
3
|
[](https://codeclimate.com/github/RobertDober/lab42_data_class)
|
3
4
|
[](https://github.com/robertdober/lab42_data_class/actions)
|
4
5
|
[](https://coveralls.io/github/RobertDober/lab42_data_class?branch=main)
|
@@ -11,9 +12,31 @@
|
|
11
12
|
|
12
13
|
An Immutable DataClass for Ruby
|
13
14
|
|
14
|
-
Exposes a class factory function `Kernel::DataClass` and a
|
15
|
-
|
16
|
-
|
15
|
+
Exposes a class factory function `Kernel::DataClass` and a module `Lab42::DataClass` which can
|
16
|
+
extend classes to become _Data Classes_.
|
17
|
+
|
18
|
+
Also exposes two _tuple_ classes, `Pair` and `Triple`
|
19
|
+
|
20
|
+
## Synopsis
|
21
|
+
|
22
|
+
Having immutable Objects has many well known advantages that I will not ponder upon in detail here.
|
23
|
+
|
24
|
+
One advantage which is of particular interest though is that, as every, _modification_ is in fact the
|
25
|
+
creation of a new object **strong contraints** on the data can **easily** be maintained, and this
|
26
|
+
library makes that available to the user.
|
27
|
+
|
28
|
+
Therefore we can summarise the features (or not so features, that is for you to decide and you to chose to use or not):
|
29
|
+
|
30
|
+
- Immutable with an Interface à la `OpenStruct`
|
31
|
+
- Attributes are predefined and can have **default values**
|
32
|
+
- Construction with _keyword arguments_, **exclusively**
|
33
|
+
- Conversion to `Hash` instances (if you must)
|
34
|
+
- Pattern matching exactly like `Hash` instances
|
35
|
+
- Possibility to impose **strong constraints** on attributes
|
36
|
+
- Predefined constraints and concise syntax for constraints
|
37
|
+
- Possibility to impose **arbitrary validation** (constraints on the whole object)
|
38
|
+
- Declaration of **dependent attributes** which are memoized (thank you _Immutability_)
|
39
|
+
- Inheritance with **mixin of other dataclasses** (multiple if you must)
|
17
40
|
|
18
41
|
## Usage
|
19
42
|
|
@@ -33,465 +56,128 @@ In your code
|
|
33
56
|
require 'lab42/data_class'
|
34
57
|
```
|
35
58
|
|
59
|
+
## Speculations (literate specs)
|
36
60
|
|
37
|
-
|
38
|
-
|
39
|
-
Well let us [speculate about](https://github.com/RobertDober/speculate_about) it to find out:
|
40
|
-
|
41
|
-
## Context `DataClass`
|
42
|
-
|
43
|
-
|
44
|
-
### Context: `DataClass` function
|
45
|
-
|
46
|
-
Given
|
47
|
-
```ruby
|
48
|
-
let(:my_data_class) { DataClass(:name, email: nil) }
|
49
|
-
let(:my_instance) { my_data_class.new(name: "robert") }
|
50
|
-
```
|
51
|
-
|
52
|
-
Then we can access its fields
|
53
|
-
```ruby
|
54
|
-
expect(my_instance.name).to eq("robert")
|
55
|
-
expect(my_instance.email).to be_nil
|
56
|
-
```
|
57
|
-
|
58
|
-
But we cannot access undefined fields
|
59
|
-
```ruby
|
60
|
-
expect{ my_instance.undefined }.to raise_error(NoMethodError)
|
61
|
-
```
|
62
|
-
|
63
|
-
And we need to provide values to fields without defaults
|
64
|
-
```ruby
|
65
|
-
expect{ my_data_class.new(email: "some@mail.org") }
|
66
|
-
.to raise_error(ArgumentError, "missing initializers for [:name]")
|
67
|
-
```
|
68
|
-
And we can extract the values
|
69
|
-
```ruby
|
70
|
-
expect(my_instance.to_h).to eq(name: "robert", email: nil)
|
71
|
-
```
|
72
|
-
|
73
|
-
#### Context: Immutable → self
|
74
|
-
|
75
|
-
Then `my_instance` is frozen:
|
76
|
-
```ruby
|
77
|
-
expect(my_instance).to be_frozen
|
78
|
-
```
|
79
|
-
And we cannot even mute `my_instance` by means of metaprogramming
|
80
|
-
```ruby
|
81
|
-
expect{ my_instance.instance_variable_set("@x", nil) }.to raise_error(FrozenError)
|
82
|
-
```
|
83
|
-
|
84
|
-
#### Context: Immutable → Cloning
|
85
|
-
|
86
|
-
Given
|
87
|
-
```ruby
|
88
|
-
let(:other_instance) { my_instance.merge(email: "robert@mail.provider") }
|
89
|
-
```
|
90
|
-
Then we have a new instance with the old instance unchanged
|
91
|
-
```ruby
|
92
|
-
expect(other_instance.to_h).to eq(name: "robert", email: "robert@mail.provider")
|
93
|
-
expect(my_instance.to_h).to eq(name: "robert", email: nil)
|
94
|
-
```
|
95
|
-
And the new instance is frozen again
|
96
|
-
```ruby
|
97
|
-
expect(other_instance).to be_frozen
|
98
|
-
```
|
99
|
-
|
100
|
-
### Context: Defining behavior with blocks
|
101
|
-
|
102
|
-
Given
|
103
|
-
```ruby
|
104
|
-
let :my_data_class do
|
105
|
-
DataClass :value, prefix: "<", suffix: ">" do
|
106
|
-
def show
|
107
|
-
[prefix, value, suffix].join
|
108
|
-
end
|
109
|
-
end
|
110
|
-
end
|
111
|
-
let(:my_instance) { my_data_class.new(value: 42) }
|
112
|
-
```
|
113
|
-
|
114
|
-
Then I have defined a method on my dataclass
|
115
|
-
```ruby
|
116
|
-
expect(my_instance.show).to eq("<42>")
|
117
|
-
```
|
118
|
-
|
119
|
-
### Context: Equality
|
120
|
-
|
121
|
-
Given two instances of a DataClass
|
122
|
-
```ruby
|
123
|
-
let(:data_class) { DataClass :a }
|
124
|
-
let(:instance1) { data_class.new(a: 1) }
|
125
|
-
let(:instance2) { data_class.new(a: 1) }
|
126
|
-
```
|
127
|
-
Then they are equal in the sense of `==` and `eql?`
|
128
|
-
```ruby
|
129
|
-
expect(instance1).to eq(instance2)
|
130
|
-
expect(instance2).to eq(instance1)
|
131
|
-
expect(instance1 == instance2).to be_truthy
|
132
|
-
expect(instance2 == instance1).to be_truthy
|
133
|
-
```
|
134
|
-
But not in the sense of `equal?`, of course
|
135
|
-
```ruby
|
136
|
-
expect(instance1).not_to be_equal(instance2)
|
137
|
-
expect(instance2).not_to be_equal(instance1)
|
138
|
-
```
|
139
|
-
|
140
|
-
#### Context: Immutability of `dataclass` modified classes
|
141
|
-
|
142
|
-
Then we still get frozen instances
|
143
|
-
```ruby
|
144
|
-
expect(instance1).to be_frozen
|
145
|
-
```
|
146
|
-
|
147
|
-
### Context: Inheritance
|
148
|
-
|
149
|
-
... is a no, we do not want inheritance although we **like** code reuse, how to do it then?
|
150
|
-
|
151
|
-
Well there shall be many different possibilities, depending on your style, use case and
|
152
|
-
context, here is just one example:
|
153
|
-
|
154
|
-
Given a class factory
|
155
|
-
```ruby
|
156
|
-
let :token do
|
157
|
-
->(*a, **k) do
|
158
|
-
DataClass(*a, **(k.merge(text: "")))
|
159
|
-
end
|
160
|
-
end
|
161
|
-
```
|
162
|
-
|
163
|
-
Then we have reused the `token` successfully
|
164
|
-
```ruby
|
165
|
-
empty = token.()
|
166
|
-
integer = token.(:value)
|
167
|
-
boolean = token.(value: false)
|
168
|
-
|
169
|
-
expect(empty.new.to_h).to eq(text: "")
|
170
|
-
expect(integer.new(value: -1).to_h).to eq(text: "", value: -1)
|
171
|
-
expect(boolean.new.value).to eq(false)
|
172
|
-
```
|
173
|
-
|
174
|
-
#### Context: Mixing in a module can be used of course
|
175
|
-
|
176
|
-
Given a behavior like
|
177
|
-
```ruby
|
178
|
-
module Humanize
|
179
|
-
def humanize
|
180
|
-
"my value is #{value}"
|
181
|
-
end
|
182
|
-
end
|
183
|
-
|
184
|
-
let(:class_level) { DataClass(value: 1).include(Humanize) }
|
185
|
-
```
|
186
|
-
|
187
|
-
Then we can access the included method
|
188
|
-
```ruby
|
189
|
-
expect(class_level.new.humanize).to eq("my value is 1")
|
190
|
-
```
|
191
|
-
|
192
|
-
### Context: Pattern Matching
|
193
|
-
|
194
|
-
A `DataClass` object behaves like the result of it's `to_h` in pattern matching
|
195
|
-
|
196
|
-
Given
|
197
|
-
```ruby
|
198
|
-
let(:numbers) { DataClass(:name, values: []) }
|
199
|
-
let(:odds) { numbers.new(name: "odds", values: (1..4).map{ _1 + _1 + 1}) }
|
200
|
-
let(:evens) { numbers.new(name: "evens", values: (1..4).map{ _1 + _1}) }
|
201
|
-
```
|
202
|
-
|
203
|
-
Then we can match accordingly
|
204
|
-
```ruby
|
205
|
-
match = case odds
|
206
|
-
in {name: "odds", values: [1, *]}
|
207
|
-
:not_really
|
208
|
-
in {name: "evens"}
|
209
|
-
:still_naaah
|
210
|
-
in {name: "odds", values: [hd, *]}
|
211
|
-
hd
|
212
|
-
else
|
213
|
-
:strange
|
214
|
-
end
|
215
|
-
expect(match).to eq(3)
|
216
|
-
```
|
217
|
-
|
218
|
-
And in `in` expressions
|
219
|
-
```ruby
|
220
|
-
evens => {values: [_, second, *]}
|
221
|
-
expect(second).to eq(4)
|
222
|
-
```
|
223
|
-
|
224
|
-
#### Context: In Case Statements
|
225
|
-
|
226
|
-
Given a nice little dataclass `Box`
|
227
|
-
```ruby
|
228
|
-
let(:box) { DataClass content: nil }
|
229
|
-
```
|
230
|
-
|
231
|
-
Then we can also use it in a case statement
|
232
|
-
```ruby
|
233
|
-
value = case box.new
|
234
|
-
when box
|
235
|
-
42
|
236
|
-
else
|
237
|
-
0
|
238
|
-
end
|
239
|
-
expect(value).to eq(42)
|
240
|
-
```
|
241
|
-
|
242
|
-
And all the associated methods
|
243
|
-
```ruby
|
244
|
-
expect(box.new).to be_a(box)
|
245
|
-
expect(box === box.new).to be_truthy
|
246
|
-
```
|
247
|
-
|
248
|
-
### Context: Behaving like a `Proc`
|
249
|
-
|
250
|
-
It is useful to be able to filter heterogeneous lists of `DataClass` instances by means of `&to_proc`, therefore
|
61
|
+
The following specs are executed with the [speculate about](https://github.com/RobertDober/speculate_about) gem.
|
251
62
|
|
252
|
-
Given
|
63
|
+
Given that we have imported the `Lab42` namespace
|
253
64
|
```ruby
|
254
|
-
|
255
|
-
let(:class2) { DataClass :value }
|
65
|
+
DataClass = Lab42::DataClass
|
256
66
|
```
|
257
67
|
|
258
|
-
|
259
|
-
```ruby
|
260
|
-
let(:list) {[class1.new(value: 1), class2.new(value: 2), class1.new(value: 3)]}
|
261
|
-
```
|
262
|
-
|
263
|
-
Then we can filter
|
264
|
-
```ruby
|
265
|
-
expect(list.filter(&class2)).to eq([class2.new(value: 2)])
|
266
|
-
```
|
267
|
-
|
268
|
-
### Context: Behaving like a `Hash`
|
269
|
-
|
270
|
-
We have already seen the `to_h` method, however if we want to pass an instance of `DataClass` as
|
271
|
-
keyword parameters we need an implementation of `to_hash`, which of course is just an alias
|
272
|
-
|
273
|
-
Given this keyword method
|
274
|
-
```ruby
|
275
|
-
def extract_value(value:, **others)
|
276
|
-
[value, others]
|
277
|
-
end
|
278
|
-
```
|
279
|
-
And this `DataClass`:
|
280
|
-
```ruby
|
281
|
-
let(:my_class) { DataClass(value: 1, base: 2) }
|
282
|
-
```
|
68
|
+
## Context: Data Classes
|
283
69
|
|
284
|
-
|
285
|
-
```ruby
|
286
|
-
expect(extract_value(**my_class.new)).to eq([1, base: 2])
|
287
|
-
```
|
288
|
-
|
289
|
-
### Context: Constraints
|
290
|
-
|
291
|
-
Values of attributes of a `DataClass` can have constraints
|
70
|
+
### Basic Use Case
|
292
71
|
|
293
|
-
Given a
|
72
|
+
Given a simple Data Class
|
294
73
|
```ruby
|
295
|
-
|
296
|
-
DataClass
|
74
|
+
class SimpleDataClass
|
75
|
+
extend DataClass
|
76
|
+
attributes :a, :b
|
297
77
|
end
|
298
78
|
```
|
299
79
|
|
300
|
-
|
80
|
+
And an instance of it
|
301
81
|
```ruby
|
302
|
-
|
303
|
-
expect(switch.new.merge(on: true).on).to eq(true)
|
82
|
+
let(:simple_instance) { SimpleDataClass.new(a: 1, b: 2) }
|
304
83
|
```
|
305
84
|
|
306
|
-
|
85
|
+
Then we access the fields
|
307
86
|
```ruby
|
308
|
-
expect
|
309
|
-
|
310
|
-
expect{ switch.new.merge(on: 42) }
|
311
|
-
.to raise_error(Lab42::DataClass::ConstraintError, "value 42 is not allowed for attribute :on")
|
87
|
+
expect(simple_instance.a).to eq(1)
|
88
|
+
expect(simple_instance.b).to eq(2)
|
312
89
|
```
|
313
90
|
|
314
|
-
And
|
91
|
+
And we convert to a hash
|
315
92
|
```ruby
|
316
|
-
|
317
|
-
error_body = " undefined method `>' for nil:NilClass"
|
318
|
-
error_message = [error_head, error_body].join("\n")
|
319
|
-
|
320
|
-
expect{ DataClass(value: nil).with_constraint(value: -> { _1 > 0 }) }
|
321
|
-
.to raise_error(Lab42::DataClass::ConstraintError, /#{error_message}/)
|
93
|
+
expect(simple_instance.to_h).to eq(a: 1, b: 2)
|
322
94
|
```
|
323
95
|
|
324
|
-
And
|
96
|
+
And we can derive new instances
|
325
97
|
```ruby
|
326
|
-
|
327
|
-
|
98
|
+
new_instance = simple_instance.merge(b: 3)
|
99
|
+
expect(new_instance.to_h).to eq(a: 1, b: 3)
|
100
|
+
expect(simple_instance.to_h).to eq(a: 1, b: 2)
|
328
101
|
```
|
329
102
|
|
103
|
+
For detailed speculations please see [here](speculations/DATA_CLASSES.md)
|
330
104
|
|
331
|
-
|
105
|
+
## Context: `DataClass` function
|
332
106
|
|
333
|
-
|
334
|
-
it
|
107
|
+
As seen in the speculations above it seems appropriate to declare a `Class` and
|
108
|
+
extend it as we will add quite some code for constraints, derived attributes and validations.
|
335
109
|
|
336
|
-
|
337
|
-
```ruby
|
338
|
-
let(:constraint_error) { Lab42::DataClass::ConstraintError }
|
339
|
-
let(:positive) { DataClass(:value) }
|
340
|
-
```
|
110
|
+
However a more concise _Factory Function_ might still be very useful in some use cases...
|
341
111
|
|
342
|
-
|
112
|
+
Enter `Kernel::DataClass` **The Function**
|
343
113
|
|
344
|
-
|
114
|
+
### Context: Just Attributes
|
345
115
|
|
346
|
-
|
347
|
-
|
348
|
-
positive_by_symbol = positive.with_constraint(value: :positive?)
|
349
|
-
|
350
|
-
expect(positive_by_symbol.new(value: 1).value).to eq(1)
|
351
|
-
expect{positive_by_symbol.new(value: 0)}.to raise_error(constraint_error)
|
352
|
-
```
|
116
|
+
If there are no _Constraints_, _Derived Attributes_, _Validation_ or _Inheritance_ this concise syntax
|
117
|
+
might easily be preferred by many:
|
353
118
|
|
354
|
-
|
355
|
-
|
356
|
-
... are also sent to the value of the attribute, this time we can provide paramaters
|
357
|
-
And we can implement a different form of `Positive`
|
119
|
+
Given some example instances like these
|
358
120
|
```ruby
|
359
|
-
|
360
|
-
|
361
|
-
expect(positive_by_ary.new(value: 1).value).to eq(1)
|
362
|
-
expect{positive_by_ary.new(value: 0)}.to raise_error(constraint_error)
|
121
|
+
let(:my_data_class) { DataClass(:name, email: nil) }
|
122
|
+
let(:my_instance) { my_data_class.new(name: "robert") }
|
363
123
|
```
|
364
124
|
|
365
|
-
|
366
|
-
|
367
|
-
##### Membership
|
368
|
-
|
369
|
-
And this works with a `Set`
|
125
|
+
Then we can access its fields
|
370
126
|
```ruby
|
371
|
-
|
372
|
-
|
373
|
-
expect(positive_by_set.new(value: 1).value).to eq(1)
|
374
|
-
expect{positive_by_set.new(value: 0)}.to raise_error(constraint_error)
|
127
|
+
expect(my_instance.name).to eq("robert")
|
128
|
+
expect(my_instance[:email]).to be_nil
|
375
129
|
```
|
376
130
|
|
377
|
-
|
131
|
+
But we cannot access undefined fields
|
378
132
|
```ruby
|
379
|
-
|
380
|
-
|
381
|
-
expect(positive_by_range.new(value: 1).value).to eq(1)
|
382
|
-
expect{positive_by_range.new(value: 0)}.to raise_error(constraint_error)
|
133
|
+
expect{ my_instance.undefined }.to raise_error(NoMethodError)
|
383
134
|
```
|
384
135
|
|
385
|
-
|
386
|
-
|
387
|
-
This seems quite obvious, and of course it works
|
388
|
-
|
389
|
-
Then we can also have a regex based constraint
|
136
|
+
And this is even true for the `[]` syntax
|
390
137
|
```ruby
|
391
|
-
|
392
|
-
|
393
|
-
expect(vowel.new(word: "alpha").word).to eq("alpha")
|
394
|
-
expect{vowel.new(word: "krk")}.to raise_error(constraint_error)
|
138
|
+
expect{ my_instance[:undefined] }.to raise_error(KeyError)
|
395
139
|
```
|
396
140
|
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
Then we can also use instance methods to implement our `Positive`
|
141
|
+
And we need to provide values to fields without defaults
|
401
142
|
```ruby
|
402
|
-
|
403
|
-
|
404
|
-
expect(positive_by_instance_method.new(value: 1).value).to eq(1)
|
405
|
-
expect{positive_by_instance_method.new(value: 0)}.to raise_error(constraint_error)
|
143
|
+
expect{ my_data_class.new(email: "some@mail.org") }
|
144
|
+
.to raise_error(ArgumentError, "missing initializers for [:name]")
|
406
145
|
```
|
407
|
-
|
408
|
-
Or we can use methods to implement it
|
146
|
+
And we can extract the values
|
409
147
|
```ruby
|
410
|
-
|
411
|
-
|
412
|
-
expect(positive_by_method.new(value: 1).value).to eq(1)
|
413
|
-
expect{positive_by_method.new(value: 0)}.to raise_error(constraint_error)
|
148
|
+
expect(my_instance.to_h).to eq(name: "robert", email: nil)
|
414
149
|
```
|
415
150
|
|
416
|
-
#### Context:
|
417
|
-
|
418
|
-
So far we have only speculated about constraints concerning one attribute, however sometimes we want
|
419
|
-
to have arbitrary constraints which can only be calculated by access to more attributes
|
420
|
-
|
421
|
-
Given a `Point` DataClass
|
422
|
-
```ruby
|
423
|
-
let(:point) { DataClass(:x, :y).validate{ |point| point.x > point.y } }
|
424
|
-
let(:validation_error) { Lab42::DataClass::ValidationError }
|
425
|
-
```
|
151
|
+
#### Context: Immutable → self
|
426
152
|
|
427
|
-
Then
|
153
|
+
Then `my_instance` is frozen:
|
428
154
|
```ruby
|
429
|
-
expect
|
430
|
-
.to raise_error(validation_error)
|
155
|
+
expect(my_instance).to be_frozen
|
431
156
|
```
|
432
|
-
|
433
|
-
But as validation might need more than the default values we will not execute them at compile time
|
157
|
+
And we cannot even mute `my_instance` by means of metaprogramming
|
434
158
|
```ruby
|
435
|
-
expect{
|
436
|
-
.to_not raise_error
|
159
|
+
expect{ my_instance.instance_variable_set("@x", nil) }.to raise_error(FrozenError)
|
437
160
|
```
|
438
161
|
|
439
|
-
|
440
|
-
```ruby
|
441
|
-
better_point = DataClass(:x, :y).validate(:too_left){ |point| point.x > point.y }
|
442
|
-
ok_point = better_point.new(x: 1, y: 0)
|
443
|
-
expect{ ok_point.merge(y: 1) }
|
444
|
-
.to raise_error(validation_error, "too_left")
|
445
|
-
```
|
162
|
+
#### Context: Immutable → Cloning
|
446
163
|
|
447
|
-
|
164
|
+
Given
|
448
165
|
```ruby
|
449
|
-
|
450
|
-
\#<Proc:0x[0-9a-f]+ \s .* spec/speculations/README_spec\.rb: \d+ > \z
|
451
|
-
}x
|
452
|
-
expect{ point.new(x: 0, y: 1) }
|
453
|
-
.to raise_error(validation_error, error_message_rgx)
|
166
|
+
let(:other_instance) { my_instance.merge(email: "robert@mail.provider") }
|
454
167
|
```
|
455
|
-
|
456
|
-
### Context: Usage with `extend`
|
457
|
-
|
458
|
-
All the above mentioned features can be achieved with a more conventional syntax by extending a class
|
459
|
-
with `Lab42::DataClass`
|
460
|
-
|
461
|
-
Given a class that extends `DataClass`
|
168
|
+
Then we have a new instance with the old instance unchanged
|
462
169
|
```ruby
|
463
|
-
|
464
|
-
|
465
|
-
extend Lab42::DataClass
|
466
|
-
attributes :age, member: false
|
467
|
-
constraint :member, Set.new([false, true])
|
468
|
-
validate(:too_young_for_member) { |instance| !(instance.member && instance.age < 18) }
|
469
|
-
end
|
470
|
-
end
|
471
|
-
let(:constraint_error) { Lab42::DataClass::ConstraintError }
|
472
|
-
let(:validation_error) { Lab42::DataClass::ValidationError }
|
473
|
-
let(:my_instance) { my_class.new(age: 42) }
|
474
|
-
let(:my_vip) { my_instance.merge(member: true) }
|
170
|
+
expect(other_instance.to_h).to eq(name: "robert", email: "robert@mail.provider")
|
171
|
+
expect(my_instance.to_h).to eq(name: "robert", email: nil)
|
475
172
|
```
|
476
|
-
|
477
|
-
Then we can observe that instances of such a class
|
173
|
+
And the new instance is frozen again
|
478
174
|
```ruby
|
479
|
-
expect(
|
480
|
-
expect(my_vip.to_h).to eq(age: 42, member: true)
|
481
|
-
expect(my_instance.member).to be_falsy
|
175
|
+
expect(other_instance).to be_frozen
|
482
176
|
```
|
483
177
|
|
484
|
-
|
485
|
-
|
486
|
-
expect{my_instance.merge(member: nil)}
|
487
|
-
.to raise_error(constraint_error)
|
488
|
-
```
|
178
|
+
For speculations how to add all the other features to the _Factory Function_ syntax please
|
179
|
+
look [here](speculations/FACTORY_FUNCTION.md)
|
489
180
|
|
490
|
-
And of course validations still work too
|
491
|
-
```ruby
|
492
|
-
expect{ my_vip.merge(age: 17) }
|
493
|
-
.to raise_error(validation_error, "too_young_for_member")
|
494
|
-
```
|
495
181
|
|
496
182
|
|
497
183
|
## Context: `Pair` and `Triple`
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab42
|
4
|
+
module DataClass
|
5
|
+
class Proxy
|
6
|
+
module Derived
|
7
|
+
private
|
8
|
+
def _define_derived
|
9
|
+
proxy = self
|
10
|
+
->(*) do
|
11
|
+
define_method :derive do |att_name, &blk|
|
12
|
+
proxy.define_derived_attribute(att_name, &blk)
|
13
|
+
self
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
# SPDX-License-Identifier: Apache-2.0
|
@@ -4,6 +4,10 @@ module Lab42
|
|
4
4
|
module DataClass
|
5
5
|
class Proxy
|
6
6
|
module Memos
|
7
|
+
def all_attributes
|
8
|
+
@__all_attributes__ ||= members&.union(Set.new(derived_attributes.keys))
|
9
|
+
end
|
10
|
+
|
7
11
|
def constraints
|
8
12
|
@__constraints__ ||= Hash.new { |h, k| h[k] = [] }
|
9
13
|
end
|
@@ -12,6 +16,10 @@ module Lab42
|
|
12
16
|
@__defaults__ ||= {}
|
13
17
|
end
|
14
18
|
|
19
|
+
def derived_attributes
|
20
|
+
@__derived_attributes__ ||= {}
|
21
|
+
end
|
22
|
+
|
15
23
|
def members
|
16
24
|
@__members__ ||= unless (positionals + defaults.keys).empty?
|
17
25
|
Set.new(positionals + defaults.keys)
|
@@ -2,15 +2,33 @@
|
|
2
2
|
|
3
3
|
require 'set'
|
4
4
|
require_relative 'proxy/constraints'
|
5
|
+
require_relative 'proxy/derived'
|
5
6
|
require_relative 'proxy/memos'
|
6
7
|
require_relative 'proxy/validations'
|
7
8
|
require_relative 'proxy/mixin'
|
8
9
|
module Lab42
|
9
10
|
module DataClass
|
10
11
|
class Proxy
|
11
|
-
include Constraints, Memos, Validations
|
12
|
+
include Constraints, Derived, Memos, Validations
|
12
13
|
|
13
|
-
attr_reader :actual_params, :block, :klass
|
14
|
+
attr_reader :actual_params, :block, :klass, :klass_defined
|
15
|
+
|
16
|
+
def self.from_parent(parent, klass)
|
17
|
+
new(klass).tap do |proxy|
|
18
|
+
proxy.positionals.push(*parent.positionals)
|
19
|
+
proxy.defaults.update(parent.defaults)
|
20
|
+
proxy.constraints.update(parent.constraints)
|
21
|
+
proxy.validations.push(*parent.validations)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def access(data_class_instance, key)
|
26
|
+
if all_attributes.member?(key)
|
27
|
+
data_class_instance.send(key)
|
28
|
+
else
|
29
|
+
raise KeyError, "#{key} is not an attribute of #{data_class_instance}"
|
30
|
+
end
|
31
|
+
end
|
14
32
|
|
15
33
|
def check!(**params)
|
16
34
|
@actual_params = params
|
@@ -21,22 +39,33 @@ module Lab42
|
|
21
39
|
end
|
22
40
|
|
23
41
|
def define_class!
|
42
|
+
return if @klass_defined
|
43
|
+
|
44
|
+
@klass_defined = true
|
24
45
|
klass.module_eval(&_define_attr_reader)
|
25
|
-
klass.module_eval(&_define_initializer)
|
46
|
+
klass.module_eval(&_define_initializer) if Class === klass
|
26
47
|
_define_methods
|
27
48
|
klass.include(Mixin)
|
28
49
|
klass.module_eval(&block) if block
|
29
50
|
klass
|
30
51
|
end
|
31
52
|
|
53
|
+
def define_derived_attribute(name, &blk)
|
54
|
+
positionals.delete(name)
|
55
|
+
defaults.delete(name)
|
56
|
+
derived_attributes.update(name => true) do |_key, _old,|
|
57
|
+
raise DuplicateDefinitionError, "Redefinition of derived attribute #{name}"
|
58
|
+
end
|
59
|
+
klass.module_eval(&_define_derived_attribute(name, &blk))
|
60
|
+
end
|
61
|
+
|
32
62
|
def init(data_class, **params)
|
33
63
|
_init(data_class, defaults.merge(params))
|
34
64
|
end
|
35
65
|
|
36
66
|
def to_hash(data_class_instance)
|
37
|
-
|
38
|
-
.
|
39
|
-
.to_h
|
67
|
+
all_attributes
|
68
|
+
.inject({}) { |result, (k, _)| result.merge(k => data_class_instance[k]) }
|
40
69
|
end
|
41
70
|
|
42
71
|
def update!(with_positionals, with_keywords)
|
@@ -46,12 +75,13 @@ module Lab42
|
|
46
75
|
|
47
76
|
private
|
48
77
|
def initialize(*args, **kwds, &blk)
|
49
|
-
@klass = if
|
78
|
+
@klass = if Module === args.first
|
50
79
|
args.shift
|
51
80
|
else
|
52
81
|
Class.new
|
53
82
|
end
|
54
83
|
|
84
|
+
@klass_defined = false
|
55
85
|
@block = blk
|
56
86
|
defaults.update(kwds)
|
57
87
|
positionals.push(*args)
|
@@ -64,6 +94,12 @@ module Lab42
|
|
64
94
|
end
|
65
95
|
end
|
66
96
|
|
97
|
+
def _define_derived_attribute(name, &blk)
|
98
|
+
->(*) do
|
99
|
+
define_method(name) { blk.call(self) }
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
67
103
|
def _define_freezing_constructor
|
68
104
|
->(*) do
|
69
105
|
define_method :new do |*a, **p, &b|
|
@@ -93,8 +129,8 @@ module Lab42
|
|
93
129
|
end
|
94
130
|
|
95
131
|
def _define_methods
|
96
|
-
|
97
|
-
|
132
|
+
_define_singleton_methods(klass.singleton_class)
|
133
|
+
klass.module_eval(&_define_access)
|
98
134
|
klass.module_eval(&_define_to_h)
|
99
135
|
klass.module_eval(&_define_merge)
|
100
136
|
end
|
@@ -103,9 +139,19 @@ module Lab42
|
|
103
139
|
singleton.module_eval(&_define_freezing_constructor)
|
104
140
|
singleton.module_eval(&_define_to_proc)
|
105
141
|
singleton.module_eval(&_define_with_constraint)
|
142
|
+
singleton.module_eval(&_define_derived)
|
106
143
|
singleton.module_eval(&_define_with_validations)
|
107
144
|
end
|
108
145
|
|
146
|
+
def _define_access
|
147
|
+
proxy = self
|
148
|
+
->(*) do
|
149
|
+
define_method :[] do |key|
|
150
|
+
proxy.access(self, key)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
109
155
|
def _define_to_h
|
110
156
|
proxy = self
|
111
157
|
->(*) do
|
data/lib/lab42/data_class.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative './data_class/constraint_error'
|
4
|
+
require_relative './data_class/duplicate_definition_error'
|
4
5
|
require_relative './data_class/kernel'
|
5
6
|
require_relative './data_class/validation_error'
|
6
7
|
require_relative './data_class/proxy'
|
@@ -9,10 +10,18 @@ require_relative './triple'
|
|
9
10
|
|
10
11
|
module Lab42
|
11
12
|
module DataClass
|
12
|
-
def self.extended(
|
13
|
-
|
13
|
+
def self.extended(extendee)
|
14
|
+
base_proxy =
|
15
|
+
extendee
|
16
|
+
.ancestors
|
17
|
+
.grep(self)
|
18
|
+
.drop(1)
|
19
|
+
.first
|
20
|
+
&.__data_class_proxy__
|
14
21
|
|
15
|
-
|
22
|
+
proxy = base_proxy ? Proxy.from_parent(base_proxy, extendee) : Proxy.new(extendee)
|
23
|
+
|
24
|
+
extendee.module_eval do
|
16
25
|
define_singleton_method(:__data_class_proxy__){ proxy }
|
17
26
|
end
|
18
27
|
end
|
@@ -24,10 +33,16 @@ module Lab42
|
|
24
33
|
end
|
25
34
|
end
|
26
35
|
|
36
|
+
def derive(att_name, &blk)
|
37
|
+
__data_class_proxy__.define_derived_attribute(att_name, &blk)
|
38
|
+
__data_class_proxy__.define_class!
|
39
|
+
end
|
40
|
+
|
27
41
|
def constraint(member, constraint = nil, &block)
|
28
42
|
raise ArgumentError, "must not provide constraint (2nd argument) and a block" if block && constraint
|
29
43
|
|
30
44
|
__data_class_proxy__.define_constraints(member => constraint || block)
|
45
|
+
__data_class_proxy__.define_class!
|
31
46
|
end
|
32
47
|
end
|
33
48
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lab42_data_class
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Robert Dober
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-02-
|
11
|
+
date: 2022-02-27 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: |
|
14
14
|
An Immutable DataClass for Ruby
|
@@ -25,10 +25,12 @@ files:
|
|
25
25
|
- README.md
|
26
26
|
- lib/lab42/data_class.rb
|
27
27
|
- lib/lab42/data_class/constraint_error.rb
|
28
|
+
- lib/lab42/data_class/duplicate_definition_error.rb
|
28
29
|
- lib/lab42/data_class/kernel.rb
|
29
30
|
- lib/lab42/data_class/proxy.rb
|
30
31
|
- lib/lab42/data_class/proxy/constraints.rb
|
31
32
|
- lib/lab42/data_class/proxy/constraints/maker.rb
|
33
|
+
- lib/lab42/data_class/proxy/derived.rb
|
32
34
|
- lib/lab42/data_class/proxy/memos.rb
|
33
35
|
- lib/lab42/data_class/proxy/mixin.rb
|
34
36
|
- lib/lab42/data_class/proxy/validations.rb
|