lab42_data_class 0.6.0 → 0.7.2
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 -402
- data/lib/lab42/data_class/builtin_constraints.rb +14 -0
- data/lib/lab42/data_class/constraint.rb +30 -0
- data/lib/lab42/data_class/constraints/kernel.rb +86 -0
- data/lib/lab42/data_class/duplicate_definition_error.rb +9 -0
- data/lib/lab42/data_class/proxy/constraints/maker.rb +13 -4
- data/lib/lab42/data_class/proxy/constraints.rb +2 -2
- data/lib/lab42/data_class/proxy/derived.rb +21 -0
- data/lib/lab42/data_class/proxy/memos.rb +12 -0
- data/lib/lab42/data_class/proxy.rb +90 -16
- data/lib/lab42/data_class/undefined_attribute_error.rb +9 -0
- data/lib/lab42/data_class/version.rb +1 -1
- data/lib/lab42/data_class.rb +19 -3
- metadata +11 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 746ce73ef4464de258f683f0d8366f15799cb8e3ea34a062cc6f4deb34f0a60d
|
|
4
|
+
data.tar.gz: 96c679474b72b872f4885ab2c98f7ddfd5f914268b3a9da5ba59f7ae026e5eed
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 184dc66df2185e0869a5cbce9811c8bedf685123037fadf8d17c30bfd762884b9f0d0756bc0d43787dce2586c8094bb2a798aba9b54087198b59a1a0d95023c0
|
|
7
|
+
data.tar.gz: 859c555137b976c4885157c5b39d42393c528adf94abb3dd28b9cbd3e38625d3bccac9b5cf5e317cdf3dddb08494fa989dd11523902f79402ed915822d616a64
|
data/README.md
CHANGED
|
@@ -8,490 +8,174 @@
|
|
|
8
8
|
|
|
9
9
|
# Lab42::DataClass
|
|
10
10
|
|
|
11
|
-
|
|
12
11
|
An Immutable DataClass for Ruby
|
|
13
12
|
|
|
14
|
-
Exposes a class factory function `Kernel::DataClass` and a
|
|
15
|
-
|
|
16
|
-
`Triple`
|
|
17
|
-
|
|
18
|
-
## Usage
|
|
19
|
-
|
|
20
|
-
```sh
|
|
21
|
-
gem install lab42_data_class
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
With bundler
|
|
25
|
-
|
|
26
|
-
```ruby
|
|
27
|
-
gem 'lab42_data_class'
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
In your code
|
|
31
|
-
|
|
32
|
-
```ruby
|
|
33
|
-
require 'lab42/data_class'
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
## So what does it do?
|
|
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
|
-
```
|
|
13
|
+
Exposes a class factory function `Kernel::DataClass` and a module `Lab42::DataClass` which can
|
|
14
|
+
extend classes to become _Data Classes_.
|
|
191
15
|
|
|
192
|
-
|
|
16
|
+
Also exposes two _tuple_ classes, `Pair` and `Triple`
|
|
193
17
|
|
|
194
|
-
|
|
18
|
+
## Synopsis
|
|
195
19
|
|
|
196
|
-
|
|
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
|
-
```
|
|
20
|
+
Having immutable Objects has many well known advantages that I will not ponder upon in detail here.
|
|
202
21
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
```
|
|
22
|
+
One advantage which is of particular interest though is that, as every, _modification_ is in fact the
|
|
23
|
+
creation of a new object **strong contraints** on the data can **easily** be maintained, and this
|
|
24
|
+
library makes that available to the user.
|
|
217
25
|
|
|
218
|
-
|
|
219
|
-
```ruby
|
|
220
|
-
evens => {values: [_, second, *]}
|
|
221
|
-
expect(second).to eq(4)
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
#### Context: In Case Statements
|
|
26
|
+
Therefore we can summarise the features (or not so features, that is for you to decide and you to chose to use or not):
|
|
225
27
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
28
|
+
- Immutable with an Interface à la `OpenStruct`
|
|
29
|
+
- Attributes are predefined and can have **default values**
|
|
30
|
+
- Construction with _keyword arguments_, **exclusively**
|
|
31
|
+
- Conversion to `Hash` instances (if you must)
|
|
32
|
+
- Pattern matching exactly like `Hash` instances
|
|
33
|
+
- Possibility to impose **strong constraints** on attributes
|
|
34
|
+
- Predefined constraints and concise syntax for constraints
|
|
35
|
+
- Possibility to impose **arbitrary validation** (constraints on the whole object)
|
|
36
|
+
- Declaration of **dependent attributes** which are memoized (thank you _Immutability_)
|
|
37
|
+
- Inheritance with **mixin of other dataclasses** (multiple if you must)
|
|
230
38
|
|
|
231
|
-
|
|
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
|
-
```
|
|
39
|
+
## Usage
|
|
241
40
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
expect(box.new).to be_a(box)
|
|
245
|
-
expect(box === box.new).to be_truthy
|
|
41
|
+
```sh
|
|
42
|
+
gem install lab42_data_class
|
|
246
43
|
```
|
|
247
44
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
It is useful to be able to filter heterogeneous lists of `DataClass` instances by means of `&to_proc`, therefore
|
|
45
|
+
With bundler
|
|
251
46
|
|
|
252
|
-
Given two different `DataClass` objects
|
|
253
47
|
```ruby
|
|
254
|
-
|
|
255
|
-
let(:class2) { DataClass :value }
|
|
48
|
+
gem 'lab42_data_class'
|
|
256
49
|
```
|
|
257
50
|
|
|
258
|
-
|
|
259
|
-
```ruby
|
|
260
|
-
let(:list) {[class1.new(value: 1), class2.new(value: 2), class1.new(value: 3)]}
|
|
261
|
-
```
|
|
51
|
+
In your code
|
|
262
52
|
|
|
263
|
-
Then we can filter
|
|
264
53
|
```ruby
|
|
265
|
-
|
|
54
|
+
require 'lab42/data_class'
|
|
266
55
|
```
|
|
267
56
|
|
|
268
|
-
|
|
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
|
|
57
|
+
## Speculations (literate specs)
|
|
272
58
|
|
|
273
|
-
|
|
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
|
-
```
|
|
59
|
+
The following specs are executed with the [speculate about](https://github.com/RobertDober/speculate_about) gem.
|
|
283
60
|
|
|
284
|
-
|
|
61
|
+
Given that we have imported the `Lab42` namespace
|
|
285
62
|
```ruby
|
|
286
|
-
|
|
63
|
+
DataClass = Lab42::DataClass
|
|
287
64
|
```
|
|
288
65
|
|
|
289
|
-
|
|
66
|
+
## Context: Data Classes
|
|
290
67
|
|
|
291
|
-
|
|
68
|
+
### Basic Use Case
|
|
292
69
|
|
|
293
|
-
Given a
|
|
70
|
+
Given a simple Data Class
|
|
294
71
|
```ruby
|
|
295
|
-
|
|
296
|
-
DataClass
|
|
72
|
+
class SimpleDataClass
|
|
73
|
+
extend DataClass
|
|
74
|
+
attributes :a, :b
|
|
297
75
|
end
|
|
298
76
|
```
|
|
299
77
|
|
|
300
|
-
|
|
78
|
+
And an instance of it
|
|
301
79
|
```ruby
|
|
302
|
-
|
|
303
|
-
expect(switch.new.merge(on: true).on).to eq(true)
|
|
80
|
+
let(:simple_instance) { SimpleDataClass.new(a: 1, b: 2) }
|
|
304
81
|
```
|
|
305
82
|
|
|
306
|
-
|
|
83
|
+
Then we access the fields
|
|
307
84
|
```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")
|
|
85
|
+
expect(simple_instance.a).to eq(1)
|
|
86
|
+
expect(simple_instance.b).to eq(2)
|
|
312
87
|
```
|
|
313
88
|
|
|
314
|
-
And
|
|
89
|
+
And we convert to a hash
|
|
315
90
|
```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}/)
|
|
91
|
+
expect(simple_instance.to_h).to eq(a: 1, b: 2)
|
|
322
92
|
```
|
|
323
93
|
|
|
324
|
-
And
|
|
94
|
+
And we can derive new instances
|
|
325
95
|
```ruby
|
|
326
|
-
|
|
327
|
-
|
|
96
|
+
new_instance = simple_instance.merge(b: 3)
|
|
97
|
+
expect(new_instance.to_h).to eq(a: 1, b: 3)
|
|
98
|
+
expect(simple_instance.to_h).to eq(a: 1, b: 2)
|
|
328
99
|
```
|
|
329
100
|
|
|
101
|
+
For detailed speculations please see [here](speculations/DATA_CLASSES.md)
|
|
330
102
|
|
|
331
|
-
|
|
103
|
+
## Context: `DataClass` function
|
|
332
104
|
|
|
333
|
-
|
|
334
|
-
it
|
|
105
|
+
As seen in the speculations above it seems appropriate to declare a `Class` and
|
|
106
|
+
extend it as we will add quite some code for constraints, derived attributes and validations.
|
|
335
107
|
|
|
336
|
-
|
|
337
|
-
```ruby
|
|
338
|
-
let(:constraint_error) { Lab42::DataClass::ConstraintError }
|
|
339
|
-
let(:positive) { DataClass(:value) }
|
|
340
|
-
```
|
|
108
|
+
However a more concise _Factory Function_ might still be very useful in some use cases...
|
|
341
109
|
|
|
342
|
-
|
|
110
|
+
Enter `Kernel::DataClass` **The Function**
|
|
343
111
|
|
|
344
|
-
|
|
112
|
+
### Context: Just Attributes
|
|
345
113
|
|
|
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
|
-
```
|
|
114
|
+
If there are no _Constraints_, _Derived Attributes_, _Validation_ or _Inheritance_ this concise syntax
|
|
115
|
+
might easily be preferred by many:
|
|
353
116
|
|
|
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`
|
|
117
|
+
Given some example instances like these
|
|
358
118
|
```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)
|
|
119
|
+
let(:my_data_class) { DataClass(:name, email: nil) }
|
|
120
|
+
let(:my_instance) { my_data_class.new(name: "robert") }
|
|
363
121
|
```
|
|
364
122
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
##### Membership
|
|
368
|
-
|
|
369
|
-
And this works with a `Set`
|
|
123
|
+
Then we can access its fields
|
|
370
124
|
```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)
|
|
125
|
+
expect(my_instance.name).to eq("robert")
|
|
126
|
+
expect(my_instance[:email]).to be_nil
|
|
375
127
|
```
|
|
376
128
|
|
|
377
|
-
|
|
129
|
+
But we cannot access undefined fields
|
|
378
130
|
```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)
|
|
131
|
+
expect{ my_instance.undefined }.to raise_error(NoMethodError)
|
|
383
132
|
```
|
|
384
133
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
This seems quite obvious, and of course it works
|
|
388
|
-
|
|
389
|
-
Then we can also have a regex based constraint
|
|
134
|
+
And this is even true for the `[]` syntax
|
|
390
135
|
```ruby
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
expect(vowel.new(word: "alpha").word).to eq("alpha")
|
|
394
|
-
expect{vowel.new(word: "krk")}.to raise_error(constraint_error)
|
|
136
|
+
expect{ my_instance[:undefined] }.to raise_error(KeyError)
|
|
395
137
|
```
|
|
396
138
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
Then we can also use instance methods to implement our `Positive`
|
|
139
|
+
And we need to provide values to fields without defaults
|
|
401
140
|
```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)
|
|
141
|
+
expect{ my_data_class.new(email: "some@mail.org") }
|
|
142
|
+
.to raise_error(ArgumentError, "missing initializers for [:name]")
|
|
406
143
|
```
|
|
407
|
-
|
|
408
|
-
Or we can use methods to implement it
|
|
144
|
+
And we can extract the values
|
|
409
145
|
```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)
|
|
146
|
+
expect(my_instance.to_h).to eq(name: "robert", email: nil)
|
|
414
147
|
```
|
|
415
148
|
|
|
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
|
-
```
|
|
149
|
+
#### Context: Immutable → self
|
|
426
150
|
|
|
427
|
-
Then
|
|
151
|
+
Then `my_instance` is frozen:
|
|
428
152
|
```ruby
|
|
429
|
-
expect
|
|
430
|
-
.to raise_error(validation_error)
|
|
153
|
+
expect(my_instance).to be_frozen
|
|
431
154
|
```
|
|
432
|
-
|
|
433
|
-
But as validation might need more than the default values we will not execute them at compile time
|
|
155
|
+
And we cannot even mute `my_instance` by means of metaprogramming
|
|
434
156
|
```ruby
|
|
435
|
-
expect{
|
|
436
|
-
.to_not raise_error
|
|
157
|
+
expect{ my_instance.instance_variable_set("@x", nil) }.to raise_error(FrozenError)
|
|
437
158
|
```
|
|
438
159
|
|
|
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
|
-
```
|
|
160
|
+
#### Context: Immutable → Cloning
|
|
446
161
|
|
|
447
|
-
|
|
162
|
+
Given
|
|
448
163
|
```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)
|
|
164
|
+
let(:other_instance) { my_instance.merge(email: "robert@mail.provider") }
|
|
454
165
|
```
|
|
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`
|
|
166
|
+
Then we have a new instance with the old instance unchanged
|
|
462
167
|
```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) }
|
|
168
|
+
expect(other_instance.to_h).to eq(name: "robert", email: "robert@mail.provider")
|
|
169
|
+
expect(my_instance.to_h).to eq(name: "robert", email: nil)
|
|
475
170
|
```
|
|
476
|
-
|
|
477
|
-
Then we can observe that instances of such a class
|
|
171
|
+
And the new instance is frozen again
|
|
478
172
|
```ruby
|
|
479
|
-
expect(
|
|
480
|
-
expect(my_vip.to_h).to eq(age: 42, member: true)
|
|
481
|
-
expect(my_instance.member).to be_falsy
|
|
173
|
+
expect(other_instance).to be_frozen
|
|
482
174
|
```
|
|
483
175
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
expect{my_instance.merge(member: nil)}
|
|
487
|
-
.to raise_error(constraint_error)
|
|
488
|
-
```
|
|
176
|
+
For speculations how to add all the other features to the _Factory Function_ syntax please
|
|
177
|
+
look [here](speculations/FACTORY_FUNCTION.md)
|
|
489
178
|
|
|
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
179
|
|
|
496
180
|
|
|
497
181
|
## Context: `Pair` and `Triple`
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lab42/data_class"
|
|
4
|
+
module Lab42
|
|
5
|
+
module DataClass
|
|
6
|
+
class Constraint
|
|
7
|
+
attr_reader :name, :function
|
|
8
|
+
|
|
9
|
+
def call(value)
|
|
10
|
+
function.(value)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_s
|
|
14
|
+
"Constraint<#{name}>"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
def initialize(name:, function:)
|
|
19
|
+
raise ArgumentError, "name not a String, but #{name}" unless String === name
|
|
20
|
+
unless function.respond_to?(:arity) && function.arity == 1
|
|
21
|
+
raise ArgumentError, "function not a callable with arity 1 #{function}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
@name = name
|
|
25
|
+
@function = function
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kernel
|
|
4
|
+
Constraint = Lab42::DataClass::Constraint
|
|
5
|
+
Maker = Lab42::DataClass::Proxy::Constraints::Maker # TODO: Move Maker to Lab42::DataClass:ConstraintMaker
|
|
6
|
+
Anything = Constraint.new(name: "Anything", function: ->(_) { true })
|
|
7
|
+
Boolean = Constraint.new(name: "Boolean", function: -> { [false, true].member?(_1) })
|
|
8
|
+
|
|
9
|
+
def All?(constraint = nil, &blk)
|
|
10
|
+
constraint = Maker.make_constraint(constraint, &blk)
|
|
11
|
+
f = -> do
|
|
12
|
+
_1.all?(&constraint)
|
|
13
|
+
end
|
|
14
|
+
Constraint.new(name: "All?(#{constraint})", function: f)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def Any?(constraint = nil, &blk)
|
|
18
|
+
constraint = Maker.make_constraint(constraint, &blk)
|
|
19
|
+
f = -> do
|
|
20
|
+
_1.any?(&constraint)
|
|
21
|
+
end
|
|
22
|
+
Constraint.new(name: "Any?(#{constraint})", function: f)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def Choice(*constraints)
|
|
26
|
+
constraints = constraints.map{ Maker.make_constraint _1 }
|
|
27
|
+
f = ->(value) do
|
|
28
|
+
constraints.any?{ _1.(value) }
|
|
29
|
+
end
|
|
30
|
+
Constraint.new(name: "Choice(#{constraints.join(', ')})", function: f)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def Contains(str)
|
|
34
|
+
f = -> { _1.include?(str) rescue false }
|
|
35
|
+
Constraint.new(name: "Contains(#{str})", function: f)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def EndsWith(str)
|
|
39
|
+
f = -> { _1.end_with?(str) rescue false }
|
|
40
|
+
Constraint.new(name: "EndsWith(#{str})", function: f)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def Lambda(arity)
|
|
44
|
+
f = -> do
|
|
45
|
+
_1.arity == arity rescue false
|
|
46
|
+
end
|
|
47
|
+
Constraint.new(name: "Lambda(#{arity})", function: f)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def NilOr(constraint = nil, &blk)
|
|
51
|
+
constraint = Maker.make_constraint(constraint, &blk)
|
|
52
|
+
f = -> { _1.nil? || constraint.(_1) }
|
|
53
|
+
Constraint.new(name: "NilOr(#{constraint})", function: f)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def Not(constraint = nil, &blk)
|
|
57
|
+
constraint = Maker.make_constraint(constraint, &blk)
|
|
58
|
+
f = -> { !constraint.(_1) }
|
|
59
|
+
Constraint.new(name: "Not(#{constraint})", function: f)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def PairOf(fst, snd)
|
|
63
|
+
fst_constraint = Maker.make_constraint(fst)
|
|
64
|
+
snd_constraint = Maker.make_constraint(snd)
|
|
65
|
+
f = -> do
|
|
66
|
+
Lab42::Pair === _1 && fst_constraint.(_1.first) && snd_constraint.(_1.second)
|
|
67
|
+
end
|
|
68
|
+
Constraint.new(name: "PairOf(#{fst_constraint}, #{snd_constraint})", function: f)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def StartsWith(str)
|
|
72
|
+
f = -> { _1.start_with?(str) rescue false }
|
|
73
|
+
Constraint.new(name: "StartsWith(#{str})", function: f)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def TripleOf(fst, snd, trd)
|
|
77
|
+
fst_constraint = Maker.make_constraint(fst)
|
|
78
|
+
snd_constraint = Maker.make_constraint(snd)
|
|
79
|
+
trd_constraint = Maker.make_constraint(trd)
|
|
80
|
+
f = -> do
|
|
81
|
+
Lab42::Triple === _1 && fst_constraint.(_1.first) && snd_constraint.(_1.second) && trd_constraint.(_1.third)
|
|
82
|
+
end
|
|
83
|
+
Constraint.new(name: "TripleOf(#{fst_constraint}, #{snd_constraint}, #{trd_constraint})", function: f)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "lab42/data_class/constraint"
|
|
3
4
|
module Lab42
|
|
4
5
|
module DataClass
|
|
5
6
|
class Proxy
|
|
@@ -7,9 +8,17 @@ module Lab42
|
|
|
7
8
|
module Maker
|
|
8
9
|
extend self
|
|
9
10
|
|
|
10
|
-
def make_constraint(constraint)
|
|
11
|
+
def make_constraint(constraint, &blk)
|
|
12
|
+
raise ArgumentError, "must not pass a callable #{constraint} and a block" if constraint && blk
|
|
13
|
+
|
|
14
|
+
_make_constraint(constraint || blk)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def _make_constraint(constraint)
|
|
11
20
|
case constraint
|
|
12
|
-
when Proc, Method
|
|
21
|
+
when Lab42::DataClass::Constraint, Proc, Method
|
|
13
22
|
constraint
|
|
14
23
|
when Symbol
|
|
15
24
|
-> { _1.send(constraint) }
|
|
@@ -19,13 +28,13 @@ module Lab42
|
|
|
19
28
|
-> { constraint.match?(_1) }
|
|
20
29
|
when UnboundMethod
|
|
21
30
|
-> { constraint.bind(_1).() }
|
|
31
|
+
when Module
|
|
32
|
+
-> { constraint === _1 }
|
|
22
33
|
else
|
|
23
34
|
_make_member_constraint(constraint)
|
|
24
35
|
end
|
|
25
36
|
end
|
|
26
37
|
|
|
27
|
-
private
|
|
28
|
-
|
|
29
38
|
def _make_member_constraint(constraint)
|
|
30
39
|
if constraint.respond_to?(:member?)
|
|
31
40
|
-> { constraint.member?(_1) }
|
|
@@ -14,7 +14,7 @@ module Lab42
|
|
|
14
14
|
|
|
15
15
|
def define_constraint
|
|
16
16
|
->((attr, constraint)) do
|
|
17
|
-
if members
|
|
17
|
+
if members!.member?(attr)
|
|
18
18
|
constraints[attr] << Maker.make_constraint(constraint)
|
|
19
19
|
nil
|
|
20
20
|
else
|
|
@@ -26,7 +26,7 @@ module Lab42
|
|
|
26
26
|
def define_constraints(constraints)
|
|
27
27
|
errors = constraints.map(&define_constraint).compact
|
|
28
28
|
unless errors.empty?
|
|
29
|
-
raise
|
|
29
|
+
raise UndefinedAttributeError,
|
|
30
30
|
"constraints cannot be defined for undefined attributes #{errors.inspect}"
|
|
31
31
|
end
|
|
32
32
|
|
|
@@ -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,12 +16,20 @@ 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)
|
|
18
26
|
end
|
|
19
27
|
end
|
|
20
28
|
|
|
29
|
+
def members!
|
|
30
|
+
@__members__ = Set.new(positionals + defaults.keys)
|
|
31
|
+
end
|
|
32
|
+
|
|
21
33
|
def positionals
|
|
22
34
|
@__positionals__ ||= []
|
|
23
35
|
end
|
|
@@ -2,41 +2,73 @@
|
|
|
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
|
|
14
15
|
|
|
15
|
-
def
|
|
16
|
-
|
|
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
|
|
32
|
+
|
|
33
|
+
def check!(params, merge_with = defaults)
|
|
17
34
|
raise ArgumentError, "missing initializers for #{_missing_initializers}" unless _missing_initializers.empty?
|
|
18
35
|
raise ArgumentError, "illegal initializers #{_illegal_initializers}" unless _illegal_initializers.empty?
|
|
19
36
|
|
|
20
|
-
_check_constraints!(
|
|
37
|
+
_check_constraints!(merge_with.merge(params))
|
|
21
38
|
end
|
|
22
39
|
|
|
23
40
|
def define_class!
|
|
41
|
+
return if @klass_defined
|
|
42
|
+
|
|
43
|
+
@klass_defined = true
|
|
24
44
|
klass.module_eval(&_define_attr_reader)
|
|
25
|
-
klass.module_eval(&_define_initializer)
|
|
45
|
+
klass.module_eval(&_define_initializer) if Class === klass
|
|
26
46
|
_define_methods
|
|
27
47
|
klass.include(Mixin)
|
|
28
48
|
klass.module_eval(&block) if block
|
|
29
49
|
klass
|
|
30
50
|
end
|
|
31
51
|
|
|
52
|
+
def define_derived_attribute(name, &blk)
|
|
53
|
+
positionals.delete(name)
|
|
54
|
+
defaults.delete(name)
|
|
55
|
+
derived_attributes.update(name => true) do |_key, _old,|
|
|
56
|
+
raise DuplicateDefinitionError, "Redefinition of derived attribute #{name.inspect}"
|
|
57
|
+
end
|
|
58
|
+
klass.module_eval(&_define_derived_attribute(name, &blk))
|
|
59
|
+
end
|
|
60
|
+
|
|
32
61
|
def init(data_class, **params)
|
|
33
62
|
_init(data_class, defaults.merge(params))
|
|
34
63
|
end
|
|
35
64
|
|
|
65
|
+
def set_actual_params(params)
|
|
66
|
+
@actual_params = params
|
|
67
|
+
end
|
|
68
|
+
|
|
36
69
|
def to_hash(data_class_instance)
|
|
37
|
-
|
|
38
|
-
.
|
|
39
|
-
.to_h
|
|
70
|
+
all_attributes
|
|
71
|
+
.inject({}) { |result, (k, _)| result.merge(k => data_class_instance[k]) }
|
|
40
72
|
end
|
|
41
73
|
|
|
42
74
|
def update!(with_positionals, with_keywords)
|
|
@@ -46,12 +78,13 @@ module Lab42
|
|
|
46
78
|
|
|
47
79
|
private
|
|
48
80
|
def initialize(*args, **kwds, &blk)
|
|
49
|
-
@klass = if
|
|
81
|
+
@klass = if Module === args.first
|
|
50
82
|
args.shift
|
|
51
83
|
else
|
|
52
84
|
Class.new
|
|
53
85
|
end
|
|
54
86
|
|
|
87
|
+
@klass_defined = false
|
|
55
88
|
@block = blk
|
|
56
89
|
defaults.update(kwds)
|
|
57
90
|
positionals.push(*args)
|
|
@@ -64,11 +97,42 @@ module Lab42
|
|
|
64
97
|
end
|
|
65
98
|
end
|
|
66
99
|
|
|
100
|
+
def _define_derived_attribute(name, &blk)
|
|
101
|
+
->(*) do
|
|
102
|
+
if instance_methods.include?(name)
|
|
103
|
+
begin
|
|
104
|
+
remove_method(name)
|
|
105
|
+
rescue StandardError
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
define_method(name) { blk.call(self) }
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
67
113
|
def _define_freezing_constructor
|
|
114
|
+
proxy = self
|
|
115
|
+
->(*) do
|
|
116
|
+
define_method :new do |**params, &b|
|
|
117
|
+
allocate.tap do |o|
|
|
118
|
+
proxy.set_actual_params(params)
|
|
119
|
+
proxy.check!(params)
|
|
120
|
+
o.send(:initialize, **params, &b)
|
|
121
|
+
end.freeze
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def _define_merging_constructor
|
|
127
|
+
proxy = self
|
|
68
128
|
->(*) do
|
|
69
|
-
define_method :
|
|
70
|
-
|
|
129
|
+
define_method :_new_from_merge do |new_params, params|
|
|
130
|
+
allocate.tap do |o|
|
|
131
|
+
proxy.check!(new_params, {})
|
|
132
|
+
o.send(:initialize, **params)
|
|
133
|
+
end.freeze
|
|
71
134
|
end
|
|
135
|
+
private :_new_from_merge
|
|
72
136
|
end
|
|
73
137
|
end
|
|
74
138
|
|
|
@@ -76,7 +140,6 @@ module Lab42
|
|
|
76
140
|
proxy = self
|
|
77
141
|
->(*) do
|
|
78
142
|
define_method :initialize do |**params|
|
|
79
|
-
proxy.check!(**params)
|
|
80
143
|
proxy.init(self, **params)
|
|
81
144
|
proxy.validate!(self)
|
|
82
145
|
end
|
|
@@ -87,25 +150,36 @@ module Lab42
|
|
|
87
150
|
->(*) do
|
|
88
151
|
define_method :merge do |**params|
|
|
89
152
|
values = to_h.merge(params)
|
|
90
|
-
self.class.
|
|
153
|
+
self.class.send(:_new_from_merge, params, values)
|
|
91
154
|
end
|
|
92
155
|
end
|
|
93
156
|
end
|
|
94
157
|
|
|
95
158
|
def _define_methods
|
|
96
|
-
|
|
97
|
-
|
|
159
|
+
_define_singleton_methods(klass.singleton_class)
|
|
160
|
+
klass.module_eval(&_define_access)
|
|
98
161
|
klass.module_eval(&_define_to_h)
|
|
99
162
|
klass.module_eval(&_define_merge)
|
|
100
163
|
end
|
|
101
164
|
|
|
102
165
|
def _define_singleton_methods(singleton)
|
|
103
166
|
singleton.module_eval(&_define_freezing_constructor)
|
|
167
|
+
singleton.module_eval(&_define_merging_constructor)
|
|
104
168
|
singleton.module_eval(&_define_to_proc)
|
|
105
169
|
singleton.module_eval(&_define_with_constraint)
|
|
170
|
+
singleton.module_eval(&_define_derived)
|
|
106
171
|
singleton.module_eval(&_define_with_validations)
|
|
107
172
|
end
|
|
108
173
|
|
|
174
|
+
def _define_access
|
|
175
|
+
proxy = self
|
|
176
|
+
->(*) do
|
|
177
|
+
define_method :[] do |key|
|
|
178
|
+
proxy.access(self, key)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
109
183
|
def _define_to_h
|
|
110
184
|
proxy = self
|
|
111
185
|
->(*) do
|
data/lib/lab42/data_class.rb
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
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'
|
|
6
|
+
require_relative './data_class/undefined_attribute_error'
|
|
5
7
|
require_relative './data_class/validation_error'
|
|
6
8
|
require_relative './data_class/proxy'
|
|
7
9
|
require_relative './pair'
|
|
@@ -9,10 +11,18 @@ require_relative './triple'
|
|
|
9
11
|
|
|
10
12
|
module Lab42
|
|
11
13
|
module DataClass
|
|
12
|
-
def self.extended(
|
|
13
|
-
|
|
14
|
+
def self.extended(extendee)
|
|
15
|
+
base_proxy =
|
|
16
|
+
extendee
|
|
17
|
+
.ancestors
|
|
18
|
+
.grep(self)
|
|
19
|
+
.drop(1)
|
|
20
|
+
.first
|
|
21
|
+
&.__data_class_proxy__
|
|
14
22
|
|
|
15
|
-
|
|
23
|
+
proxy = base_proxy ? Proxy.from_parent(base_proxy, extendee) : Proxy.new(extendee)
|
|
24
|
+
|
|
25
|
+
extendee.module_eval do
|
|
16
26
|
define_singleton_method(:__data_class_proxy__){ proxy }
|
|
17
27
|
end
|
|
18
28
|
end
|
|
@@ -24,10 +34,16 @@ module Lab42
|
|
|
24
34
|
end
|
|
25
35
|
end
|
|
26
36
|
|
|
37
|
+
def derive(att_name, &blk)
|
|
38
|
+
__data_class_proxy__.define_derived_attribute(att_name, &blk)
|
|
39
|
+
__data_class_proxy__.define_class!
|
|
40
|
+
end
|
|
41
|
+
|
|
27
42
|
def constraint(member, constraint = nil, &block)
|
|
28
43
|
raise ArgumentError, "must not provide constraint (2nd argument) and a block" if block && constraint
|
|
29
44
|
|
|
30
45
|
__data_class_proxy__.define_constraints(member => constraint || block)
|
|
46
|
+
__data_class_proxy__.define_class!
|
|
31
47
|
end
|
|
32
48
|
end
|
|
33
49
|
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.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Robert Dober
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2022-
|
|
11
|
+
date: 2022-03-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: |
|
|
14
14
|
An Immutable DataClass for Ruby
|
|
@@ -24,14 +24,20 @@ files:
|
|
|
24
24
|
- LICENSE
|
|
25
25
|
- README.md
|
|
26
26
|
- lib/lab42/data_class.rb
|
|
27
|
+
- lib/lab42/data_class/builtin_constraints.rb
|
|
28
|
+
- lib/lab42/data_class/constraint.rb
|
|
27
29
|
- lib/lab42/data_class/constraint_error.rb
|
|
30
|
+
- lib/lab42/data_class/constraints/kernel.rb
|
|
31
|
+
- lib/lab42/data_class/duplicate_definition_error.rb
|
|
28
32
|
- lib/lab42/data_class/kernel.rb
|
|
29
33
|
- lib/lab42/data_class/proxy.rb
|
|
30
34
|
- lib/lab42/data_class/proxy/constraints.rb
|
|
31
35
|
- lib/lab42/data_class/proxy/constraints/maker.rb
|
|
36
|
+
- lib/lab42/data_class/proxy/derived.rb
|
|
32
37
|
- lib/lab42/data_class/proxy/memos.rb
|
|
33
38
|
- lib/lab42/data_class/proxy/mixin.rb
|
|
34
39
|
- lib/lab42/data_class/proxy/validations.rb
|
|
40
|
+
- lib/lab42/data_class/undefined_attribute_error.rb
|
|
35
41
|
- lib/lab42/data_class/validation_error.rb
|
|
36
42
|
- lib/lab42/data_class/version.rb
|
|
37
43
|
- lib/lab42/eq_and_patterns.rb
|
|
@@ -41,7 +47,7 @@ homepage: https://github.com/robertdober/lab42_data_class
|
|
|
41
47
|
licenses:
|
|
42
48
|
- Apache-2.0
|
|
43
49
|
metadata: {}
|
|
44
|
-
post_install_message:
|
|
50
|
+
post_install_message:
|
|
45
51
|
rdoc_options: []
|
|
46
52
|
require_paths:
|
|
47
53
|
- lib
|
|
@@ -57,7 +63,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
57
63
|
version: '0'
|
|
58
64
|
requirements: []
|
|
59
65
|
rubygems_version: 3.3.3
|
|
60
|
-
signing_key:
|
|
66
|
+
signing_key:
|
|
61
67
|
specification_version: 4
|
|
62
68
|
summary: Finally a dataclass in ruby
|
|
63
69
|
test_files: []
|