lab42_data_class 0.4.1 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +215 -1
- data/lib/lab42/data_class/constraint_error.rb +9 -0
- data/lib/lab42/data_class/kernel.rb +17 -0
- data/lib/lab42/data_class/proxy/constraints/maker.rb +39 -0
- data/lib/lab42/data_class/proxy/constraints.rb +90 -0
- data/lib/lab42/data_class/proxy/memos.rb +44 -0
- data/lib/lab42/data_class/proxy/validations.rb +38 -0
- data/lib/lab42/data_class/proxy.rb +30 -18
- data/lib/lab42/data_class/validation_error.rb +9 -0
- data/lib/lab42/data_class/version.rb +1 -1
- data/lib/lab42/data_class.rb +23 -11
- metadata +14 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1819694c4684f3f4e377f04f4eb17d0842a1075ff7859eeef443c66dd7cfb458
|
4
|
+
data.tar.gz: 5af74933329ed924d51a1820d666c671973004edbd3ab00f6ba93378df71180f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5e14f2656f03703aa867dce065bf9e4770cf043fd97d035cab9a6b869d9fb6e9749c2de3eef0b948e02c7ade07f85f75423c4b2412ed154574a6672e28411d9f
|
7
|
+
data.tar.gz: f831c9c8094f7224de088f6f03847791bcc91a33db6e565fc1ef3f45d5f3f5e29d18b05dfbcdbc7dcc422755d0ab87dc73f0c50a9de45dcd3df0916d87fb1c16
|
data/README.md
CHANGED
@@ -8,7 +8,12 @@
|
|
8
8
|
|
9
9
|
# Lab42::DataClass
|
10
10
|
|
11
|
-
|
11
|
+
|
12
|
+
An Immutable DataClass for Ruby
|
13
|
+
|
14
|
+
Exposes a class factory function `Kernel::DataClass` and a class
|
15
|
+
modifer `Module#dataclass`, also creates two _tuple_ classes, `Pair` and
|
16
|
+
`Triple`
|
12
17
|
|
13
18
|
## Usage
|
14
19
|
|
@@ -35,6 +40,7 @@ Well let us [speculate about](https://github.com/RobertDober/speculate_about) it
|
|
35
40
|
|
36
41
|
## Context `DataClass`
|
37
42
|
|
43
|
+
|
38
44
|
### Context: `DataClass` function
|
39
45
|
|
40
46
|
Given
|
@@ -280,6 +286,214 @@ Then we can pass it as keyword arguments
|
|
280
286
|
expect(extract_value(**my_class.new)).to eq([1, base: 2])
|
281
287
|
```
|
282
288
|
|
289
|
+
### Context: Constraints
|
290
|
+
|
291
|
+
Values of attributes of a `DataClass` can have constraints
|
292
|
+
|
293
|
+
Given a `DataClass` with constraints
|
294
|
+
```ruby
|
295
|
+
let :switch do
|
296
|
+
DataClass(on: false).with_constraint(on: -> { [false, true].member? _1 })
|
297
|
+
end
|
298
|
+
```
|
299
|
+
|
300
|
+
Then boolean values are acceptable
|
301
|
+
```ruby
|
302
|
+
expect{ switch.new }.not_to raise_error
|
303
|
+
expect(switch.new.merge(on: true).on).to eq(true)
|
304
|
+
```
|
305
|
+
|
306
|
+
But we can neither construct or merge with non boolean values
|
307
|
+
```ruby
|
308
|
+
expect{ switch.new(on: nil) }
|
309
|
+
.to raise_error(Lab42::DataClass::ConstraintError, "value nil is not allowed for attribute :on")
|
310
|
+
expect{ switch.new.merge(on: 42) }
|
311
|
+
.to raise_error(Lab42::DataClass::ConstraintError, "value 42 is not allowed for attribute :on")
|
312
|
+
```
|
313
|
+
|
314
|
+
And therefore defaultless attributes cannot have a constraint that is violated by a nil value
|
315
|
+
```ruby
|
316
|
+
error_head = "constraint error during validation of default value of attribute :value"
|
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}/)
|
322
|
+
```
|
323
|
+
|
324
|
+
And defining constraints for undefined attributes is not the best of ideas
|
325
|
+
```ruby
|
326
|
+
expect { DataClass(a: 1).with_constraint(b: -> {true}) }
|
327
|
+
.to raise_error(ArgumentError, "constraints cannot be defined for undefined attributes [:b]")
|
328
|
+
```
|
329
|
+
|
330
|
+
|
331
|
+
#### Context: Convenience Constraints
|
332
|
+
|
333
|
+
Often repeating patterns are implemented as non lambda constraints, depending on the type of a constraint
|
334
|
+
it is implicitly converted to a lambda as specified below:
|
335
|
+
|
336
|
+
Given a shortcut for our `ConstraintError`
|
337
|
+
```ruby
|
338
|
+
let(:constraint_error) { Lab42::DataClass::ConstraintError }
|
339
|
+
let(:positive) { DataClass(:value) }
|
340
|
+
```
|
341
|
+
|
342
|
+
##### Symbols
|
343
|
+
|
344
|
+
... are sent to the value of the attribute, this is not very surprising of course ;)
|
345
|
+
|
346
|
+
Then a first implementation of `Positive`
|
347
|
+
```ruby
|
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
|
+
```
|
353
|
+
|
354
|
+
##### Arrays
|
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`
|
358
|
+
```ruby
|
359
|
+
positive_by_ary = positive.with_constraint(value: [:>, 0])
|
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)
|
363
|
+
```
|
364
|
+
|
365
|
+
If however we are interested in membership we have to wrap the `Array` into a `Set`
|
366
|
+
|
367
|
+
##### Membership
|
368
|
+
|
369
|
+
And this works with a `Set`
|
370
|
+
```ruby
|
371
|
+
positive_by_set = positive.with_constraint(value: Set.new([*1..10]))
|
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)
|
375
|
+
```
|
376
|
+
|
377
|
+
And also with a `Range`
|
378
|
+
```ruby
|
379
|
+
positive_by_range = positive.with_constraint(value: 1..Float::INFINITY)
|
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)
|
383
|
+
```
|
384
|
+
|
385
|
+
##### Regexen
|
386
|
+
|
387
|
+
This seems quite obvious, and of course it works
|
388
|
+
|
389
|
+
Then we can also have a regex based constraint
|
390
|
+
```ruby
|
391
|
+
vowel = DataClass(:word).with_constraint(word: /[aeiou]/)
|
392
|
+
|
393
|
+
expect(vowel.new(word: "alpha").word).to eq("alpha")
|
394
|
+
expect{vowel.new(word: "krk")}.to raise_error(constraint_error)
|
395
|
+
```
|
396
|
+
|
397
|
+
##### Other callable objects as constraints
|
398
|
+
|
399
|
+
|
400
|
+
Then we can also use instance methods to implement our `Positive`
|
401
|
+
```ruby
|
402
|
+
positive_by_instance_method = positive.with_constraint(value: Fixnum.instance_method(:positive?))
|
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)
|
406
|
+
```
|
407
|
+
|
408
|
+
Or we can use methods to implement it
|
409
|
+
```ruby
|
410
|
+
positive_by_method = positive.with_constraint(value: 0.method(:<))
|
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)
|
414
|
+
```
|
415
|
+
|
416
|
+
#### Context: Global Constraints aka __Validations__
|
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
|
+
```
|
426
|
+
|
427
|
+
Then we will get a `ValidationError` if we construct a point left of the main diagonal
|
428
|
+
```ruby
|
429
|
+
expect{ point.new(x: 0, y: 1) }
|
430
|
+
.to raise_error(validation_error)
|
431
|
+
```
|
432
|
+
|
433
|
+
But as validation might need more than the default values we will not execute them at compile time
|
434
|
+
```ruby
|
435
|
+
expect{ DataClass(x: 0, y: 0).validate{ |inst| inst.x > inst.y } }
|
436
|
+
.to_not raise_error
|
437
|
+
```
|
438
|
+
|
439
|
+
And we can name validations to get better error messages
|
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
|
+
```
|
446
|
+
|
447
|
+
And remark how bad unnamed validation errors might be
|
448
|
+
```ruby
|
449
|
+
error_message_rgx = %r{
|
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)
|
454
|
+
```
|
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`
|
462
|
+
```ruby
|
463
|
+
let :my_class do
|
464
|
+
Class.new do
|
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) }
|
475
|
+
```
|
476
|
+
|
477
|
+
Then we can observe that instances of such a class
|
478
|
+
```ruby
|
479
|
+
expect(my_instance.to_h).to eq(age: 42, member: false)
|
480
|
+
expect(my_vip.to_h).to eq(age: 42, member: true)
|
481
|
+
expect(my_instance.member).to be_falsy
|
482
|
+
```
|
483
|
+
|
484
|
+
And we will get constraint errors if applicable
|
485
|
+
```ruby
|
486
|
+
expect{my_instance.merge(member: nil)}
|
487
|
+
.to raise_error(constraint_error)
|
488
|
+
```
|
489
|
+
|
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
|
+
|
496
|
+
|
283
497
|
## Context: `Pair` and `Triple`
|
284
498
|
|
285
499
|
Two special cases of a `DataClass` which behave like `Tuple` of size 2 and 3 in _Elixir_
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kernel
|
4
|
+
def DataClass(*args, **kwds, &blk)
|
5
|
+
proxy = Lab42::DataClass::Proxy.new(*args, **kwds, &blk)
|
6
|
+
proxy.define_class!
|
7
|
+
end
|
8
|
+
|
9
|
+
def Pair(first, second)
|
10
|
+
Lab42::Pair.new(first, second)
|
11
|
+
end
|
12
|
+
|
13
|
+
def Triple(first, second, third)
|
14
|
+
Lab42::Triple.new(first, second, third)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
# SPDX-License-Identifier: Apache-2.0
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab42
|
4
|
+
module DataClass
|
5
|
+
class Proxy
|
6
|
+
module Constraints
|
7
|
+
module Maker
|
8
|
+
extend self
|
9
|
+
|
10
|
+
def make_constraint(constraint)
|
11
|
+
case constraint
|
12
|
+
when Proc, Method
|
13
|
+
constraint
|
14
|
+
when Symbol
|
15
|
+
-> { _1.send(constraint) }
|
16
|
+
when Array
|
17
|
+
-> { _1.send(*constraint) }
|
18
|
+
when Regexp
|
19
|
+
-> { constraint.match?(_1) }
|
20
|
+
when UnboundMethod
|
21
|
+
-> { constraint.bind(_1).() }
|
22
|
+
else
|
23
|
+
_make_member_constraint(constraint)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def _make_member_constraint(constraint)
|
30
|
+
if constraint.respond_to?(:member?)
|
31
|
+
-> { constraint.member?(_1) }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
# SPDX-License-Identifier: Apache-2.0
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "constraints/maker"
|
4
|
+
module Lab42
|
5
|
+
module DataClass
|
6
|
+
class Proxy
|
7
|
+
module Constraints
|
8
|
+
def check_constraints_against_defaults(constraints)
|
9
|
+
errors = constraints
|
10
|
+
.map(&_check_constraint_against_default)
|
11
|
+
.compact
|
12
|
+
raise ConstraintError, errors.join("\n\n") unless errors.empty?
|
13
|
+
end
|
14
|
+
|
15
|
+
def define_constraint
|
16
|
+
->((attr, constraint)) do
|
17
|
+
if members.member?(attr)
|
18
|
+
constraints[attr] << Maker.make_constraint(constraint)
|
19
|
+
nil
|
20
|
+
else
|
21
|
+
attr
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def define_constraints(constraints)
|
27
|
+
errors = constraints.map(&define_constraint).compact
|
28
|
+
unless errors.empty?
|
29
|
+
raise ArgumentError,
|
30
|
+
"constraints cannot be defined for undefined attributes #{errors.inspect}"
|
31
|
+
end
|
32
|
+
|
33
|
+
check_constraints_against_defaults(constraints)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def _check_constraint_against_default
|
39
|
+
->((attr, constraint)) do
|
40
|
+
if defaults.key?(attr)
|
41
|
+
_check_constraint_against_default_value(attr, defaults[attr], constraint)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def _check_constraint_against_default_value(attr, value, constraint)
|
47
|
+
unless Maker.make_constraint(constraint).(value)
|
48
|
+
"default value #{value.inspect} is not allowed for attribute #{attr.inspect}"
|
49
|
+
end
|
50
|
+
rescue StandardError => e
|
51
|
+
"constraint error during validation of default value of attribute #{attr.inspect}\n #{e.message}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def _check_constraints_for_attr!
|
55
|
+
->((k, v)) do
|
56
|
+
constraints[k]
|
57
|
+
.map(&_check_constraint!(k, v))
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def _check_constraint!(attr, value)
|
62
|
+
->(constraint) do
|
63
|
+
"value #{value.inspect} is not allowed for attribute #{attr.inspect}" unless constraint.(value)
|
64
|
+
rescue StandardError => e
|
65
|
+
"constraint error during validation of attribute #{attr.inspect}\n #{e.message}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def _check_constraints!(params)
|
70
|
+
errors = params
|
71
|
+
.flat_map(&_check_constraints_for_attr!)
|
72
|
+
.compact
|
73
|
+
|
74
|
+
raise ConstraintError, errors.join("\n\n") unless errors.empty?
|
75
|
+
end
|
76
|
+
|
77
|
+
def _define_with_constraint
|
78
|
+
proxy = self
|
79
|
+
->(*) do
|
80
|
+
define_method :with_constraint do |**constraints|
|
81
|
+
proxy.define_constraints(constraints)
|
82
|
+
self
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
# SPDX-License-Identifier: Apache-2.0
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab42
|
4
|
+
module DataClass
|
5
|
+
class Proxy
|
6
|
+
module Memos
|
7
|
+
def constraints
|
8
|
+
@__constraints__ ||= Hash.new { |h, k| h[k] = [] }
|
9
|
+
end
|
10
|
+
|
11
|
+
def defaults
|
12
|
+
@__defaults__ ||= {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def members
|
16
|
+
@__members__ ||= unless (positionals + defaults.keys).empty?
|
17
|
+
Set.new(positionals + defaults.keys)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def positionals
|
22
|
+
@__positionals__ ||= []
|
23
|
+
end
|
24
|
+
|
25
|
+
def validations
|
26
|
+
@__validations__ ||= []
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def _missing_initializers
|
32
|
+
@___missing_initializers__ ||=
|
33
|
+
positionals - actual_params.keys
|
34
|
+
end
|
35
|
+
|
36
|
+
def _illegal_initializers
|
37
|
+
@___illegal_initializers__ ||=
|
38
|
+
actual_params.keys - positionals - defaults.keys
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
# SPDX-License-Identifier: Apache-2.0
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab42
|
4
|
+
module DataClass
|
5
|
+
class Proxy
|
6
|
+
module Validations
|
7
|
+
def validate!(instance)
|
8
|
+
errors = validations
|
9
|
+
.map(&_check_validation!(instance))
|
10
|
+
.compact
|
11
|
+
|
12
|
+
raise ValidationError, errors.join("\n") unless errors.empty?
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def _define_with_validations
|
18
|
+
proxy = self
|
19
|
+
->(*) do
|
20
|
+
define_method :validate do |name = nil, &block|
|
21
|
+
proxy.validations << [name, block]
|
22
|
+
self
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def _check_validation!(instance)
|
28
|
+
->((name, validation)) do
|
29
|
+
unless validation.(instance)
|
30
|
+
name || validation
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
# SPDX-License-Identifier: Apache-2.0
|
@@ -1,16 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'set'
|
4
|
+
require_relative 'proxy/constraints'
|
5
|
+
require_relative 'proxy/memos'
|
6
|
+
require_relative 'proxy/validations'
|
4
7
|
require_relative 'proxy/mixin'
|
5
8
|
module Lab42
|
6
9
|
module DataClass
|
7
10
|
class Proxy
|
8
|
-
|
11
|
+
include Constraints, Memos, Validations
|
12
|
+
|
13
|
+
attr_reader :actual_params, :block, :klass
|
9
14
|
|
10
15
|
def check!(**params)
|
11
16
|
@actual_params = params
|
12
17
|
raise ArgumentError, "missing initializers for #{_missing_initializers}" unless _missing_initializers.empty?
|
13
18
|
raise ArgumentError, "illegal initializers #{_illegal_initializers}" unless _illegal_initializers.empty?
|
19
|
+
|
20
|
+
_check_constraints!(defaults.merge(params))
|
14
21
|
end
|
15
22
|
|
16
23
|
def define_class!
|
@@ -32,15 +39,22 @@ module Lab42
|
|
32
39
|
.to_h
|
33
40
|
end
|
34
41
|
|
42
|
+
def update!(with_positionals, with_keywords)
|
43
|
+
positionals.push(*with_positionals)
|
44
|
+
defaults.update(with_keywords)
|
45
|
+
end
|
46
|
+
|
35
47
|
private
|
36
48
|
def initialize(*args, **kwds, &blk)
|
37
|
-
@klass = Class.
|
49
|
+
@klass = if Class === args.first
|
50
|
+
args.shift
|
51
|
+
else
|
52
|
+
Class.new
|
53
|
+
end
|
38
54
|
|
39
55
|
@block = blk
|
40
|
-
|
41
|
-
|
42
|
-
# TODO: Check for all symbols and no duplicates ⇒ v0.1.1
|
43
|
-
@positionals = args
|
56
|
+
defaults.update(kwds)
|
57
|
+
positionals.push(*args)
|
44
58
|
end
|
45
59
|
|
46
60
|
def _define_attr_reader
|
@@ -64,6 +78,7 @@ module Lab42
|
|
64
78
|
define_method :initialize do |**params|
|
65
79
|
proxy.check!(**params)
|
66
80
|
proxy.init(self, **params)
|
81
|
+
proxy.validate!(self)
|
67
82
|
end
|
68
83
|
end
|
69
84
|
end
|
@@ -78,12 +93,19 @@ module Lab42
|
|
78
93
|
end
|
79
94
|
|
80
95
|
def _define_methods
|
81
|
-
|
82
|
-
|
96
|
+
class << klass; self end
|
97
|
+
.tap { |singleton| _define_singleton_methods(singleton) }
|
83
98
|
klass.module_eval(&_define_to_h)
|
84
99
|
klass.module_eval(&_define_merge)
|
85
100
|
end
|
86
101
|
|
102
|
+
def _define_singleton_methods(singleton)
|
103
|
+
singleton.module_eval(&_define_freezing_constructor)
|
104
|
+
singleton.module_eval(&_define_to_proc)
|
105
|
+
singleton.module_eval(&_define_with_constraint)
|
106
|
+
singleton.module_eval(&_define_with_validations)
|
107
|
+
end
|
108
|
+
|
87
109
|
def _define_to_h
|
88
110
|
proxy = self
|
89
111
|
->(*) do
|
@@ -107,16 +129,6 @@ module Lab42
|
|
107
129
|
data_class_instance.instance_variable_set("@#{key}", value)
|
108
130
|
end
|
109
131
|
end
|
110
|
-
|
111
|
-
def _missing_initializers
|
112
|
-
@___missing_initializers__ ||=
|
113
|
-
positionals - actual_params.keys
|
114
|
-
end
|
115
|
-
|
116
|
-
def _illegal_initializers
|
117
|
-
@___illegal_initializers__ ||=
|
118
|
-
actual_params.keys - positionals - defaults.keys
|
119
|
-
end
|
120
132
|
end
|
121
133
|
end
|
122
134
|
end
|
data/lib/lab42/data_class.rb
CHANGED
@@ -1,22 +1,34 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative './data_class/constraint_error'
|
4
|
+
require_relative './data_class/kernel'
|
5
|
+
require_relative './data_class/validation_error'
|
3
6
|
require_relative './data_class/proxy'
|
4
7
|
require_relative './pair'
|
5
8
|
require_relative './triple'
|
6
9
|
|
7
|
-
module
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
end
|
10
|
+
module Lab42
|
11
|
+
module DataClass
|
12
|
+
def self.extended(extender)
|
13
|
+
proxy = Proxy.new(extender)
|
12
14
|
|
13
|
-
|
14
|
-
|
15
|
-
|
15
|
+
extender.module_eval do
|
16
|
+
define_singleton_method(:__data_class_proxy__){ proxy }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def attributes(*args, **kwds)
|
21
|
+
__data_class_proxy__.tap do |proxy|
|
22
|
+
proxy.update!(args, kwds)
|
23
|
+
proxy.define_class!
|
24
|
+
end
|
25
|
+
end
|
16
26
|
|
17
|
-
|
18
|
-
|
27
|
+
def constraint(member, constraint = nil, &block)
|
28
|
+
raise ArgumentError, "must not provide constraint (2nd argument) and a block" if block && constraint
|
29
|
+
|
30
|
+
__data_class_proxy__.define_constraints(member => constraint || block)
|
31
|
+
end
|
19
32
|
end
|
20
33
|
end
|
21
|
-
|
22
34
|
# SPDX-License-Identifier: Apache-2.0
|
metadata
CHANGED
@@ -1,20 +1,21 @@
|
|
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.6.0
|
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-02-
|
11
|
+
date: 2022-02-24 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: |
|
14
14
|
An Immutable DataClass for Ruby
|
15
15
|
|
16
16
|
Exposes a class factory function `Kernel::DataClass` and a class
|
17
|
-
modifer `Module#dataclass'
|
17
|
+
modifer `Module#dataclass', also creates two _tuple_ classes, `Pair` and
|
18
|
+
`Triple`
|
18
19
|
email: robert.dober@gmail.com
|
19
20
|
executables: []
|
20
21
|
extensions: []
|
@@ -23,8 +24,15 @@ files:
|
|
23
24
|
- LICENSE
|
24
25
|
- README.md
|
25
26
|
- lib/lab42/data_class.rb
|
27
|
+
- lib/lab42/data_class/constraint_error.rb
|
28
|
+
- lib/lab42/data_class/kernel.rb
|
26
29
|
- lib/lab42/data_class/proxy.rb
|
30
|
+
- lib/lab42/data_class/proxy/constraints.rb
|
31
|
+
- lib/lab42/data_class/proxy/constraints/maker.rb
|
32
|
+
- lib/lab42/data_class/proxy/memos.rb
|
27
33
|
- lib/lab42/data_class/proxy/mixin.rb
|
34
|
+
- lib/lab42/data_class/proxy/validations.rb
|
35
|
+
- lib/lab42/data_class/validation_error.rb
|
28
36
|
- lib/lab42/data_class/version.rb
|
29
37
|
- lib/lab42/eq_and_patterns.rb
|
30
38
|
- lib/lab42/pair.rb
|
@@ -33,7 +41,7 @@ homepage: https://github.com/robertdober/lab42_data_class
|
|
33
41
|
licenses:
|
34
42
|
- Apache-2.0
|
35
43
|
metadata: {}
|
36
|
-
post_install_message:
|
44
|
+
post_install_message:
|
37
45
|
rdoc_options: []
|
38
46
|
require_paths:
|
39
47
|
- lib
|
@@ -49,7 +57,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
49
57
|
version: '0'
|
50
58
|
requirements: []
|
51
59
|
rubygems_version: 3.3.3
|
52
|
-
signing_key:
|
60
|
+
signing_key:
|
53
61
|
specification_version: 4
|
54
62
|
summary: Finally a dataclass in ruby
|
55
63
|
test_files: []
|