lab42_data_class 0.1.2 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 04c191005baa65d6d21f1bea2bfbf8361651ab06e9c81e45cc2ab0ac7d8531e8
4
- data.tar.gz: d66a6a791c4102e5d9347441a713e893a74e1d49efd39ee145db890ea5d9db07
3
+ metadata.gz: ca6b2bccf941edcb400b3e9230d394ef867a4a37b6865f5dd76e521fdb33070f
4
+ data.tar.gz: 431c61adef3873183ecba35f86c1f5397c4b8c3b9df140d09438e8e3a5d0b1da
5
5
  SHA512:
6
- metadata.gz: 8ee5d308afdb75b72840846a7c968d95693204f9db441e8c20825c239c3a9895f0ebfb0cf49e8c2fef5c5e31b79c858cee837c91281a7b48b888451dc6f49e50
7
- data.tar.gz: 3f170816817a20cd00827d87bf4257cfa7a8e8c748f0a46be64ca30ea3d4533c1d9af9a3192676abf3ea9119b8f6c5a7ef69144c89ee1a1dd71bd165bc0d8af1
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
- A dataclass with an immutable API (you can still change the state of the object with metaprogramming and `lab42_immutable` is not ready yet!)
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, :has_parent, :members, :positionals
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 = kwds.fetch(:__klass__){ Class.new }
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 _define_eql?
53
+ def _define_freezing_constructor
52
54
  ->(*) do
53
- define_method :== do |other|
54
- other.is_a?(self.class) &&
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, __klass__: self.class, **proxy.defaults, &proxy.block)
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)
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Lab42
4
4
  module DataClass
5
- VERSION = "0.1.2"
5
+ VERSION = "0.3.2"
6
6
  end
7
7
  end
8
8
  # SPDX-License-Identifier: Apache-2.0
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative './data_class/for_module'
4
3
  require_relative './data_class/proxy'
5
4
 
6
5
  module Kernel
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.1.2
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-21 00:00:00.000000000 Z
11
+ date: 2022-01-30 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description: introduces a new Kernel function DataClass
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