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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aceb33a1b0059198c851a897ab6dd3636a9796f25db0912a43ffeb4bcad9af30
4
- data.tar.gz: 675db3e326eee1cc570f62d4d0199cabcd0841b42dc362e5eb247299d27f1183
3
+ metadata.gz: 380e377e525e9ed59c3154706e0183a744ff91660db261aa3a4ee012ad8feaf0
4
+ data.tar.gz: 615e920f65193a34f20adb0480311f86f16dfcd8d9ae9c68bfafca1b38af8919
5
5
  SHA512:
6
- metadata.gz: c1cf90718ec48d5c3cd155b5bb7f3330b30a251e2f1a0b16394a3051893dbc701c3b60085647a737d4fe660e977b70386f0556b7d5db680be7eef1afb381b869
7
- data.tar.gz: 1d3257b60414f30b9ebb699c9067155ea4669e68e30ba9998e98a92719ecb86f891888b05876cbfae810fdd22a0b7906571826f2ad932ca86113dcee6543fd79
6
+ metadata.gz: 3264235d103059314e29f031c7ec6d1b7d3edc37c65e415bb61a14d0ae71d58edd794c1eeaf7ad7dc5e211d9e5b6a891861fef6e934e1bce1552558db4a26803
7
+ data.tar.gz: 20f7c237d5d2c2b7e4088e6c050fb4b1115460e0dd051ec2a08fcd019d6fa624e6a4bfadb559912c1384fe69f38f9d1ae39f5cd21e0ecc0174f9ad33a1a68e65
data/README.md CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  # active_module
3
2
  [![Gem Version](https://img.shields.io/gem/v/active_module)](https://rubygems.org/gems/active_module)
4
3
  [![License: MIT](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT)
@@ -6,9 +5,7 @@
6
5
  [![100% Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/pedrorolo/active_module/blob/main/spec/spec_helper.rb)
7
6
  [![Gem Total Downloads](https://img.shields.io/gem/dt/active_module?style=flat)](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](#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
- ```ruby
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 #=> Nested::Module:Module
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
- ```ruby
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
- object.module_field #=> Nested::Module:Module
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 'active_module', "~> 0.6"
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
- class MyClass;
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 => "this is the db representation of module1"}
133
+ attribute :module_field,
134
+ :active_module,
135
+ possible_modules: [MyModule1, MyModule2, MyClass, MyClass::MyModule1],
136
+ mapping: { MyModule1 => "m1" }
125
137
  ```
126
138
 
127
- And this is it! Easy!<br>
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
- MyARObject.where(module_field: MyARObject::MyModule1)
158
+ The most ergonomic way is to use underscored symbols. For flat modules,
159
+ use the underscored name directly:
138
160
 
139
- my_ar_object.module_field = MyARObject::MyModule1
161
+ ```ruby
162
+ MyARObject.create!(module_field: :my_module1)
140
163
 
141
- my_ar_object.module_field #=> MyARObject::MyModule1:Module
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
- But as typing fully qualified module names is not very ergonomic, you may also use symbols instead:
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: :MyClass)
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
- MyARObject.where(module_field: :MyClass)
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
- my_ar_object.module_field = :MyClass
187
+ ```ruby
188
+ # Given possible_modules: [MyModule1, MyClass::MyModule1]
189
+ # :MyModule1 resolves to the flat MyModule1 (not MyClass::MyModule1)
152
190
 
153
- my_ar_object.module_field #=> MyARObject::MyClass:Class
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
- However, if there is the need for disambiguation, you can always use strings instead:
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::MyModule::Module
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 =~ :MyModule1
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, :MyModule1)
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!(module_field: :Strategy1).run_strategy! #=> "strategy1 called"
248
- MyARObject.create!(module_field: :Strategy2).run_strategy! #=> "strategy2 called"
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!(module_field: :Strategy1).run_strategy! #=> "strategy1 called"
281
- MyARObject.create!(module_field: :Strategy2).run_strategy! #=> "strategy2 called"
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
- # As if the domain model class
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).provier.do_something!
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= 'www.ebay.com'
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= 'www.amazon.com'
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 < provider.number_of_attempts
412
- result = get_page(provider.url)
413
- if(result)
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.inc
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
- This example is only to show the possibility.
428
- This would probably benefit from using a meta programming abstraction and we will provide something
429
- like that in the future.
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
- Java/C# enums allow defining methods on the enum, which are shared across all enum values:
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
- extend Base
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
- module InNegotiations
466
- extend Base
467
- end
563
+ class MyARObject < ActiveRecord::Base
564
+ attribute :pipeline_stage,
565
+ :active_module,
566
+ possible_modules: PipelineStage.all
468
567
 
469
- module LostDeal
470
- extend Base
471
- end
568
+ active_module_enum :pipeline_stage
569
+ end
570
+ ```
571
+
572
+ This generates:
472
573
 
473
- module PaidOut
474
- extend Base
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 :pipeline_stage,
480
- :active_module,
481
- possible_modules: PipelineStage.all
713
+ attribute :status, :active_module,
714
+ possible_modules: [Status, Nested::Status]
715
+ active_module_enum :status
482
716
  end
483
717
 
484
- object = MyARObject.new(pipeline_stage: :InitialStage)
485
- object.pipeline_stage&.frontend_representation #=> "INITIAL_STAGE"
486
- object.pipeline_stage = :InNegotiations
487
- object.pipeline_stage&.database_representation #=> "PipelineStage::InNegotiations"
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).
@@ -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.to_sym] ||
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
@@ -1,6 +1,91 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveModule
4
- class Enum
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
- name_parts = name.split("::")
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 ||= Set.new(possible_names.map(&:to_sym))
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.flat_map do |module_instance|
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
@@ -12,6 +12,7 @@ module ActiveModule
12
12
 
13
13
  require "active_record"
14
14
  ActiveRecord::Type.register(type_symbol, ActiveModule::Base)
15
+ ActiveRecord::Base.extend(ActiveModule::Enum)
15
16
  end
16
17
  end
17
18
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveModule
4
- VERSION = "0.6.8"
4
+ VERSION = "0.8.0"
5
5
  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.6.8
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.1.3
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.1.3
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/comparison.rb
77
- - lib/active_module/enum/module_refinement.rb
78
- - lib/active_module/enum/modules_index.rb
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