lab42_data_class 0.1.1 → 0.3.1

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: c4e2d75e76874aa4cce514081f4be6cd1ab19d827b8e8bb60645f4036a202402
4
- data.tar.gz: 4f2dd94d174acd1ea78b70b5063709566ba16b323d8d549221fdecaf10d184d3
3
+ metadata.gz: 8e4127a34f013386209c71666f9d77e1b5c8fd439324443580e00c7e9018fdb4
4
+ data.tar.gz: c61d6f1648c9c73cd89883a0c0e6ebd6ea813f51dcdb26f141a99a962e4db5fa
5
5
  SHA512:
6
- metadata.gz: da62522c51b10954ca4dd86e4b2edcf31a13497a1ec65d169a6944d95c0c7aa6b53d8fb3d7f025ea369420ee8676a312db16c7120677f22753b61996a745bfee
7
- data.tar.gz: fc638df334bfb4ad58934d2e9736aad751465cb078a106f47758edbd7265bb9f49a48ef1d0af3d5626b14d7376c20e8be3880fdd53c17e9e26beeeb6e3bf02db
6
+ metadata.gz: 74b643eb6d43c00aedede31195354412a0b7dcabc5f5da0d1c3f5dbfca4dc03abc1b13a68e10307813153b6e8fe0c502a075a9c4ef17c61f1163ec251e03b3f7
7
+ data.tar.gz: '007384216191284048c0fcec62adedb04c8f41a549b92d7cdefcbaa3fa2f4536cce737d4a21a6c8d3648fe62a539fd9f201cd036566b9ff3251ac2929856a6a7'
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,24 +105,110 @@ 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
108
 
95
- Given
109
+ ### Context: Equality
110
+
111
+ Given two instances of a DataClass
112
+ ```ruby
113
+ let(:data_class) { DataClass :a }
114
+ let(:instance1) { data_class.new(a: 1) }
115
+ let(:instance2) { data_class.new(a: 1) }
116
+ ```
117
+ Then they are equal in the sense of `==` and `eql?`
96
118
  ```ruby
97
- class DC
98
- dataclass x: 1, y: 41
99
- def sum; x + y end
119
+ expect(instance1).to eq(instance2)
120
+ expect(instance2).to eq(instance1)
121
+ expect(instance1 == instance2).to be_truthy
122
+ expect(instance2 == instance1).to be_truthy
123
+ ```
124
+ But not in the sense of `equal?`, of course
125
+ ```ruby
126
+ expect(instance1).not_to be_equal(instance2)
127
+ expect(instance2).not_to be_equal(instance1)
128
+ ```
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
100
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
+ An `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}) }
101
191
  ```
102
192
 
103
- Then we can define methods on it
193
+ Then we can match accordingly
104
194
  ```ruby
105
- expect(DC.new.sum).to eq(42)
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)
106
206
  ```
107
207
 
108
- And we have a nice name for our instances
208
+ And in `in` expressions
109
209
  ```ruby
110
- expect(DC.new.class.name).to eq("DC")
210
+ evens => {values: [_, second, *]}
211
+ expect(second).to eq(4)
111
212
  ```
112
213
 
113
214
  # LICENSE
@@ -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,6 +50,14 @@ module Lab42
48
50
  end
49
51
  end
50
52
 
53
+ def _define_freezing_constructor
54
+ ->(*) do
55
+ define_method :new do |*a, **p, &b|
56
+ super(*a, **p, &b).freeze
57
+ end
58
+ end
59
+ end
60
+
51
61
  def _define_initializer
52
62
  proxy = self
53
63
  ->(*) do
@@ -63,16 +73,16 @@ module Lab42
63
73
  ->(*) do
64
74
  define_method :merge do |**params|
65
75
  values = to_h.merge(params)
66
- DataClass(*proxy.positionals, __klass__: self.class, **proxy.defaults, &proxy.block)
76
+ DataClass(*proxy.positionals, **proxy.defaults, &proxy.block)
67
77
  .new(**values)
68
78
  end
69
79
  end
70
80
  end
71
81
 
72
82
  def _define_methods
83
+ (class << klass; self end).module_eval(&_define_freezing_constructor)
73
84
  klass.module_eval(&_define_to_h)
74
85
  klass.module_eval(&_define_merge)
75
- klass.module_eval(&block) if block
76
86
  end
77
87
 
78
88
  def _define_to_h
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Lab42
4
4
  module DataClass
5
- VERSION = "0.1.1"
5
+ VERSION = "0.3.1"
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.1
4
+ version: 0.3.1
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-23 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