lab42_data_class 0.1.2 → 0.3.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 +145 -21
- data/lib/lab42/data_class/proxy/mixin.rb +19 -0
- data/lib/lab42/data_class/proxy.rb +19 -10
- data/lib/lab42/data_class/version.rb +1 -1
- data/lib/lab42/data_class.rb +0 -1
- metadata +8 -4
- data/lib/lab42/data_class/for_module.rb +0 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ca6b2bccf941edcb400b3e9230d394ef867a4a37b6865f5dd76e521fdb33070f
|
4
|
+
data.tar.gz: 431c61adef3873183ecba35f86c1f5397c4b8c3b9df140d09438e8e3a5d0b1da
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3f63d89d02aa4a4e2ac551ce79c6f09787ab7d7f173ef459e10a689b0c24dd1682b6d27e756534108187a113784a632adfa384858d2e553e8ca37a72d4bb1b09
|
7
|
+
data.tar.gz: 8be0680e0cd43d98a9fe8bfdc8c1844ad076e65fdba2b7360f2471d2f74498dbe937fd74c456231b80066541739551e76cf58356b9fa45bdbf02a5fdab673573
|
data/README.md
CHANGED
@@ -6,7 +6,7 @@
|
|
6
6
|
|
7
7
|
# Lab42::DataClass
|
8
8
|
|
9
|
-
|
9
|
+
An immutable dataclass
|
10
10
|
|
11
11
|
## Usage
|
12
12
|
|
@@ -60,7 +60,18 @@ And we can extract the values
|
|
60
60
|
expect(my_instance.to_h).to eq(name: "robert", email: nil)
|
61
61
|
```
|
62
62
|
|
63
|
-
#### Context: Immutable
|
63
|
+
#### Context: Immutable → self
|
64
|
+
|
65
|
+
Then `my_instance` is frozen:
|
66
|
+
```ruby
|
67
|
+
expect(my_instance).to be_frozen
|
68
|
+
```
|
69
|
+
And we cannot even mute `my_instance` by means of metaprogramming
|
70
|
+
```ruby
|
71
|
+
expect{ my_instance.instance_variable_set("@x", nil) }.to raise_error(FrozenError)
|
72
|
+
```
|
73
|
+
|
74
|
+
#### Context: Immutable → Cloning
|
64
75
|
|
65
76
|
Given
|
66
77
|
```ruby
|
@@ -71,6 +82,10 @@ Then we have a new instance with the old instance unchanged
|
|
71
82
|
expect(other_instance.to_h).to eq(name: "robert", email: "robert@mail.provider")
|
72
83
|
expect(my_instance.to_h).to eq(name: "robert", email: nil)
|
73
84
|
```
|
85
|
+
And the new instance is frozen again
|
86
|
+
```ruby
|
87
|
+
expect(other_instance).to be_frozen
|
88
|
+
```
|
74
89
|
|
75
90
|
### Context: Defining behavior with blocks
|
76
91
|
|
@@ -90,25 +105,6 @@ Then I have defined a method on my dataclass
|
|
90
105
|
```ruby
|
91
106
|
expect(my_instance.show).to eq("<42>")
|
92
107
|
```
|
93
|
-
### Context: Making a dataclass from a class
|
94
|
-
|
95
|
-
Given
|
96
|
-
```ruby
|
97
|
-
class DC
|
98
|
-
dataclass x: 1, y: 41
|
99
|
-
def sum; x + y end
|
100
|
-
end
|
101
|
-
```
|
102
|
-
|
103
|
-
Then we can define methods on it
|
104
|
-
```ruby
|
105
|
-
expect(DC.new.sum).to eq(42)
|
106
|
-
```
|
107
|
-
|
108
|
-
And we have a nice name for our instances
|
109
|
-
```ruby
|
110
|
-
expect(DC.new.class.name).to eq("DC")
|
111
|
-
```
|
112
108
|
|
113
109
|
### Context: Equality
|
114
110
|
|
@@ -131,6 +127,134 @@ But not in the sense of `equal?`, of course
|
|
131
127
|
expect(instance2).not_to be_equal(instance1)
|
132
128
|
```
|
133
129
|
|
130
|
+
#### Context: Immutability of `dataclass` modified classes
|
131
|
+
|
132
|
+
Then we still get frozen instances
|
133
|
+
```ruby
|
134
|
+
expect(instance1).to be_frozen
|
135
|
+
```
|
136
|
+
|
137
|
+
### Context: Inheritance
|
138
|
+
|
139
|
+
... is a no, we do not want inheritance although we **like** code reuse, how to do it then?
|
140
|
+
|
141
|
+
Well there shall be many different possibilities, depending on your style, use case and
|
142
|
+
context, here is just one example:
|
143
|
+
|
144
|
+
Given a class factory
|
145
|
+
```ruby
|
146
|
+
let :token do
|
147
|
+
->(*a, **k) do
|
148
|
+
DataClass(*a, **(k.merge(text: "")))
|
149
|
+
end
|
150
|
+
end
|
151
|
+
```
|
152
|
+
|
153
|
+
Then we have reused the `token` successfully
|
154
|
+
```ruby
|
155
|
+
empty = token.()
|
156
|
+
integer = token.(:value)
|
157
|
+
boolean = token.(value: false)
|
158
|
+
|
159
|
+
expect(empty.new.to_h).to eq(text: "")
|
160
|
+
expect(integer.new(value: -1).to_h).to eq(text: "", value: -1)
|
161
|
+
expect(boolean.new.value).to eq(false)
|
162
|
+
```
|
163
|
+
|
164
|
+
#### Context: Mixing in a module can be used of course
|
165
|
+
|
166
|
+
Given a behavior like
|
167
|
+
```ruby
|
168
|
+
module Humanize
|
169
|
+
def humanize
|
170
|
+
"my value is #{value}"
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
let(:class_level) { DataClass(value: 1).include(Humanize) }
|
175
|
+
```
|
176
|
+
|
177
|
+
Then we can access the included method
|
178
|
+
```ruby
|
179
|
+
expect(class_level.new.humanize).to eq("my value is 1")
|
180
|
+
```
|
181
|
+
|
182
|
+
### Context: Pattern Matching
|
183
|
+
|
184
|
+
A `DataClass` object behaves like the result of it's `to_h` in pattern matching
|
185
|
+
|
186
|
+
Given
|
187
|
+
```ruby
|
188
|
+
let(:numbers) { DataClass(:name, values: []) }
|
189
|
+
let(:odds) { numbers.new(name: "odds", values: (1..4).map{ _1 + _1 + 1}) }
|
190
|
+
let(:evens) { numbers.new(name: "evens", values: (1..4).map{ _1 + _1}) }
|
191
|
+
```
|
192
|
+
|
193
|
+
Then we can match accordingly
|
194
|
+
```ruby
|
195
|
+
match = case odds
|
196
|
+
in {name: "odds", values: [1, *]}
|
197
|
+
:not_really
|
198
|
+
in {name: "evens"}
|
199
|
+
:still_naaah
|
200
|
+
in {name: "odds", values: [hd, *]}
|
201
|
+
hd
|
202
|
+
else
|
203
|
+
:strange
|
204
|
+
end
|
205
|
+
expect(match).to eq(3)
|
206
|
+
```
|
207
|
+
|
208
|
+
And in `in` expressions
|
209
|
+
```ruby
|
210
|
+
evens => {values: [_, second, *]}
|
211
|
+
expect(second).to eq(4)
|
212
|
+
```
|
213
|
+
|
214
|
+
#### Context: In Case Statements
|
215
|
+
|
216
|
+
Given a nice little dataclass `Box`
|
217
|
+
```ruby
|
218
|
+
let(:box) { DataClass content: nil }
|
219
|
+
```
|
220
|
+
|
221
|
+
Then we can also use it in a case statement
|
222
|
+
```ruby
|
223
|
+
value = case box.new
|
224
|
+
when box
|
225
|
+
42
|
226
|
+
else
|
227
|
+
0
|
228
|
+
end
|
229
|
+
expect(value).to eq(42)
|
230
|
+
```
|
231
|
+
|
232
|
+
And all the associated methods
|
233
|
+
```ruby
|
234
|
+
expect(box.new).to be_a(box)
|
235
|
+
expect(box === box.new).to be_truthy
|
236
|
+
```
|
237
|
+
|
238
|
+
### Context: Behaving like a proc
|
239
|
+
|
240
|
+
It is useful to be able to filter heterogeneous lists of `DataClass` instances by means of `&to_proc`, therefore
|
241
|
+
|
242
|
+
Given two different `DataClass` objects
|
243
|
+
```ruby
|
244
|
+
let(:class1) { DataClass :value }
|
245
|
+
let(:class2) { DataClass :value }
|
246
|
+
```
|
247
|
+
|
248
|
+
And a list of instances
|
249
|
+
```ruby
|
250
|
+
let(:list) {[class1.new(value: 1), class2.new(value: 2), class1.new(value: 3)]}
|
251
|
+
```
|
252
|
+
|
253
|
+
Then we can filter
|
254
|
+
```ruby
|
255
|
+
expect(list.filter(&class2)).to eq([class2.new(value: 2)])
|
256
|
+
```
|
257
|
+
|
134
258
|
# LICENSE
|
135
259
|
|
136
260
|
Copyright 2022 Robert Dober robert.dober@gmail.com
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lab42
|
4
|
+
module DataClass
|
5
|
+
class Proxy
|
6
|
+
module Mixin
|
7
|
+
def ==(other)
|
8
|
+
other.is_a?(self.class) &&
|
9
|
+
to_h == other.to_h
|
10
|
+
end
|
11
|
+
|
12
|
+
def deconstruct_keys(*)
|
13
|
+
to_h
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
# SPDX-License-Identifier: Apache-2.0
|
@@ -1,10 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'set'
|
4
|
+
require_relative 'proxy/mixin'
|
4
5
|
module Lab42
|
5
6
|
module DataClass
|
6
7
|
class Proxy
|
7
|
-
attr_reader :actual_params, :block, :defaults, :klass, :
|
8
|
+
attr_reader :actual_params, :block, :defaults, :klass, :members, :positionals
|
8
9
|
|
9
10
|
def check!(**params)
|
10
11
|
@actual_params = params
|
@@ -16,6 +17,8 @@ module Lab42
|
|
16
17
|
klass.module_eval(&_define_attr_reader)
|
17
18
|
klass.module_eval(&_define_initializer)
|
18
19
|
_define_methods
|
20
|
+
klass.include(Mixin)
|
21
|
+
klass.module_eval(&block) if block
|
19
22
|
klass
|
20
23
|
end
|
21
24
|
|
@@ -31,8 +34,7 @@ module Lab42
|
|
31
34
|
|
32
35
|
private
|
33
36
|
def initialize(*args, **kwds, &blk)
|
34
|
-
@klass =
|
35
|
-
@has_parent = !!kwds.delete(:__klass__)
|
37
|
+
@klass = Class.new
|
36
38
|
|
37
39
|
@block = blk
|
38
40
|
@defaults = kwds
|
@@ -48,11 +50,10 @@ module Lab42
|
|
48
50
|
end
|
49
51
|
end
|
50
52
|
|
51
|
-
def
|
53
|
+
def _define_freezing_constructor
|
52
54
|
->(*) do
|
53
|
-
define_method
|
54
|
-
|
55
|
-
to_h == other.to_h
|
55
|
+
define_method :new do |*a, **p, &b|
|
56
|
+
super(*a, **p, &b).freeze
|
56
57
|
end
|
57
58
|
end
|
58
59
|
end
|
@@ -72,17 +73,17 @@ module Lab42
|
|
72
73
|
->(*) do
|
73
74
|
define_method :merge do |**params|
|
74
75
|
values = to_h.merge(params)
|
75
|
-
DataClass(*proxy.positionals,
|
76
|
+
DataClass(*proxy.positionals, **proxy.defaults, &proxy.block)
|
76
77
|
.new(**values)
|
77
78
|
end
|
78
79
|
end
|
79
80
|
end
|
80
81
|
|
81
82
|
def _define_methods
|
83
|
+
(class << klass; self end).module_eval(&_define_freezing_constructor)
|
84
|
+
(class << klass; self end).module_eval(&_define_to_proc)
|
82
85
|
klass.module_eval(&_define_to_h)
|
83
86
|
klass.module_eval(&_define_merge)
|
84
|
-
klass.module_eval(&_define_eql?)
|
85
|
-
klass.module_eval(&block) if block
|
86
87
|
end
|
87
88
|
|
88
89
|
def _define_to_h
|
@@ -94,6 +95,14 @@ module Lab42
|
|
94
95
|
end
|
95
96
|
end
|
96
97
|
|
98
|
+
def _define_to_proc
|
99
|
+
->(*) do
|
100
|
+
define_method :to_proc do
|
101
|
+
->(other) { self === other }
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
97
106
|
def _init(data_class_instance, params)
|
98
107
|
params.each do |key, value|
|
99
108
|
data_class_instance.instance_variable_set("@#{key}", value)
|
data/lib/lab42/data_class.rb
CHANGED
metadata
CHANGED
@@ -1,16 +1,20 @@
|
|
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.3.2
|
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-01-
|
11
|
+
date: 2022-01-30 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
|
-
description:
|
13
|
+
description: |
|
14
|
+
An Immutable DataClass for Ruby
|
15
|
+
|
16
|
+
Exposes a class factory function `Kernel::DataClass` and a class
|
17
|
+
modifer `Module#dataclass'
|
14
18
|
email: robert.dober@gmail.com
|
15
19
|
executables: []
|
16
20
|
extensions: []
|
@@ -19,8 +23,8 @@ files:
|
|
19
23
|
- LICENSE
|
20
24
|
- README.md
|
21
25
|
- lib/lab42/data_class.rb
|
22
|
-
- lib/lab42/data_class/for_module.rb
|
23
26
|
- lib/lab42/data_class/proxy.rb
|
27
|
+
- lib/lab42/data_class/proxy/mixin.rb
|
24
28
|
- lib/lab42/data_class/version.rb
|
25
29
|
homepage: https://github.com/robertdober/lab42_data_class
|
26
30
|
licenses:
|
@@ -1,15 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'proxy'
|
4
|
-
|
5
|
-
module Lab42
|
6
|
-
module DataClass
|
7
|
-
class ::Module
|
8
|
-
def dataclass(*args, **defaults)
|
9
|
-
proxy = Lab42::DataClass::Proxy.new(*args, __klass__: self, **defaults)
|
10
|
-
proxy.define_class!
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
15
|
-
# SPDX-License-Identifier: Apache-2.0
|