active_module 0.6.8 → 0.8.0
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 +377 -105
- data/lib/active_module/base.rb +3 -7
- data/lib/active_module/enum/class_methods_definer.rb +49 -0
- data/lib/active_module/enum/instance_methods_definer.rb +24 -0
- data/lib/active_module/enum/util.rb +79 -0
- data/lib/active_module/enum.rb +86 -1
- data/lib/active_module/module_refinement.rb +23 -9
- data/lib/active_module/modules_index.rb +6 -2
- data/lib/active_module/register.rb +1 -0
- data/lib/active_module/version.rb +1 -1
- metadata +28 -8
- data/lib/active_module/enum/comparison.rb +0 -28
- data/lib/active_module/enum/module_refinement.rb +0 -32
- data/lib/active_module/enum/modules_index.rb +0 -19
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 380e377e525e9ed59c3154706e0183a744ff91660db261aa3a4ee012ad8feaf0
|
|
4
|
+
data.tar.gz: 615e920f65193a34f20adb0480311f86f16dfcd8d9ae9c68bfafca1b38af8919
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3264235d103059314e29f031c7ec6d1b7d3edc37c65e415bb61a14d0ae71d58edd794c1eeaf7ad7dc5e211d9e5b6a891861fef6e934e1bce1552558db4a26803
|
|
7
|
+
data.tar.gz: 20f7c237d5d2c2b7e4088e6c050fb4b1115460e0dd051ec2a08fcd019d6fa624e6a4bfadb559912c1384fe69f38f9d1ae39f5cd21e0ecc0174f9ad33a1a68e65
|
data/README.md
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
# active_module
|
|
3
2
|
[](https://rubygems.org/gems/active_module)
|
|
4
3
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -6,9 +5,7 @@
|
|
|
6
5
|
[](https://github.com/pedrorolo/active_module/blob/main/spec/spec_helper.rb)
|
|
7
6
|
[](https://bestgems.org/gems/active_module)
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
#### *Modules and Classes as first-class active record values!*
|
|
8
|
+
*Modules and Classes as first-class active record values!*
|
|
12
9
|
|
|
13
10
|
ActiveModel/ActiveRecord implementation of the Module attribute type.
|
|
14
11
|
|
|
@@ -23,38 +20,46 @@ This is a very generic mechanism that enables many possible utilizations, for in
|
|
|
23
20
|
- **Static configuration management**
|
|
24
21
|
- **Rich Java/C#-like enums**
|
|
25
22
|
|
|
26
|
-
You can find examples of these in [Usage -> Examples](#
|
|
23
|
+
You can find examples of these in [Usage -> Examples](#examples).
|
|
27
24
|
|
|
28
25
|
## TL;DR
|
|
29
26
|
|
|
30
27
|
Declare module attributes like this:
|
|
28
|
+
|
|
31
29
|
```ruby
|
|
32
30
|
class MyARObject < ActiveRecord::Base
|
|
33
|
-
attribute :module_field,
|
|
34
|
-
:active_module,
|
|
31
|
+
attribute :module_field,
|
|
32
|
+
:active_module,
|
|
35
33
|
possible_modules: [MyModule1, MyClass, Nested::Module]
|
|
34
|
+
|
|
35
|
+
# Optional: adds Rails-enum-like helpers (my_module1?, my_module1!,
|
|
36
|
+
# with_my_module1, ...). Drop this line if you don't need them:
|
|
37
|
+
active_module_enum :module_field
|
|
36
38
|
end
|
|
37
39
|
```
|
|
38
40
|
|
|
39
41
|
Assign them like this:
|
|
40
|
-
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
41
44
|
object.module_field = Nested::Module
|
|
42
45
|
object.module_field = :Module
|
|
43
46
|
object.module_field = "Module"
|
|
44
|
-
object.module_field
|
|
47
|
+
object.module_field = :nested_module # underscored nested name
|
|
48
|
+
object.module_field #=> Nested::Module
|
|
45
49
|
```
|
|
46
50
|
|
|
47
51
|
Query them like this:
|
|
48
|
-
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
49
54
|
MyARObject.where(module_field: Nested::Module)
|
|
50
55
|
MyARObject.where(module_field: :Module)
|
|
51
56
|
MyARObject.where(module_field: "Module")
|
|
52
|
-
|
|
57
|
+
MyARObject.where(module_field: :nested_module) # underscored nested name
|
|
53
58
|
```
|
|
54
59
|
|
|
55
60
|
And compare them like this:
|
|
56
61
|
|
|
57
|
-
```ruby
|
|
62
|
+
```ruby
|
|
58
63
|
object.module_field == Nested::Module
|
|
59
64
|
|
|
60
65
|
module MyNameSpace
|
|
@@ -62,6 +67,7 @@ module MyNameSpace
|
|
|
62
67
|
|
|
63
68
|
object.module_field =~ :Module
|
|
64
69
|
object.module_field =~ "Module"
|
|
70
|
+
object.module_field =~ :nested_module # underscored nested name
|
|
65
71
|
end
|
|
66
72
|
```
|
|
67
73
|
|
|
@@ -70,7 +76,7 @@ end
|
|
|
70
76
|
Add to your gemfile - and if you are using rails - that's all you need:
|
|
71
77
|
|
|
72
78
|
```ruby
|
|
73
|
-
gem
|
|
79
|
+
gem "active_module", "~> 0.8"
|
|
74
80
|
```
|
|
75
81
|
|
|
76
82
|
If you are not using rails, just issue this command after loading active record
|
|
@@ -89,7 +95,8 @@ ActiveRecord::Type.register(:active_module, ActiveModule::Base)
|
|
|
89
95
|
|
|
90
96
|
## Usage
|
|
91
97
|
|
|
92
|
-
Add a string field to the table you want to hold a module attribute in your migrations
|
|
98
|
+
Add a string field to the table you want to hold a module attribute in your migrations:
|
|
99
|
+
|
|
93
100
|
```ruby
|
|
94
101
|
create_table :my_ar_objects do |t|
|
|
95
102
|
t.string :module_field, index: true
|
|
@@ -97,74 +104,120 @@ end
|
|
|
97
104
|
```
|
|
98
105
|
|
|
99
106
|
Now given this random module hierarchy:
|
|
107
|
+
|
|
100
108
|
```ruby
|
|
101
109
|
class MyARObject < ActiveRecord::Base
|
|
102
110
|
module MyModule1; end
|
|
103
111
|
module MyModule2; end
|
|
104
|
-
|
|
112
|
+
|
|
113
|
+
class MyClass
|
|
105
114
|
module MyModule1; end
|
|
106
115
|
end
|
|
107
116
|
end
|
|
108
117
|
```
|
|
118
|
+
|
|
109
119
|
You can make the field refer to one of these modules/classes like this:
|
|
120
|
+
|
|
110
121
|
```ruby
|
|
111
122
|
class MyARObject < ActiveRecord::Base
|
|
112
|
-
attribute :module_field,
|
|
113
|
-
:active_module,
|
|
123
|
+
attribute :module_field,
|
|
124
|
+
:active_module,
|
|
114
125
|
possible_modules: [MyModule1, MyModule2, MyClass, MyClass::MyModule1]
|
|
115
126
|
end
|
|
116
127
|
```
|
|
117
128
|
|
|
118
|
-
Optionally, you can specify how to map your modules into the database
|
|
129
|
+
Optionally, you can specify how to map your modules into the database
|
|
119
130
|
(the default is the module's fully qualified name):
|
|
131
|
+
|
|
120
132
|
```ruby
|
|
121
|
-
attribute :module_field,
|
|
122
|
-
:active_module,
|
|
123
|
-
possible_modules: [MyModule1, MyModule2, MyClass, MyClass::MyModule1]
|
|
124
|
-
mapping: {MyModule1 => "
|
|
133
|
+
attribute :module_field,
|
|
134
|
+
:active_module,
|
|
135
|
+
possible_modules: [MyModule1, MyModule2, MyClass, MyClass::MyModule1],
|
|
136
|
+
mapping: { MyModule1 => "m1" }
|
|
125
137
|
```
|
|
126
138
|
|
|
127
|
-
|
|
139
|
+
Modules not included in the mapping hash will use their fully qualified
|
|
140
|
+
name as the database representation. Assignment and querying still work
|
|
141
|
+
with module literals, symbols, and strings:
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
my_ar_object.module_field = :MyModule1
|
|
145
|
+
my_ar_object.module_field #=> MyARObject::MyModule1
|
|
146
|
+
|
|
147
|
+
MyARObject.where(module_field: :MyModule1)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The mapping only affects what is stored in the database column.
|
|
151
|
+
|
|
152
|
+
And this is it! Easy!
|
|
128
153
|
|
|
129
154
|
### Assigning and querying module attributes
|
|
155
|
+
|
|
130
156
|
Now you can use this attribute in many handy ways!
|
|
131
|
-
<br>
|
|
132
|
-
<br>
|
|
133
|
-
For instance, you may refer to it using module literals:
|
|
134
|
-
```ruby
|
|
135
|
-
MyARObject.create!(module_field: MyARObject::MyModule1)
|
|
136
157
|
|
|
137
|
-
|
|
158
|
+
The most ergonomic way is to use underscored symbols. For flat modules,
|
|
159
|
+
use the underscored name directly:
|
|
138
160
|
|
|
139
|
-
|
|
161
|
+
```ruby
|
|
162
|
+
MyARObject.create!(module_field: :my_module1)
|
|
140
163
|
|
|
141
|
-
|
|
164
|
+
MyARObject.where(module_field: :my_module1)
|
|
142
165
|
|
|
166
|
+
my_ar_object.module_field = :my_module1
|
|
167
|
+
|
|
168
|
+
my_ar_object.module_field #=> MyARObject::MyModule1
|
|
143
169
|
```
|
|
144
|
-
|
|
170
|
+
|
|
171
|
+
Nested modules can be referenced using underscored symbols at any
|
|
172
|
+
nesting level:
|
|
145
173
|
|
|
146
174
|
```ruby
|
|
147
|
-
MyARObject.create!(module_field: :
|
|
175
|
+
MyARObject.create!(module_field: :my_class_my_module1) # partial nesting
|
|
176
|
+
MyARObject.create!(module_field: :my_module1) # demodulized name
|
|
177
|
+
MyARObject.where(module_field: :my_class_my_module1) # all segments joined
|
|
178
|
+
|
|
179
|
+
my_ar_object.module_field = :my_class_my_module1
|
|
180
|
+
my_ar_object.module_field #=> MyARObject::MyClass::MyModule1
|
|
181
|
+
```
|
|
148
182
|
|
|
149
|
-
|
|
183
|
+
When a demodulized name is ambiguous (shared by modules at different
|
|
184
|
+
nesting levels), the least-nested module always wins for assignment
|
|
185
|
+
and querying:
|
|
150
186
|
|
|
151
|
-
|
|
187
|
+
```ruby
|
|
188
|
+
# Given possible_modules: [MyModule1, MyClass::MyModule1]
|
|
189
|
+
# :MyModule1 resolves to the flat MyModule1 (not MyClass::MyModule1)
|
|
152
190
|
|
|
153
|
-
|
|
191
|
+
MyARObject.create!(module_field: :MyModule1) # sets to MyModule1
|
|
192
|
+
MyARObject.where(module_field: :MyModule1) # filters by MyModule1
|
|
193
|
+
my_ar_object.module_field = :MyModule1 # assigns MyModule1
|
|
154
194
|
|
|
195
|
+
# Use the underscored nested name for the nested module
|
|
196
|
+
MyARObject.where(module_field: :my_class_my_module1) # filters by MyClass::MyModule1
|
|
155
197
|
```
|
|
156
198
|
|
|
157
|
-
|
|
199
|
+
You can always refer to modules using their fully qualified names
|
|
200
|
+
via symbols or module literals:
|
|
158
201
|
|
|
159
202
|
```ruby
|
|
203
|
+
MyARObject.create!(module_field: :MyModule1)
|
|
204
|
+
MyARObject.create!(module_field: MyARObject::MyModule1)
|
|
205
|
+
|
|
206
|
+
MyARObject.where(module_field: :MyModule1)
|
|
207
|
+
MyARObject.where(module_field: MyARObject::MyModule1)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
And if there is the need for disambiguation, you can always use
|
|
211
|
+
fully qualified strings:
|
|
160
212
|
|
|
213
|
+
```ruby
|
|
161
214
|
MyARObject.create!(module_field: "MyClass::MyModule1")
|
|
162
215
|
|
|
163
216
|
MyARObject.where(module_field: "MyClass::MyModule1")
|
|
164
217
|
|
|
165
218
|
my_ar_object.module_field = "MyClass::MyModule1"
|
|
166
219
|
|
|
167
|
-
my_ar_object.module_field #=> MyARObject::MyClass::
|
|
220
|
+
my_ar_object.module_field #=> MyARObject::MyClass::MyModule1
|
|
168
221
|
```
|
|
169
222
|
|
|
170
223
|
### Comparing modules with strings and symbols
|
|
@@ -178,7 +231,9 @@ module YourClassOrModuleThatWantsToCompare
|
|
|
178
231
|
using ActiveModule::Comparison
|
|
179
232
|
|
|
180
233
|
def method_that_compares
|
|
181
|
-
my_ar_object.module_field =~ :
|
|
234
|
+
my_ar_object.module_field =~ :my_module1 # underscored name
|
|
235
|
+
my_ar_object.module_field =~ :my_class_my_module1 # nested underscored
|
|
236
|
+
my_ar_object.module_field =~ "MyClass::MyModule1" # fully qualified string
|
|
182
237
|
end
|
|
183
238
|
end
|
|
184
239
|
```
|
|
@@ -186,7 +241,7 @@ end
|
|
|
186
241
|
or like this, if you don't want to use the refinement:
|
|
187
242
|
|
|
188
243
|
```ruby
|
|
189
|
-
ActiveModule::Comparison.compare(my_ar_object.module_field, :
|
|
244
|
+
ActiveModule::Comparison.compare(my_ar_object.module_field, :my_module1)
|
|
190
245
|
```
|
|
191
246
|
|
|
192
247
|
but in this last case it would probably make more sense to simply use a module literal:
|
|
@@ -200,10 +255,11 @@ my_ar_object.module_field == MyClass::MyModule1
|
|
|
200
255
|
|
|
201
256
|
### Composition-based polymorphism (Strategy design pattern)
|
|
202
257
|
|
|
203
|
-
[The Strategy design pattern](https://en.wikipedia.org/wiki/Strategy_pattern) allows composition based polymorphism. This enables runtime polymorphism (by changing the strategy in runtime),
|
|
258
|
+
[The Strategy design pattern](https://en.wikipedia.org/wiki/Strategy_pattern) allows composition based polymorphism. This enables runtime polymorphism (by changing the strategy in runtime),
|
|
204
259
|
and multiple-polymorphism (by composing an object of multiple strategies).
|
|
205
260
|
|
|
206
|
-
If you want to use classes this will do:
|
|
261
|
+
If you want to use classes this will do:
|
|
262
|
+
|
|
207
263
|
```ruby
|
|
208
264
|
class MyARObject < ActiveRecord::Base
|
|
209
265
|
attribute :strategy_class, :active_module, possible_modules: StrategySuperclass.subclasses
|
|
@@ -235,8 +291,8 @@ class MyARObject < ActiveRecord::Base
|
|
|
235
291
|
end
|
|
236
292
|
end
|
|
237
293
|
|
|
238
|
-
attribute :strategy,
|
|
239
|
-
:active_module,
|
|
294
|
+
attribute :strategy,
|
|
295
|
+
:active_module,
|
|
240
296
|
possible_modules: [Strategy1, Strategy2]
|
|
241
297
|
|
|
242
298
|
def run_strategy!(some_args)
|
|
@@ -244,8 +300,8 @@ class MyARObject < ActiveRecord::Base
|
|
|
244
300
|
end
|
|
245
301
|
end
|
|
246
302
|
|
|
247
|
-
MyARObject.create!(
|
|
248
|
-
MyARObject.create!(
|
|
303
|
+
MyARObject.create!(strategy: :Strategy1).run_strategy! #=> "strategy1 called"
|
|
304
|
+
MyARObject.create!(strategy: :Strategy2).run_strategy! #=> "strategy2 called"
|
|
249
305
|
```
|
|
250
306
|
|
|
251
307
|
You can later easily promote these modules to classes if you need instance variables:
|
|
@@ -268,8 +324,8 @@ class MyARObject < ActiveRecord::Base
|
|
|
268
324
|
end
|
|
269
325
|
end
|
|
270
326
|
|
|
271
|
-
attribute :strategy,
|
|
272
|
-
:active_module,
|
|
327
|
+
attribute :strategy,
|
|
328
|
+
:active_module,
|
|
273
329
|
possible_modules: [Strategy1, Strategy2]
|
|
274
330
|
|
|
275
331
|
def run_strategy!(some_args)
|
|
@@ -277,17 +333,17 @@ class MyARObject < ActiveRecord::Base
|
|
|
277
333
|
end
|
|
278
334
|
end
|
|
279
335
|
|
|
280
|
-
MyARObject.create!(
|
|
281
|
-
MyARObject.create!(
|
|
336
|
+
MyARObject.create!(strategy: :Strategy1).run_strategy! #=> "strategy1 called"
|
|
337
|
+
MyARObject.create!(strategy: :Strategy2).run_strategy! #=> "strategy2 called"
|
|
282
338
|
```
|
|
283
339
|
|
|
284
340
|
|
|
285
341
|
### Rapid prototyping static domain objects
|
|
286
342
|
|
|
287
|
-
```ruby
|
|
343
|
+
```ruby
|
|
288
344
|
# Provider domain Object
|
|
289
345
|
module Provider
|
|
290
|
-
|
|
346
|
+
# As if the domain model class
|
|
291
347
|
def self.all
|
|
292
348
|
[Ebay, Amazon]
|
|
293
349
|
end
|
|
@@ -307,20 +363,21 @@ module Provider
|
|
|
307
363
|
end
|
|
308
364
|
|
|
309
365
|
class MyARObject < ActiveRecord::Base
|
|
310
|
-
attribute :provider,
|
|
311
|
-
:active_module,
|
|
366
|
+
attribute :provider,
|
|
367
|
+
:active_module,
|
|
312
368
|
possible_modules: Provider.all
|
|
313
369
|
end
|
|
314
370
|
|
|
315
|
-
MyARObject.create!(provider: :Ebay).
|
|
371
|
+
MyARObject.create!(provider: :Ebay).provider.do_something!
|
|
316
372
|
#=> "do something with the ebay provider config"
|
|
317
|
-
MyARObject.create!(provider: Provider::Amazon).provider.do_something!
|
|
373
|
+
MyARObject.create!(provider: Provider::Amazon).provider.do_something!
|
|
318
374
|
#=> "do something with the amazon provider config"
|
|
319
375
|
```
|
|
320
376
|
|
|
321
377
|
What is interesting about this is that we can later easily promote
|
|
322
|
-
our provider objects into full fledged ActiveRecord objects without
|
|
378
|
+
our provider objects into full fledged ActiveRecord objects without
|
|
323
379
|
big changes to our code:
|
|
380
|
+
|
|
324
381
|
```ruby
|
|
325
382
|
class Provider < ActiveRecord::Base
|
|
326
383
|
def do_something!
|
|
@@ -333,10 +390,10 @@ class MyARObject < ActiveRecord::Base
|
|
|
333
390
|
end
|
|
334
391
|
```
|
|
335
392
|
|
|
336
|
-
Just in case you'd like to have shared code amongst the instances in the above example,
|
|
393
|
+
Just in case you'd like to have shared code amongst the instances in the above example,
|
|
337
394
|
this is how you could do so:
|
|
338
395
|
|
|
339
|
-
```ruby
|
|
396
|
+
```ruby
|
|
340
397
|
# Provider domain Object
|
|
341
398
|
module Provider
|
|
342
399
|
# As if the domain model class
|
|
@@ -344,7 +401,7 @@ module Provider
|
|
|
344
401
|
[Ebay, Amazon]
|
|
345
402
|
end
|
|
346
403
|
|
|
347
|
-
module Base
|
|
404
|
+
module Base
|
|
348
405
|
def do_something!
|
|
349
406
|
"do something with #{something_from_an_instance}"
|
|
350
407
|
end
|
|
@@ -375,24 +432,24 @@ end
|
|
|
375
432
|
### Static configuration management
|
|
376
433
|
|
|
377
434
|
This example is not much different than previous one. It however stresses that the module we
|
|
378
|
-
refer to might be used as a source of configuration parameters that change the behaviour of
|
|
435
|
+
refer to might be used as a source of configuration parameters that change the behaviour of
|
|
379
436
|
the class it belongs to:
|
|
380
437
|
|
|
381
|
-
```ruby
|
|
438
|
+
```ruby
|
|
382
439
|
# Provider domain Object
|
|
383
440
|
module ProviderConfig
|
|
384
441
|
module Ebay
|
|
385
442
|
module_function
|
|
386
443
|
|
|
387
|
-
def url=
|
|
388
|
-
def number_of_attempts= 5
|
|
444
|
+
def url = "www.ebay.com"
|
|
445
|
+
def number_of_attempts = 5
|
|
389
446
|
end
|
|
390
447
|
|
|
391
448
|
module Amazon
|
|
392
449
|
module_function
|
|
393
450
|
|
|
394
|
-
def url=
|
|
395
|
-
def number_of_attempts= 10
|
|
451
|
+
def url = "www.amazon.com"
|
|
452
|
+
def number_of_attempts = 10
|
|
396
453
|
end
|
|
397
454
|
|
|
398
455
|
def self.all
|
|
@@ -401,19 +458,19 @@ module ProviderConfig
|
|
|
401
458
|
end
|
|
402
459
|
|
|
403
460
|
class MyARObject < ActiveRecord::Base
|
|
404
|
-
attribute :provider_config,
|
|
405
|
-
:active_module,
|
|
461
|
+
attribute :provider_config,
|
|
462
|
+
:active_module,
|
|
406
463
|
possible_modules: ProviderConfig.all
|
|
407
464
|
|
|
408
465
|
def load_page!
|
|
409
466
|
n_attempts = 0
|
|
410
467
|
result = nil
|
|
411
|
-
while n_attempts <
|
|
412
|
-
result = get_page(
|
|
413
|
-
if
|
|
468
|
+
while n_attempts < provider_config.number_of_attempts
|
|
469
|
+
result = get_page(provider_config.url)
|
|
470
|
+
if result
|
|
414
471
|
return result
|
|
415
472
|
else
|
|
416
|
-
n_attempts
|
|
473
|
+
n_attempts += 1
|
|
417
474
|
end
|
|
418
475
|
end
|
|
419
476
|
result
|
|
@@ -423,12 +480,61 @@ end
|
|
|
423
480
|
MyARObject.create!(provider_config: :Ebay).load_page!
|
|
424
481
|
```
|
|
425
482
|
|
|
426
|
-
### Rich Java/C#-like enums
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
483
|
+
### Rich Java/C#-like enums with `active_module_enum`
|
|
484
|
+
|
|
485
|
+
Java/C# enums allow defining methods on the enum, which are shared across all enum values.
|
|
486
|
+
ActiveModule supports this pattern with `active_module_enum`, which generates query, bang,
|
|
487
|
+
and scope methods for your active_module attributes.
|
|
488
|
+
|
|
489
|
+
Nested modules can always be referenced using underscored names at any level of nesting.
|
|
490
|
+
For example, given a deeply nested module:
|
|
491
|
+
|
|
492
|
+
```ruby
|
|
493
|
+
module Lime
|
|
494
|
+
module Banana
|
|
495
|
+
module Strawberry; end
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
class MyARObject < ActiveRecord::Base
|
|
500
|
+
attribute :fruit,
|
|
501
|
+
:active_module,
|
|
502
|
+
possible_modules: [Lime::Banana::Strawberry]
|
|
503
|
+
end
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
All of the following resolve to `Lime::Banana::Strawberry`:
|
|
507
|
+
|
|
508
|
+
```ruby
|
|
509
|
+
MyARObject.create!(fruit: :strawberry) # last segment only
|
|
510
|
+
MyARObject.create!(fruit: :banana_strawberry) # last two segments
|
|
511
|
+
MyARObject.create!(fruit: :lime_banana_strawberry) # all segments
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
This also works for querying:
|
|
515
|
+
|
|
516
|
+
```ruby
|
|
517
|
+
MyARObject.where(fruit: :banana_strawberry)
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
And for comparison (with `ActiveModule::Comparison`):
|
|
521
|
+
|
|
522
|
+
```ruby
|
|
523
|
+
module MyModuleOrClass
|
|
524
|
+
using ActiveModule::Comparison
|
|
525
|
+
|
|
526
|
+
def self.match?(mod, value)
|
|
527
|
+
mod =~ value
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
MyModuleOrClass.match?(Lime::Banana::Strawberry, :banana_strawberry) #=> true
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
#### `active_module_enum` — generating query, bang, and scope methods
|
|
430
535
|
|
|
431
|
-
|
|
536
|
+
The `active_module_enum` method generates Rails-enum-style convenience methods
|
|
537
|
+
for your active_module attributes:
|
|
432
538
|
|
|
433
539
|
```ruby
|
|
434
540
|
module PipelineStage
|
|
@@ -438,55 +544,217 @@ module PipelineStage
|
|
|
438
544
|
[InitialContact, InNegotiations, LostDeal, PaidOut]
|
|
439
545
|
end
|
|
440
546
|
|
|
441
|
-
def cast(stage)
|
|
442
|
-
self.all.map(&:external_provider_code).find{|code| code == stage} ||
|
|
443
|
-
self.all.map(&:database_representation).find{|code| code == stage} ||
|
|
444
|
-
self.all.map(&:frontend_representation).find{|code| code == stage}
|
|
445
|
-
end
|
|
446
|
-
|
|
447
547
|
module Base
|
|
448
548
|
def external_provider_code
|
|
449
549
|
@external_provider_code ||= self.name.underscore
|
|
450
550
|
end
|
|
451
551
|
|
|
452
|
-
def database_representation
|
|
453
|
-
self.name
|
|
454
|
-
end
|
|
455
|
-
|
|
456
552
|
def frontend_representation
|
|
457
553
|
@frontend_representation ||= self.name.demodulize.upcase
|
|
458
554
|
end
|
|
459
555
|
end
|
|
460
556
|
|
|
461
|
-
module InitialContact
|
|
462
|
-
|
|
463
|
-
end
|
|
557
|
+
module InitialContact; extend Base; end
|
|
558
|
+
module InNegotiations; extend Base; end
|
|
559
|
+
module LostDeal; extend Base; end
|
|
560
|
+
module PaidOut; extend Base; end
|
|
561
|
+
end
|
|
464
562
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
563
|
+
class MyARObject < ActiveRecord::Base
|
|
564
|
+
attribute :pipeline_stage,
|
|
565
|
+
:active_module,
|
|
566
|
+
possible_modules: PipelineStage.all
|
|
468
567
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
568
|
+
active_module_enum :pipeline_stage
|
|
569
|
+
end
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
This generates:
|
|
472
573
|
|
|
473
|
-
|
|
474
|
-
|
|
574
|
+
```ruby
|
|
575
|
+
# Instance query methods (?)
|
|
576
|
+
object = MyARObject.new(pipeline_stage: :initial_contact)
|
|
577
|
+
object.initial_contact? #=> true
|
|
578
|
+
object.lost_deal? #=> false
|
|
579
|
+
|
|
580
|
+
# Instance bang methods (!) — set and save
|
|
581
|
+
object.initial_contact!
|
|
582
|
+
object.reload
|
|
583
|
+
object.pipeline_stage #=> PipelineStage::InitialContact
|
|
584
|
+
|
|
585
|
+
# Class-level scopes
|
|
586
|
+
MyARObject.with_initial_contact #=> ActiveRecord::Relation
|
|
587
|
+
MyARObject.with_lost_deal #=> ActiveRecord::Relation
|
|
588
|
+
|
|
589
|
+
# Class-level query methods (same as scopes)
|
|
590
|
+
MyARObject.initial_contact #=> ActiveRecord::Relation
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
The pluralized attribute name method returns a hash mapping
|
|
594
|
+
modules to their fully qualified names:
|
|
595
|
+
|
|
596
|
+
```ruby
|
|
597
|
+
MyARObject.pipeline_stages
|
|
598
|
+
#=> { PipelineStage::InitialContact => "PipelineStage::InitialContact",
|
|
599
|
+
# PipelineStage::InNegotiations => "PipelineStage::InNegotiations",
|
|
600
|
+
# PipelineStage::LostDeal => "PipelineStage::LostDeal",
|
|
601
|
+
# PipelineStage::PaidOut => "PipelineStage::PaidOut" }
|
|
602
|
+
|
|
603
|
+
MyARObject.pipeline_stages.keys #=> [PipelineStage::InitialContact, ...]
|
|
604
|
+
MyARObject.pipeline_stages.values #=> ["PipelineStage::InitialContact", ...]
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
All forms of underscored symbol names work for assignment and querying:
|
|
608
|
+
|
|
609
|
+
```ruby
|
|
610
|
+
MyARObject.create!(pipeline_stage: :initial_contact)
|
|
611
|
+
MyARObject.create!(pipeline_stage: :in_negotiations)
|
|
612
|
+
MyARObject.where(pipeline_stage: :lost_deal)
|
|
613
|
+
MyARObject.where(pipeline_stage: :paid_out)
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
For nested modules, methods are generated at **all nesting levels**:
|
|
617
|
+
|
|
618
|
+
```ruby
|
|
619
|
+
module Lime
|
|
620
|
+
module Banana
|
|
621
|
+
module Strawberry; end
|
|
475
622
|
end
|
|
476
623
|
end
|
|
477
624
|
|
|
625
|
+
class Fruit < ActiveRecord::Base
|
|
626
|
+
attribute :kind, :active_module,
|
|
627
|
+
possible_modules: [Lime::Banana::Strawberry]
|
|
628
|
+
active_module_enum :kind
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
object = Fruit.new(kind: :strawberry)
|
|
632
|
+
object.strawberry? #=> true
|
|
633
|
+
object.banana_strawberry? #=> true (partial nesting)
|
|
634
|
+
object.lime_banana_strawberry? #=> true (full nesting)
|
|
635
|
+
|
|
636
|
+
Fruit.with_banana_strawberry #=> ActiveRecord::Relation
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
The pluralized attribute name method also works with nested modules.
|
|
640
|
+
Each module maps to its fully qualified name:
|
|
641
|
+
|
|
642
|
+
```ruby
|
|
643
|
+
module StatusA; end
|
|
644
|
+
module StatusB; end
|
|
645
|
+
module Nested
|
|
646
|
+
module StatusA; end
|
|
647
|
+
module StatusB; end
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
class Fruit < ActiveRecord::Base
|
|
651
|
+
attribute :kind, :active_module,
|
|
652
|
+
possible_modules: [StatusA, StatusB,
|
|
653
|
+
Nested::StatusA, Nested::StatusB]
|
|
654
|
+
active_module_enum :kind
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
Fruit.kinds
|
|
658
|
+
#=> { StatusA => "StatusA", StatusB => "StatusB",
|
|
659
|
+
# Nested::StatusA => "Nested::StatusA",
|
|
660
|
+
# Nested::StatusB => "Nested::StatusB" }
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
All underscored forms work for assignment and querying:
|
|
664
|
+
|
|
665
|
+
```ruby
|
|
666
|
+
Fruit.create!(kind: :strawberry)
|
|
667
|
+
Fruit.create!(kind: :banana_strawberry)
|
|
668
|
+
Fruit.create!(kind: :lime_banana_strawberry)
|
|
669
|
+
Fruit.where(kind: :banana_strawberry)
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
##### Options
|
|
673
|
+
|
|
674
|
+
`active_module_enum` accepts the following options:
|
|
675
|
+
|
|
676
|
+
```ruby
|
|
677
|
+
active_module_enum :pipeline_stage,
|
|
678
|
+
prefix: true, # prefix method names with the attribute name
|
|
679
|
+
suffix: true, # suffix method names with the attribute name
|
|
680
|
+
scope: true, # generate with_ scopes (default: true)
|
|
681
|
+
instance_methods: true, # generate ? and ! methods (default: true)
|
|
682
|
+
on_ambiguous: :warn # :warn or :silence (default: :warn)
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
- **`prefix: true`** — prefixes methods with the attribute name:
|
|
686
|
+
`pipeline_stage_initial_contact?`, `with_pipeline_stage_initial_contact`
|
|
687
|
+
- **`prefix: "custom"`** — prefixes with a custom string:
|
|
688
|
+
`custom_initial_contact?`, `with_custom_initial_contact`
|
|
689
|
+
- **`suffix: true`** — suffixes methods with the attribute name:
|
|
690
|
+
`initial_contact_pipeline_stage?`, `with_initial_contact_pipeline_stage`
|
|
691
|
+
- **`suffix: "custom"`** — suffixes with a custom string:
|
|
692
|
+
`initial_contact_custom?`, `with_initial_contact_custom`
|
|
693
|
+
- **`scope: false`** — skips scope generation
|
|
694
|
+
- **`instance_methods: false`** — skips `?` and `!` method generation
|
|
695
|
+
- **`on_ambiguous: :silence`** — suppresses warnings when multiple
|
|
696
|
+
modules share the same demodulized name (e.g. `Tino` and
|
|
697
|
+
`Banana::Tino` both producing `tino?`)
|
|
698
|
+
|
|
699
|
+
##### Ambiguity resolution
|
|
700
|
+
|
|
701
|
+
When two modules at different nesting levels produce the same demodulized
|
|
702
|
+
name (e.g. `Status` and `Nested::Status` both mapping to `status?`),
|
|
703
|
+
**the least-nested module always wins** across all contexts —
|
|
704
|
+
assignment, querying, scopes, and enum methods:
|
|
705
|
+
|
|
706
|
+
```ruby
|
|
707
|
+
module Status; end
|
|
708
|
+
module Nested
|
|
709
|
+
module Status; end
|
|
710
|
+
end
|
|
711
|
+
|
|
478
712
|
class MyARObject < ActiveRecord::Base
|
|
479
|
-
attribute :
|
|
480
|
-
:
|
|
481
|
-
|
|
713
|
+
attribute :status, :active_module,
|
|
714
|
+
possible_modules: [Status, Nested::Status]
|
|
715
|
+
active_module_enum :status
|
|
482
716
|
end
|
|
483
717
|
|
|
484
|
-
|
|
485
|
-
object.
|
|
486
|
-
object.
|
|
487
|
-
|
|
718
|
+
# Assignment resolves to the flat module
|
|
719
|
+
object = MyARObject.new(status: :status)
|
|
720
|
+
object.status #=> Status (flat, not Nested::Status)
|
|
721
|
+
|
|
722
|
+
# Querying resolves to the flat module
|
|
723
|
+
MyARObject.where(status: :status) # filters by Status
|
|
724
|
+
MyARObject.find_by(status: "Status") # finds Status
|
|
725
|
+
|
|
726
|
+
# Enum query resolves to the flat module
|
|
727
|
+
object.status? #=> true (matches Status)
|
|
728
|
+
object.nested_status? #=> true (use underscored name for Nested::Status)
|
|
729
|
+
|
|
730
|
+
# Bang method resolves to the flat module
|
|
731
|
+
object.status!
|
|
732
|
+
object.reload
|
|
733
|
+
object.status #=> Status
|
|
488
734
|
```
|
|
489
735
|
|
|
736
|
+
To access the nested module, always use its underscored form:
|
|
737
|
+
|
|
738
|
+
```ruby
|
|
739
|
+
object.nested_status? #=> true
|
|
740
|
+
object.nested_status! #=> sets to Nested::Status
|
|
741
|
+
MyARObject.with_nested_status #=> ActiveRecord::Relation filtering by Nested::Status
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
This resolution applies consistently to `with_` scopes, class-level
|
|
745
|
+
query methods, `find_by`/`where`, and assignment via symbol or string —
|
|
746
|
+
the least-nested module wins for the ambiguous name:
|
|
747
|
+
|
|
748
|
+
```ruby
|
|
749
|
+
MyARObject.create!(status: Nested::Status)
|
|
750
|
+
MyARObject.create!(status: Status)
|
|
751
|
+
|
|
752
|
+
MyARObject.status.count #=> 1 (flat Status only)
|
|
753
|
+
MyARObject.with_nested_status.count #=> 1 (Nested::Status only)
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
Note: when both `prefix: true` and `suffix: true` are set, only `prefix` takes
|
|
757
|
+
effect.
|
|
490
758
|
|
|
491
759
|
## Development
|
|
492
760
|
|
|
@@ -497,3 +765,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
|
497
765
|
## Contributing
|
|
498
766
|
|
|
499
767
|
Bug reports and pull requests are welcome on GitHub at https://github.com/pedrorolo/active_module.
|
|
768
|
+
|
|
769
|
+
## License
|
|
770
|
+
|
|
771
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/lib/active_module/base.rb
CHANGED
|
@@ -6,9 +6,7 @@ module ActiveModule
|
|
|
6
6
|
|
|
7
7
|
def initialize(possible_modules_or_mapping = [],
|
|
8
8
|
possible_modules: [],
|
|
9
|
-
mapping: {}
|
|
10
|
-
enum_compatibility: false)
|
|
11
|
-
@enum_compatibility = enum_compatibility
|
|
9
|
+
mapping: {})
|
|
12
10
|
if possible_modules_or_mapping.is_a?(Array)
|
|
13
11
|
@possible_modules =
|
|
14
12
|
(possible_modules_or_mapping + possible_modules + mapping.keys).uniq
|
|
@@ -72,7 +70,7 @@ module ActiveModule
|
|
|
72
70
|
end
|
|
73
71
|
|
|
74
72
|
def str_to_module(str)
|
|
75
|
-
modules_index[str
|
|
73
|
+
modules_index[str] ||
|
|
76
74
|
raise_invalid_module_value_error(str)
|
|
77
75
|
end
|
|
78
76
|
|
|
@@ -93,9 +91,7 @@ module ActiveModule
|
|
|
93
91
|
end
|
|
94
92
|
|
|
95
93
|
def modules_index
|
|
96
|
-
@modules_index ||=
|
|
97
|
-
(@enum_compatibility ? Enum::ModulesIndex : ModulesIndex)
|
|
98
|
-
.new(@possible_modules)
|
|
94
|
+
@modules_index ||= ModulesIndex.new(@possible_modules)
|
|
99
95
|
end
|
|
100
96
|
|
|
101
97
|
def from_db
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveModule
|
|
4
|
+
module Enum
|
|
5
|
+
module ClassMethodsDefiner
|
|
6
|
+
def define_class_query(attribute_name, mod,
|
|
7
|
+
value_name, opts,
|
|
8
|
+
existing)
|
|
9
|
+
name = enum_method_name(value_name, opts)
|
|
10
|
+
return if existing[:singleton]
|
|
11
|
+
.include?(name.to_sym)
|
|
12
|
+
return if respond_to?(name)
|
|
13
|
+
|
|
14
|
+
define_singleton_method(name) do
|
|
15
|
+
where(attribute_name => mod)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def define_enum_scope(attribute_name, mod,
|
|
20
|
+
value_name, opts,
|
|
21
|
+
existing)
|
|
22
|
+
name = enum_method_name(value_name, opts)
|
|
23
|
+
scope_name = :"with_#{name}"
|
|
24
|
+
return if existing[:singleton]
|
|
25
|
+
.include?(scope_name)
|
|
26
|
+
return if respond_to?(scope_name)
|
|
27
|
+
|
|
28
|
+
define_singleton_method(scope_name) do
|
|
29
|
+
where(attribute_name => mod)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def define_fields_method(attribute_name, modules)
|
|
34
|
+
fields = build_fields_map(modules)
|
|
35
|
+
name = attribute_name.to_s.pluralize
|
|
36
|
+
return if singleton_methods.include?(name.to_sym)
|
|
37
|
+
return if respond_to?(name)
|
|
38
|
+
|
|
39
|
+
define_singleton_method(name) { fields }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def build_fields_map(modules)
|
|
43
|
+
modules.each_with_object({}) do |mod, h|
|
|
44
|
+
h[mod] = mod.name
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveModule
|
|
4
|
+
module Enum
|
|
5
|
+
module InstanceMethodsDefiner
|
|
6
|
+
def define_instance_methods(attribute_name, mod,
|
|
7
|
+
value_name, opts,
|
|
8
|
+
existing)
|
|
9
|
+
name = enum_method_name(value_name, opts)
|
|
10
|
+
define_if_new(
|
|
11
|
+
existing[:instance], :"#{name}?"
|
|
12
|
+
) do
|
|
13
|
+
public_send(attribute_name) == mod
|
|
14
|
+
end
|
|
15
|
+
define_if_new(
|
|
16
|
+
existing[:instance], :"#{name}!"
|
|
17
|
+
) do
|
|
18
|
+
public_send("#{attribute_name}=", mod)
|
|
19
|
+
save!
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveModule
|
|
4
|
+
module Enum
|
|
5
|
+
module Util
|
|
6
|
+
using ModuleRefinement
|
|
7
|
+
|
|
8
|
+
def determine_prefix(attribute_name, prefix, suffix)
|
|
9
|
+
if prefix.is_a?(String)
|
|
10
|
+
prefix
|
|
11
|
+
elsif prefix == true ||
|
|
12
|
+
(prefix.nil? && suffix == true &&
|
|
13
|
+
!prefix.nil?)
|
|
14
|
+
prefix == true ? attribute_name.to_s : nil
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def determine_suffix(attribute_name, prefix, suffix)
|
|
19
|
+
if suffix.is_a?(String)
|
|
20
|
+
suffix
|
|
21
|
+
elsif suffix == true && prefix != true
|
|
22
|
+
attribute_name.to_s
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def enum_method_name(value_name, opts)
|
|
27
|
+
[opts[:prefix], value_name, opts[:suffix]]
|
|
28
|
+
.compact.join("_")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def define_if_new(existing, name, &block)
|
|
32
|
+
return if existing.include?(name)
|
|
33
|
+
return if new.respond_to?(name)
|
|
34
|
+
|
|
35
|
+
define_method(name, &block)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def warn_if_ambiguous(mod, value_name, opts, maps)
|
|
39
|
+
return unless maps[:ambiguity][value_name]&.size&.> 1
|
|
40
|
+
return if opts[:on_ambiguous] == :silence
|
|
41
|
+
|
|
42
|
+
name = enum_method_name(value_name, opts)
|
|
43
|
+
alts = (maps[:unique][mod] || [])
|
|
44
|
+
.map { |a| "`#{a}`" }
|
|
45
|
+
warn("WARNING: `#{name}` is ambiguous. " \
|
|
46
|
+
"Use #{alts.join(", ")} for " \
|
|
47
|
+
"unambiguous access. " \
|
|
48
|
+
"Pass on_ambiguous: :silence " \
|
|
49
|
+
"to suppress this warning.")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def build_name_maps(modules)
|
|
53
|
+
ambiguity = build_ambiguity_map(modules)
|
|
54
|
+
unique = build_unique_names_map(
|
|
55
|
+
modules, ambiguity
|
|
56
|
+
)
|
|
57
|
+
{ ambiguity: ambiguity, unique: unique }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def build_ambiguity_map(modules)
|
|
61
|
+
modules.each_with_object({}) do |mod, h|
|
|
62
|
+
mod.underscored_names.each do |name|
|
|
63
|
+
(h[name] ||= []) << mod
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def build_unique_names_map(modules, ambiguity)
|
|
69
|
+
modules.each_with_object({}) do |mod, h|
|
|
70
|
+
mod.underscored_names.each do |name|
|
|
71
|
+
next if ambiguity[name].size > 1
|
|
72
|
+
|
|
73
|
+
(h[mod] ||= []) << name
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
data/lib/active_module/enum.rb
CHANGED
|
@@ -1,6 +1,91 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module ActiveModule
|
|
4
|
-
|
|
4
|
+
module Enum
|
|
5
|
+
using ModuleRefinement
|
|
6
|
+
include Util
|
|
7
|
+
include InstanceMethodsDefiner
|
|
8
|
+
include ClassMethodsDefiner
|
|
9
|
+
|
|
10
|
+
def active_module_enum(attribute_name,
|
|
11
|
+
prefix: nil,
|
|
12
|
+
suffix: nil,
|
|
13
|
+
scope: true,
|
|
14
|
+
**options)
|
|
15
|
+
type = attribute_types[attribute_name.to_s]
|
|
16
|
+
unless type.is_a?(ActiveModule::Base)
|
|
17
|
+
raise ArgumentError,
|
|
18
|
+
"#{attribute_name} is not " \
|
|
19
|
+
"an active_module attribute"
|
|
20
|
+
end
|
|
21
|
+
opts = build_enum_opts(
|
|
22
|
+
attribute_name, prefix, suffix,
|
|
23
|
+
scope, options
|
|
24
|
+
)
|
|
25
|
+
mods = type.possible_modules
|
|
26
|
+
define_enum_methods(attribute_name, mods, opts)
|
|
27
|
+
define_fields_method(attribute_name, mods)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def build_enum_opts(attribute_name, prefix,
|
|
33
|
+
suffix, scope, options)
|
|
34
|
+
{
|
|
35
|
+
prefix: determine_prefix(
|
|
36
|
+
attribute_name, prefix, suffix
|
|
37
|
+
),
|
|
38
|
+
suffix: determine_suffix(
|
|
39
|
+
attribute_name, prefix, suffix
|
|
40
|
+
),
|
|
41
|
+
scope: scope,
|
|
42
|
+
instance_methods: options.fetch(
|
|
43
|
+
:instance_methods, true
|
|
44
|
+
),
|
|
45
|
+
on_ambiguous: options.fetch(
|
|
46
|
+
:on_ambiguous, :warn
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def define_enum_methods(attribute_name, modules, opts)
|
|
52
|
+
sorted = modules
|
|
53
|
+
.sort_by { |m| m.name.count("::") }
|
|
54
|
+
maps = build_name_maps(sorted)
|
|
55
|
+
existing = {
|
|
56
|
+
instance: instance_methods,
|
|
57
|
+
singleton: singleton_methods
|
|
58
|
+
}
|
|
59
|
+
sorted.each do |mod|
|
|
60
|
+
generate_methods(
|
|
61
|
+
attribute_name, mod, opts, maps, existing
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def generate_methods(attribute_name, mod, opts,
|
|
67
|
+
maps, existing)
|
|
68
|
+
mod.underscored_names.each do |value_name|
|
|
69
|
+
warn_if_ambiguous(mod, value_name, opts, maps)
|
|
70
|
+
if opts[:instance_methods]
|
|
71
|
+
define_instance_methods(
|
|
72
|
+
attribute_name, mod, value_name,
|
|
73
|
+
opts, existing
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
if opts[:instance_methods]
|
|
77
|
+
define_class_query(
|
|
78
|
+
attribute_name, mod, value_name,
|
|
79
|
+
opts, existing
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
next unless opts[:scope]
|
|
83
|
+
|
|
84
|
+
define_enum_scope(
|
|
85
|
+
attribute_name, mod, value_name,
|
|
86
|
+
opts, existing
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
5
90
|
end
|
|
6
91
|
end
|
|
@@ -4,23 +4,37 @@ module ActiveModule
|
|
|
4
4
|
module ModuleRefinement
|
|
5
5
|
refine ::Module do
|
|
6
6
|
def possible_names
|
|
7
|
-
|
|
8
|
-
[qualified_name].tap do |possible_names|
|
|
9
|
-
loop do
|
|
10
|
-
possible_names << name_parts.join("::").freeze
|
|
11
|
-
name_parts = name_parts.drop(1)
|
|
12
|
-
break if name_parts.empty?
|
|
13
|
-
end
|
|
14
|
-
end
|
|
7
|
+
underscored_names + colon_delimited_names
|
|
15
8
|
end
|
|
16
9
|
|
|
17
10
|
def possible_symbol_names_set
|
|
18
|
-
@possible_symbol_names_set ||=
|
|
11
|
+
@possible_symbol_names_set ||=
|
|
12
|
+
Set.new(possible_names.map(&:to_sym))
|
|
19
13
|
end
|
|
20
14
|
|
|
21
15
|
def qualified_name
|
|
22
16
|
"::#{name}"
|
|
23
17
|
end
|
|
18
|
+
|
|
19
|
+
def underscored_names
|
|
20
|
+
parts = name.split("::")
|
|
21
|
+
(1..parts.length).map do |i|
|
|
22
|
+
parts.last(i).map(&:underscore).join("_")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def colon_delimited_names
|
|
29
|
+
name_parts = name.split("::")
|
|
30
|
+
[qualified_name].tap do |names|
|
|
31
|
+
loop do
|
|
32
|
+
names << name_parts.join("::").freeze
|
|
33
|
+
name_parts = name_parts.drop(1)
|
|
34
|
+
break if name_parts.empty?
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
24
38
|
end
|
|
25
39
|
end
|
|
26
40
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "active_support/core_ext/hash/indifferent_access"
|
|
4
|
+
|
|
3
5
|
# Indexes modules by symbols of their qualified and unqualified names.
|
|
4
6
|
module ActiveModule
|
|
5
7
|
class ModulesIndex
|
|
@@ -22,11 +24,13 @@ module ActiveModule
|
|
|
22
24
|
|
|
23
25
|
def index
|
|
24
26
|
@index ||=
|
|
25
|
-
@modules
|
|
27
|
+
@modules
|
|
28
|
+
.sort_by { |m| -m.name.count("::") }
|
|
29
|
+
.flat_map do |module_instance|
|
|
26
30
|
possible_names(module_instance).map do |name|
|
|
27
31
|
[name.to_sym, module_instance]
|
|
28
32
|
end
|
|
29
|
-
end.to_h.freeze
|
|
33
|
+
end.to_h.with_indifferent_access.freeze
|
|
30
34
|
end
|
|
31
35
|
end
|
|
32
36
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: active_module
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Pedro Rolo
|
|
@@ -13,9 +13,9 @@ dependencies:
|
|
|
13
13
|
name: activemodel
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
|
-
- - "
|
|
16
|
+
- - "<"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 8.
|
|
18
|
+
version: '8.2'
|
|
19
19
|
- - ">="
|
|
20
20
|
- !ruby/object:Gem::Version
|
|
21
21
|
version: '7.1'
|
|
@@ -23,9 +23,29 @@ dependencies:
|
|
|
23
23
|
prerelease: false
|
|
24
24
|
version_requirements: !ruby/object:Gem::Requirement
|
|
25
25
|
requirements:
|
|
26
|
-
- - "
|
|
26
|
+
- - "<"
|
|
27
27
|
- !ruby/object:Gem::Version
|
|
28
|
-
version: 8.
|
|
28
|
+
version: '8.2'
|
|
29
|
+
- - ">="
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '7.1'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: activesupport
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - "<"
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '8.2'
|
|
39
|
+
- - ">="
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '7.1'
|
|
42
|
+
type: :runtime
|
|
43
|
+
prerelease: false
|
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - "<"
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '8.2'
|
|
29
49
|
- - ">="
|
|
30
50
|
- !ruby/object:Gem::Version
|
|
31
51
|
version: '7.1'
|
|
@@ -73,9 +93,9 @@ files:
|
|
|
73
93
|
- lib/active_module/base.rb
|
|
74
94
|
- lib/active_module/comparison.rb
|
|
75
95
|
- lib/active_module/enum.rb
|
|
76
|
-
- lib/active_module/enum/
|
|
77
|
-
- lib/active_module/enum/
|
|
78
|
-
- lib/active_module/enum/
|
|
96
|
+
- lib/active_module/enum/class_methods_definer.rb
|
|
97
|
+
- lib/active_module/enum/instance_methods_definer.rb
|
|
98
|
+
- lib/active_module/enum/util.rb
|
|
79
99
|
- lib/active_module/invalid_module_value.rb
|
|
80
100
|
- lib/active_module/module_refinement.rb
|
|
81
101
|
- lib/active_module/modules_index.rb
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActiveModule
|
|
4
|
-
class Enum
|
|
5
|
-
module Comparison
|
|
6
|
-
refine ::Module do
|
|
7
|
-
using ActiveModule::Enum::ModuleRefinement
|
|
8
|
-
|
|
9
|
-
def =~(other)
|
|
10
|
-
case other
|
|
11
|
-
when ::Symbol
|
|
12
|
-
possible_symbol_names_set.include?(other)
|
|
13
|
-
when ::String
|
|
14
|
-
possible_symbol_names_set.include?(other.to_sym)
|
|
15
|
-
else
|
|
16
|
-
self == other
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
using self
|
|
22
|
-
|
|
23
|
-
def self.compare(module1, module2)
|
|
24
|
-
module1 =~ module2
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActiveModule
|
|
4
|
-
class Enum
|
|
5
|
-
module ModuleRefinement
|
|
6
|
-
refine ::Module do
|
|
7
|
-
using ActiveModule::ModuleRefinement
|
|
8
|
-
|
|
9
|
-
def possible_names
|
|
10
|
-
overriden_possible_names
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def enum_symbol
|
|
14
|
-
name.demodulize.underscore.to_sym
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def possible_symbol_names_set
|
|
18
|
-
@possible_symbol_names_set ||=
|
|
19
|
-
Set.new(overriden_possible_names.map(&:to_sym))
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
private
|
|
23
|
-
|
|
24
|
-
def overriden_possible_names
|
|
25
|
-
[enum_symbol.to_s] + possible_names
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
using self
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "module_refinement"
|
|
4
|
-
|
|
5
|
-
# Indexes modules by symbols of their qualified and unqualified names.
|
|
6
|
-
module ActiveModule
|
|
7
|
-
class Enum
|
|
8
|
-
class ModulesIndex < ActiveModule::ModulesIndex
|
|
9
|
-
using ActiveModule::Enum::ModuleRefinement
|
|
10
|
-
|
|
11
|
-
protected
|
|
12
|
-
|
|
13
|
-
# so that this is using the enum refinement
|
|
14
|
-
def possible_names(module_instance)
|
|
15
|
-
module_instance.possible_names
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|