lab42_data_class 0.3.3 → 0.5.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 +234 -2
- data/lib/lab42/data_class/constraint_error.rb +9 -0
- data/lib/lab42/data_class/proxy/constraints/maker.rb +39 -0
- data/lib/lab42/data_class/proxy/constraints.rb +86 -0
- data/lib/lab42/data_class/proxy/validations.rb +38 -0
- data/lib/lab42/data_class/proxy.rb +26 -7
- 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 +12 -0
- data/lib/lab42/eq_and_patterns.rb +19 -0
- data/lib/lab42/pair.rb +21 -0
- data/lib/lab42/triple.rb +22 -0
- metadata +15 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4d8b97ca60e61ad4b3ec85555649e5538ca35ef1018bcdf2a1cd9f86d4183718
|
4
|
+
data.tar.gz: 7076ca2be4a3a8e5428b2fa0fa14523c416d491d14a8ba254842405332215dba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c74e746f2f71ac1cbd89a10566b5655e0cb4e2e9d6ff6031e147bb833b8af91b5e1251e89b0ed826401a1915e3654b8543d065138fc705491bf1c684d85d4b97
|
7
|
+
data.tar.gz: bf15dd5127834637d6c9e0e78cbaf4cb50e75afd03b3ba4d763e186ad65f00df9076892bd151bebad30d4e1d8d49b287225e00ef0ccf6eed3e7965ba1f47af62
|
data/README.md
CHANGED
@@ -1,12 +1,19 @@
|
|
1
1
|
|
2
|
-
[![
|
2
|
+
[![Issue Count](https://codeclimate.com/github/RobertDober/lab42_data_class/badges/issue_count.svg)](https://codeclimate.com/github/RobertDober/lab42_data_class)
|
3
3
|
[![CI](https://github.com/robertdober/lab42_data_class/workflows/CI/badge.svg)](https://github.com/robertdober/lab42_data_class/actions)
|
4
4
|
[![Coverage Status](https://coveralls.io/repos/github/RobertDober/lab42_data_class/badge.svg?branch=main)](https://coveralls.io/github/RobertDober/lab42_data_class?branch=main)
|
5
|
+
[![Gem Version](http://img.shields.io/gem/v/lab42_data_class.svg)](https://rubygems.org/gems/lab42_data_class)
|
6
|
+
[![Gem Downloads](https://img.shields.io/gem/dt/lab42_data_class.svg)](https://rubygems.org/gems/lab42_data_class)
|
5
7
|
|
6
8
|
|
7
9
|
# Lab42::DataClass
|
8
10
|
|
9
|
-
|
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`
|
10
17
|
|
11
18
|
## Usage
|
12
19
|
|
@@ -31,6 +38,8 @@ require 'lab42/data_class'
|
|
31
38
|
|
32
39
|
Well let us [speculate about](https://github.com/RobertDober/speculate_about) it to find out:
|
33
40
|
|
41
|
+
## Context `DataClass`
|
42
|
+
|
34
43
|
### Context: `DataClass` function
|
35
44
|
|
36
45
|
Given
|
@@ -276,6 +285,229 @@ Then we can pass it as keyword arguments
|
|
276
285
|
expect(extract_value(**my_class.new)).to eq([1, base: 2])
|
277
286
|
```
|
278
287
|
|
288
|
+
### Context: Constraints
|
289
|
+
|
290
|
+
Values of attributes of a `DataClass` can have constraints
|
291
|
+
|
292
|
+
Given a `DataClass` with constraints
|
293
|
+
```ruby
|
294
|
+
let :switch do
|
295
|
+
DataClass(on: false).with_constraint(on: -> { [false, true].member? _1 })
|
296
|
+
end
|
297
|
+
```
|
298
|
+
|
299
|
+
Then boolean values are acceptable
|
300
|
+
```ruby
|
301
|
+
expect{ switch.new }.not_to raise_error
|
302
|
+
expect(switch.new.merge(on: true).on).to eq(true)
|
303
|
+
```
|
304
|
+
|
305
|
+
But we can neither construct or merge with non boolean values
|
306
|
+
```ruby
|
307
|
+
expect{ switch.new(on: nil) }
|
308
|
+
.to raise_error(Lab42::DataClass::ConstraintError, "value nil is not allowed for attribute :on")
|
309
|
+
expect{ switch.new.merge(on: 42) }
|
310
|
+
.to raise_error(Lab42::DataClass::ConstraintError, "value 42 is not allowed for attribute :on")
|
311
|
+
```
|
312
|
+
|
313
|
+
And therefore defaultless attributes cannot have a constraint that is violated by a nil value
|
314
|
+
```ruby
|
315
|
+
error_head = "constraint error during validation of default value of attribute :value"
|
316
|
+
error_body = " undefined method `>' for nil:NilClass"
|
317
|
+
error_message = [error_head, error_body].join("\n")
|
318
|
+
|
319
|
+
expect{ DataClass(value: nil).with_constraint(value: -> { _1 > 0 }) }
|
320
|
+
.to raise_error(Lab42::DataClass::ConstraintError, /#{error_message}/)
|
321
|
+
```
|
322
|
+
|
323
|
+
And defining constraints for undefined attributes is not the best of ideas
|
324
|
+
```ruby
|
325
|
+
expect { DataClass(a: 1).with_constraint(b: -> {true}) }
|
326
|
+
.to raise_error(ArgumentError, "constraints cannot be defined for undefined attributes [:b]")
|
327
|
+
```
|
328
|
+
|
329
|
+
|
330
|
+
#### Context: Convenience Constraints
|
331
|
+
|
332
|
+
Often repeating patterns are implemented as non lambda constraints, depending on the type of a constraint
|
333
|
+
it is implicitly converted to a lambda as specified below:
|
334
|
+
|
335
|
+
Given a shortcut for our `ConstraintError`
|
336
|
+
```ruby
|
337
|
+
let(:constraint_error) { Lab42::DataClass::ConstraintError }
|
338
|
+
let(:positive) { DataClass(:value) }
|
339
|
+
```
|
340
|
+
|
341
|
+
##### Symbols
|
342
|
+
|
343
|
+
... are sent to the value of the attribute, this is not very surprising of course ;)
|
344
|
+
|
345
|
+
Then a first implementation of `Positive`
|
346
|
+
```ruby
|
347
|
+
positive_by_symbol = positive.with_constraint(value: :positive?)
|
348
|
+
|
349
|
+
expect(positive_by_symbol.new(value: 1).value).to eq(1)
|
350
|
+
expect{positive_by_symbol.new(value: 0)}.to raise_error(constraint_error)
|
351
|
+
```
|
352
|
+
|
353
|
+
##### Arrays
|
354
|
+
|
355
|
+
... are also sent to the value of the attribute, this time we can provide paramaters
|
356
|
+
And we can implement a different form of `Positive`
|
357
|
+
```ruby
|
358
|
+
positive_by_ary = positive.with_constraint(value: [:>, 0])
|
359
|
+
|
360
|
+
expect(positive_by_ary.new(value: 1).value).to eq(1)
|
361
|
+
expect{positive_by_ary.new(value: 0)}.to raise_error(constraint_error)
|
362
|
+
```
|
363
|
+
|
364
|
+
If however we are interested in membership we have to wrap the `Array` into a `Set`
|
365
|
+
|
366
|
+
##### Membership
|
367
|
+
|
368
|
+
And this works with a `Set`
|
369
|
+
```ruby
|
370
|
+
positive_by_set = positive.with_constraint(value: Set.new([*1..10]))
|
371
|
+
|
372
|
+
expect(positive_by_set.new(value: 1).value).to eq(1)
|
373
|
+
expect{positive_by_set.new(value: 0)}.to raise_error(constraint_error)
|
374
|
+
```
|
375
|
+
|
376
|
+
And also with a `Range`
|
377
|
+
```ruby
|
378
|
+
positive_by_range = positive.with_constraint(value: 1..Float::INFINITY)
|
379
|
+
|
380
|
+
expect(positive_by_range.new(value: 1).value).to eq(1)
|
381
|
+
expect{positive_by_range.new(value: 0)}.to raise_error(constraint_error)
|
382
|
+
```
|
383
|
+
|
384
|
+
##### Regexen
|
385
|
+
|
386
|
+
This seems quite obvious, and of course it works
|
387
|
+
|
388
|
+
Then we can also have a regex based constraint
|
389
|
+
```ruby
|
390
|
+
vowel = DataClass(:word).with_constraint(word: /[aeiou]/)
|
391
|
+
|
392
|
+
expect(vowel.new(word: "alpha").word).to eq("alpha")
|
393
|
+
expect{vowel.new(word: "krk")}.to raise_error(constraint_error)
|
394
|
+
```
|
395
|
+
|
396
|
+
##### Other callable objects as constraints
|
397
|
+
|
398
|
+
|
399
|
+
Then we can also use instance methods to implement our `Positive`
|
400
|
+
```ruby
|
401
|
+
positive_by_instance_method = positive.with_constraint(value: Fixnum.instance_method(:positive?))
|
402
|
+
|
403
|
+
expect(positive_by_instance_method.new(value: 1).value).to eq(1)
|
404
|
+
expect{positive_by_instance_method.new(value: 0)}.to raise_error(constraint_error)
|
405
|
+
```
|
406
|
+
|
407
|
+
Or we can use methods to implement it
|
408
|
+
```ruby
|
409
|
+
positive_by_method = positive.with_constraint(value: 0.method(:<))
|
410
|
+
|
411
|
+
expect(positive_by_method.new(value: 1).value).to eq(1)
|
412
|
+
expect{positive_by_method.new(value: 0)}.to raise_error(constraint_error)
|
413
|
+
```
|
414
|
+
|
415
|
+
#### Context: Global Constraints aka __Validations__
|
416
|
+
|
417
|
+
So far we have only speculated about constraints concerning one attribute, however sometimes we want
|
418
|
+
to have arbitrary constraints which can only be calculated by access to more attributes
|
419
|
+
|
420
|
+
Given a `Point` DataClass
|
421
|
+
```ruby
|
422
|
+
let(:point) { DataClass(:x, :y).validate{ |point| point.x > point.y } }
|
423
|
+
let(:validation_error) { Lab42::DataClass::ValidationError }
|
424
|
+
```
|
425
|
+
|
426
|
+
Then we will get a `ValidationError` if we construct a point left of the main diagonal
|
427
|
+
```ruby
|
428
|
+
expect{ point.new(x: 0, y: 1) }
|
429
|
+
.to raise_error(validation_error)
|
430
|
+
```
|
431
|
+
|
432
|
+
But as validation might need more than the default values we will not execute them at compile time
|
433
|
+
```ruby
|
434
|
+
expect{ DataClass(x: 0, y: 0).validate{ |inst| inst.x > inst.y } }
|
435
|
+
.to_not raise_error
|
436
|
+
```
|
437
|
+
|
438
|
+
And we can name validations to get better error messages
|
439
|
+
```ruby
|
440
|
+
better_point = DataClass(:x, :y).validate(:too_left){ |point| point.x > point.y }
|
441
|
+
ok_point = better_point.new(x: 1, y: 0)
|
442
|
+
expect{ ok_point.merge(y: 1) }
|
443
|
+
.to raise_error(validation_error, "too_left")
|
444
|
+
```
|
445
|
+
|
446
|
+
And remark how bad unnamed validation errors might be
|
447
|
+
```ruby
|
448
|
+
error_message_rgx = %r{
|
449
|
+
\#<Proc:0x[0-9a-f]+ \s .* spec/speculations/README_spec\.rb: \d+ > \z
|
450
|
+
}x
|
451
|
+
expect{ point.new(x: 0, y: 1) }
|
452
|
+
.to raise_error(validation_error, error_message_rgx)
|
453
|
+
```
|
454
|
+
|
455
|
+
## Context: `Pair` and `Triple`
|
456
|
+
|
457
|
+
Two special cases of a `DataClass` which behave like `Tuple` of size 2 and 3 in _Elixir_
|
458
|
+
|
459
|
+
|
460
|
+
They distinguish themselves from `DataClass` classes by accepting only positional arguments, and
|
461
|
+
cannot be converted to hashes.
|
462
|
+
|
463
|
+
These are actually two classes and not class factories as they have a fixed interface , but let us speculate about them to learn what they can do for us.
|
464
|
+
|
465
|
+
### Context: Constructor functions
|
466
|
+
|
467
|
+
Given a pair
|
468
|
+
```ruby
|
469
|
+
let(:token) { Pair("12", 12) }
|
470
|
+
let(:node) { Triple("42", 4, 2) }
|
471
|
+
```
|
472
|
+
|
473
|
+
Then we can access their elements
|
474
|
+
```ruby
|
475
|
+
expect(token.first).to eq("12")
|
476
|
+
expect(token.second).to eq(12)
|
477
|
+
expect(node.first).to eq("42")
|
478
|
+
expect(node.second).to eq(4)
|
479
|
+
expect(node.third).to eq(2)
|
480
|
+
```
|
481
|
+
|
482
|
+
And we can treat them like _Indexable_
|
483
|
+
```ruby
|
484
|
+
expect(token[1]).to eq(12)
|
485
|
+
expect(token[-2]).to eq("12")
|
486
|
+
expect(node[2]).to eq(2)
|
487
|
+
```
|
488
|
+
|
489
|
+
And convert them to arrays of course
|
490
|
+
```ruby
|
491
|
+
expect(token.to_a).to eq(["12", 12])
|
492
|
+
expect(node.to_a).to eq(["42", 4, 2])
|
493
|
+
```
|
494
|
+
|
495
|
+
And they behave like arrays in pattern matching too
|
496
|
+
```ruby
|
497
|
+
token => [str, int]
|
498
|
+
node => [root, lft, rgt]
|
499
|
+
expect(str).to eq("12")
|
500
|
+
expect(int).to eq(12)
|
501
|
+
expect(root).to eq("42")
|
502
|
+
expect(lft).to eq(4)
|
503
|
+
expect(rgt).to eq(2)
|
504
|
+
```
|
505
|
+
|
506
|
+
And of course the factory functions are equivalent to the constructors
|
507
|
+
```ruby
|
508
|
+
expect(token).to eq(Lab42::Pair.new("12", 12))
|
509
|
+
expect(node).to eq(Lab42::Triple.new("42", 4, 2))
|
510
|
+
```
|
279
511
|
|
280
512
|
# LICENSE
|
281
513
|
|
@@ -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,86 @@
|
|
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
|
+
private
|
27
|
+
|
28
|
+
def _check_constraint_against_default
|
29
|
+
->((attr, constraint)) do
|
30
|
+
if defaults.key?(attr)
|
31
|
+
_check_constraint_against_default_value(attr, defaults[attr], constraint)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def _check_constraint_against_default_value(attr, value, constraint)
|
37
|
+
unless constraint.(value)
|
38
|
+
"default value #{value.inspect} is not allowed for attribute #{attr.inspect}"
|
39
|
+
end
|
40
|
+
rescue StandardError => e
|
41
|
+
"constraint error during validation of default value of attribute #{attr.inspect}\n #{e.message}"
|
42
|
+
end
|
43
|
+
|
44
|
+
def _check_constraints_for_attr!
|
45
|
+
->((k, v)) do
|
46
|
+
constraints[k]
|
47
|
+
.map(&_check_constraint!(k, v))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def _check_constraint!(attr, value)
|
52
|
+
->(constraint) do
|
53
|
+
"value #{value.inspect} is not allowed for attribute #{attr.inspect}" unless constraint.(value)
|
54
|
+
rescue StandardError => e
|
55
|
+
"constraint error during validation of attribute #{attr.inspect}\n #{e.message}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def _check_constraints!(params)
|
60
|
+
errors = params
|
61
|
+
.flat_map(&_check_constraints_for_attr!)
|
62
|
+
.compact
|
63
|
+
|
64
|
+
raise ConstraintError, errors.join("\n\n") unless errors.empty?
|
65
|
+
end
|
66
|
+
|
67
|
+
def _define_with_constraint
|
68
|
+
proxy = self
|
69
|
+
->(*) do
|
70
|
+
define_method :with_constraint do |**constraints|
|
71
|
+
errors = constraints.map(&proxy.define_constraint).compact
|
72
|
+
unless errors.empty?
|
73
|
+
raise ArgumentError,
|
74
|
+
"constraints cannot be defined for undefined attributes #{errors.inspect}"
|
75
|
+
end
|
76
|
+
|
77
|
+
proxy.check_constraints_against_defaults(constraints)
|
78
|
+
self
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
# 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/validations'
|
4
6
|
require_relative 'proxy/mixin'
|
5
7
|
module Lab42
|
6
8
|
module DataClass
|
7
9
|
class Proxy
|
8
|
-
|
10
|
+
include Constraints, Validations
|
11
|
+
|
12
|
+
attr_reader :actual_params, :all_params, :block, :constraints, :defaults, :klass, :members, :positionals
|
9
13
|
|
10
14
|
def check!(**params)
|
11
15
|
@actual_params = params
|
16
|
+
@all_params = defaults.merge(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!(all_params)
|
14
21
|
end
|
15
22
|
|
16
23
|
def define_class!
|
@@ -32,14 +39,20 @@ module Lab42
|
|
32
39
|
.to_h
|
33
40
|
end
|
34
41
|
|
42
|
+
def validations
|
43
|
+
@__validations__ ||= []
|
44
|
+
end
|
45
|
+
|
35
46
|
private
|
36
47
|
def initialize(*args, **kwds, &blk)
|
37
48
|
@klass = Class.new
|
38
49
|
|
50
|
+
@constraints = Hash.new { |h, k| h[k] = [] }
|
51
|
+
|
39
52
|
@block = blk
|
40
53
|
@defaults = kwds
|
41
54
|
@members = Set.new(args + kwds.keys)
|
42
|
-
# TODO: Check for all symbols and no duplicates ⇒ v0.
|
55
|
+
# TODO: Check for all symbols and no duplicates ⇒ v0.5.1
|
43
56
|
@positionals = args
|
44
57
|
end
|
45
58
|
|
@@ -64,28 +77,34 @@ module Lab42
|
|
64
77
|
define_method :initialize do |**params|
|
65
78
|
proxy.check!(**params)
|
66
79
|
proxy.init(self, **params)
|
80
|
+
proxy.validate!(self)
|
67
81
|
end
|
68
82
|
end
|
69
83
|
end
|
70
84
|
|
71
85
|
def _define_merge
|
72
|
-
proxy = self
|
73
86
|
->(*) do
|
74
87
|
define_method :merge do |**params|
|
75
88
|
values = to_h.merge(params)
|
76
|
-
|
77
|
-
.new(**values)
|
89
|
+
self.class.new(**values)
|
78
90
|
end
|
79
91
|
end
|
80
92
|
end
|
81
93
|
|
82
94
|
def _define_methods
|
83
|
-
|
84
|
-
|
95
|
+
class << klass; self end
|
96
|
+
.tap { |singleton| _define_singleton_methods(singleton) }
|
85
97
|
klass.module_eval(&_define_to_h)
|
86
98
|
klass.module_eval(&_define_merge)
|
87
99
|
end
|
88
100
|
|
101
|
+
def _define_singleton_methods(singleton)
|
102
|
+
singleton.module_eval(&_define_freezing_constructor)
|
103
|
+
singleton.module_eval(&_define_to_proc)
|
104
|
+
singleton.module_eval(&_define_with_constraint)
|
105
|
+
singleton.module_eval(&_define_with_validations)
|
106
|
+
end
|
107
|
+
|
89
108
|
def _define_to_h
|
90
109
|
proxy = self
|
91
110
|
->(*) do
|
data/lib/lab42/data_class.rb
CHANGED
@@ -1,12 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative './data_class/constraint_error'
|
4
|
+
require_relative './data_class/validation_error'
|
3
5
|
require_relative './data_class/proxy'
|
6
|
+
require_relative './pair'
|
7
|
+
require_relative './triple'
|
4
8
|
|
5
9
|
module Kernel
|
6
10
|
def DataClass(*args, **kwds, &blk)
|
7
11
|
proxy = Lab42::DataClass::Proxy.new(*args, **kwds, &blk)
|
8
12
|
proxy.define_class!
|
9
13
|
end
|
14
|
+
|
15
|
+
def Pair(first, second)
|
16
|
+
Lab42::Pair.new(first, second)
|
17
|
+
end
|
18
|
+
|
19
|
+
def Triple(first, second, third)
|
20
|
+
Lab42::Triple.new(first, second, third)
|
21
|
+
end
|
10
22
|
end
|
11
23
|
|
12
24
|
# SPDX-License-Identifier: Apache-2.0
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab42
|
4
|
+
module EqAndPatterns
|
5
|
+
def [](idx)
|
6
|
+
to_a[idx]
|
7
|
+
end
|
8
|
+
|
9
|
+
def ==(other)
|
10
|
+
other.is_a?(self.class) &&
|
11
|
+
to_a == other.to_a
|
12
|
+
end
|
13
|
+
|
14
|
+
def deconstruct(*)
|
15
|
+
to_a
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
# SPDX-License-Identifier: Apache-2.0
|
data/lib/lab42/pair.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'eq_and_patterns'
|
4
|
+
module Lab42
|
5
|
+
class Pair
|
6
|
+
attr_reader :first, :second
|
7
|
+
include EqAndPatterns
|
8
|
+
|
9
|
+
def to_a
|
10
|
+
[first, second]
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def initialize(first, second)
|
16
|
+
@first = first
|
17
|
+
@second = second
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
# SPDX-License-Identifier: Apache-2.0
|
data/lib/lab42/triple.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'eq_and_patterns'
|
4
|
+
module Lab42
|
5
|
+
class Triple
|
6
|
+
attr_reader :first, :second, :third
|
7
|
+
include EqAndPatterns
|
8
|
+
|
9
|
+
def to_a
|
10
|
+
[first, second, third]
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def initialize(first, second, third)
|
16
|
+
@first = first
|
17
|
+
@second = second
|
18
|
+
@third = third
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
# 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.5.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-
|
11
|
+
date: 2022-02-23 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,14 +24,22 @@ files:
|
|
23
24
|
- LICENSE
|
24
25
|
- README.md
|
25
26
|
- lib/lab42/data_class.rb
|
27
|
+
- lib/lab42/data_class/constraint_error.rb
|
26
28
|
- lib/lab42/data_class/proxy.rb
|
29
|
+
- lib/lab42/data_class/proxy/constraints.rb
|
30
|
+
- lib/lab42/data_class/proxy/constraints/maker.rb
|
27
31
|
- lib/lab42/data_class/proxy/mixin.rb
|
32
|
+
- lib/lab42/data_class/proxy/validations.rb
|
33
|
+
- lib/lab42/data_class/validation_error.rb
|
28
34
|
- lib/lab42/data_class/version.rb
|
35
|
+
- lib/lab42/eq_and_patterns.rb
|
36
|
+
- lib/lab42/pair.rb
|
37
|
+
- lib/lab42/triple.rb
|
29
38
|
homepage: https://github.com/robertdober/lab42_data_class
|
30
39
|
licenses:
|
31
40
|
- Apache-2.0
|
32
41
|
metadata: {}
|
33
|
-
post_install_message:
|
42
|
+
post_install_message:
|
34
43
|
rdoc_options: []
|
35
44
|
require_paths:
|
36
45
|
- lib
|
@@ -46,7 +55,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
46
55
|
version: '0'
|
47
56
|
requirements: []
|
48
57
|
rubygems_version: 3.3.3
|
49
|
-
signing_key:
|
58
|
+
signing_key:
|
50
59
|
specification_version: 4
|
51
60
|
summary: Finally a dataclass in ruby
|
52
61
|
test_files: []
|