active_module 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f2bfc44bc97bbd3905636170057b010c4e2bd7cdb04bb749dcb0951c4ca7ae21
4
+ data.tar.gz: 22bec343c43a708452e724d648c7520dd923459d197c426acf796ca983163965
5
+ SHA512:
6
+ metadata.gz: 930ab5f0f7998773628a5d63fe58108b2cd9aaa430c0c168b49f08152dd965c06cb12439a83eade0a8ba929782bca3522c2e0a3f6506aa8883439e2e1c84962e
7
+ data.tar.gz: 22aa9eb2fd4d4c9a3d74671eee90f22c91aef2eb0f2b6591729fe15cad177c25d8a0a0ed17ac16b14f28f499d3f206bfe87e644115cf3f0384b3000adc7cd630
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,27 @@
1
+ require: rubocop-rspec
2
+
3
+ AllCops:
4
+ TargetRubyVersion: 3.0
5
+ SuggestExtensions: false
6
+ NewCops: disable
7
+
8
+ Style/StringLiterals:
9
+ EnforcedStyle: double_quotes
10
+
11
+ Style/StringLiteralsInInterpolation:
12
+ EnforcedStyle: double_quotes
13
+
14
+ Layout/LineLength:
15
+ Max: 80
16
+
17
+ Metrics/MethodLength:
18
+ Max: 20
19
+
20
+ Metrics/BlockLength:
21
+ Max: 100
22
+
23
+ Style/Documentation:
24
+ Enabled: false
25
+
26
+ RSpec/ExampleLength:
27
+ Max: 10
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Pedro Miguel dos Santos Morte Martins Rolo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,367 @@
1
+ # ActiveModule
2
+
3
+ *"Let's make `Modules` and `Classes` first-class values in active record!"*
4
+
5
+ ActiveModel/ActiveRecord implementation of the Module attribute type.
6
+
7
+ - Allows storing a reference to a `Module` or `Class` (because they are modules) in a `:string`
8
+ database field in a safe and efficient way.
9
+ - It automatically casts strings and symbols into modules when creating and querying objects.
10
+ - Symbols or strings refer to the modules using unqualified names.
11
+
12
+ This is a very generic mechanism that enables many possible utilizations, for instance:
13
+ - Strategy Pattern (composition-based polymorphism)
14
+ - Rapid prototyping static domain objects
15
+ - Static configuration management
16
+ - Rich Java/C#-like enums
17
+
18
+ You can find examples of these in [Usage -> Examples :](#Examples)
19
+
20
+ ## Installation
21
+
22
+ Add to your gemfile:
23
+
24
+ ```ruby
25
+ gem 'active_module', "~>0.1"
26
+ ```
27
+
28
+ Add to a rails initializer, such as `intializers/types.rb`
29
+
30
+ ```ruby
31
+ ActiveModule.register!
32
+ ```
33
+
34
+ or this, if you prefer to have a better idea of what you are doing:
35
+
36
+ ```ruby
37
+ ActiveModel::Type.register(:active_module, ActiveModule::Base)
38
+ ActiveRecord::Type.register(:active_module, ActiveModule::Base)
39
+ ```
40
+
41
+
42
+ ## Usage
43
+
44
+ Add a string field to the table you want to hold a module attribute in your migrations
45
+ ```ruby
46
+ create_table :my_ar_objects do |t|
47
+ t.string :module_field
48
+ end
49
+ ```
50
+
51
+ Now given this module structure:
52
+ ```ruby
53
+ class MyARObject < ActiveRecord::Base
54
+ module MyModule1; end
55
+ module MyModule2; end
56
+ class MyClass;
57
+ module Module1; end
58
+ end
59
+ end
60
+ ```
61
+ You can make the field refer to one of these modules/classes like this:
62
+ ```ruby
63
+ class MyARObject < ActiveRecord::Base
64
+ attribute :module_field,
65
+ :active_module,
66
+ possible_modules: [MyModule1, MyModule2, MyClass, MyClass::Module1]
67
+ end
68
+ ```
69
+ And this is it! Easy!<br>
70
+
71
+ ### Assigning and querying module attributes
72
+ Now you can use this attribute in many handy ways:
73
+ <br>
74
+ <br>
75
+ For instance, you may refer to it using module literals:
76
+ ```ruby
77
+ MyARObject.create!(module_field: MyARObject::MyModule1)
78
+
79
+ MyARObject.where(module_field: MyARObject::MyModule1)
80
+
81
+ my_ar_object.module_field = MyARObject::MyModule1
82
+
83
+ my_ar_object.module_field #=> MyARObject::MyModule1:Module
84
+
85
+ ```
86
+ But as typing fully qualified module names is not very ergonomic, you may also use symbols instead:
87
+
88
+ ```ruby
89
+ MyARObject.create!(module_field: :MyClass)
90
+
91
+ MyARObject.where(module_field: :MyClass)
92
+
93
+ my_ar_object.module_field = :MyClass
94
+
95
+ my_ar_object.module_field #=> MyARObject::MyClass:Class
96
+
97
+ ```
98
+
99
+ However, if there is the need for disambiguation, you can always use strings instead:
100
+
101
+ ```ruby
102
+
103
+ MyARObject.create!(module_field: "MyClass::MyModule1")
104
+
105
+ MyARObject.where(module_field: "MyClass::MyModule1")
106
+
107
+ my_ar_object.module_field = "MyClass::MyModule1"
108
+
109
+ my_ar_object.module_field #=> MyARObject::MyClass::MyModule::Module
110
+ ```
111
+
112
+ ### Examples
113
+
114
+ #### Strategy Pattern (composition-based polymorphism)
115
+
116
+ [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),
117
+ and multiple-polymorphism (by composing an object of multiple strategies).
118
+
119
+ ```ruby
120
+ class MyARObject < ActiveRecord::Base
121
+ module Strategy1
122
+ def self.call
123
+ "strategy1 called"
124
+ end
125
+ end
126
+
127
+ module Strategy2
128
+ def self.call
129
+ "strategy2 called"
130
+ end
131
+ end
132
+
133
+ attribute :strategy,
134
+ :active_module,
135
+ possible_modules: [Strategy1, Strategy2]
136
+
137
+ def run_strategy!
138
+ # here we could pass arguments to the strategy, and if
139
+ # in this case strategies were classes we could also
140
+ # instantiate them
141
+ strategy.call
142
+ end
143
+ end
144
+
145
+ MyARObject.create!(module_field: :Strategy1).run_strategy! #=> "strategy1 called"
146
+ MyARObject.create!(module_field: :Strategy1).run_strategy! #=> "strategy2 called"
147
+ ```
148
+
149
+
150
+ #### Rapid prototyping static domain objects
151
+
152
+ ```ruby
153
+ # Provider domain Object
154
+ module Provider
155
+ # As if the domain model class
156
+ def self.all
157
+ [Ebay, Amazon]
158
+ end
159
+
160
+ # As if the domain model instances
161
+ module Ebay
162
+ def self.do_something!
163
+ "do something with the ebay provider config"
164
+ end
165
+ end
166
+
167
+ module Amazon
168
+ def self.do_something!
169
+ "do something with the amazon provider config"
170
+ end
171
+ end
172
+ end
173
+
174
+ class MyARObject < ActiveRecord::Base
175
+ attribute :provider,
176
+ :active_module,
177
+ possible_modules: Provider.all
178
+ end
179
+
180
+ MyARObject.create!(provider: :Ebay).provier.do_something!
181
+ #=> "do something with the ebay provider config"
182
+ MyARObject.create!(provider: Provider::Amazon).provider.do_something!
183
+ #=> "do something with the amazon provider config"
184
+ ```
185
+
186
+ What is interesting about this is that we can later easily promote
187
+ our provider objects into full fledged ActiveRecord objects without
188
+ big changes to our code:
189
+ ```ruby
190
+ class Provider < ActiveRecord::Base
191
+ def do_something!
192
+ #...
193
+ end
194
+ end
195
+
196
+ class MyARObject < ActiveRecord::Base
197
+ belongs_to :provider
198
+ end
199
+ ```
200
+
201
+ Just in case you'd like to have shared code amongst the instances in the above example,
202
+ this is how you could do so:
203
+
204
+ ```ruby
205
+ # Provider domain Object
206
+ module Provider
207
+ # As if the domain model class
208
+ module Base
209
+ def do_something!
210
+ "do something with #{something_from_an_instance}"
211
+ end
212
+ end
213
+
214
+ # As if the domain model instances
215
+ module Ebay
216
+ include Base
217
+ extend self
218
+
219
+ def something_from_an_instance
220
+ "the ebay provider config"
221
+ end
222
+ end
223
+
224
+ module Amazon
225
+ include Base
226
+ extend self
227
+
228
+ def something_from_an_instance
229
+ "the amazon provider config"
230
+ end
231
+ end
232
+
233
+ # As if the domain model class
234
+ def self.all
235
+ [Ebay, Amazon]
236
+ end
237
+ end
238
+ ```
239
+
240
+
241
+ #### Static configuration management
242
+
243
+ This example is not much different than previous one. It however stresses that the module we
244
+ refer to might be used as a source of configuration parameters that change the behaviour of
245
+ the class it belongs to:
246
+
247
+ ```ruby
248
+ # Provider domain Object
249
+ module ProviderConfig
250
+ module Ebay
251
+ module_function
252
+
253
+ def url= 'www.ebay.com'
254
+ def number_of_attempts= 5
255
+ end
256
+
257
+ module Amazon
258
+ module_function
259
+
260
+ def url= 'www.amazon.com'
261
+ def number_of_attempts= 10
262
+ end
263
+
264
+ def self.all
265
+ [Ebay, Amazon]
266
+ end
267
+ end
268
+
269
+ class MyARObject < ActiveRecord::Base
270
+ attribute :provider_config,
271
+ :active_module,
272
+ possible_modules: ProviderConfig.all
273
+
274
+ def load_page!
275
+ n_attempts = 0
276
+ result = nil
277
+ while n_attempts < provider.number_of_attempts
278
+ result = get_page(provider.url)
279
+ if(result)
280
+ return result
281
+ else
282
+ n_attempts.inc
283
+ end
284
+ end
285
+ result
286
+ end
287
+ end
288
+
289
+ MyARObject.create!(provider_config: :Ebay).load_page!
290
+ ```
291
+
292
+ #### Rich Java/C#-like enums
293
+ This example is only to show the possibility.
294
+ This would probably benefit from using a meta programming abstraction
295
+ and there are already gems with this kind of functionality such as `enumerizable`
296
+
297
+ In a real world project, I guess it would rather make sense to extend `ActiveModule::Base` or even `ActiveModel::Type::Value`. But here it goes for the sake of example.
298
+
299
+ Java/C# enums allow defining methods on the enum, which are shared across all enum values:
300
+
301
+ ```ruby
302
+ module PipelineStage
303
+ module Base
304
+ def external_provider_code
305
+ @external_provider_code ||= self.name.underscore
306
+ end
307
+
308
+ def database_representation
309
+ self.name
310
+ end
311
+
312
+ def frontend_representation
313
+ @frontend_representation ||= self.name.demodulize.upcase
314
+ end
315
+ end
316
+
317
+ module InitialContact
318
+ extend Base
319
+ end
320
+
321
+ module InNegotiations
322
+ extend Base
323
+ end
324
+
325
+ module LostDeal
326
+ extend Base
327
+ end
328
+
329
+ module PaidOut
330
+ extend Base
331
+ end
332
+
333
+ module_function
334
+
335
+ def all
336
+ [InitialContact, InNegotiations, LostDeal, PaidOut]
337
+ end
338
+
339
+ def cast(stage)
340
+ self.all.map(&:external_provider_code).find{|code| code == stage} ||
341
+ self.all.map(&:database_representation).find{|code| code == stage} ||
342
+ self.all.map(&:frontend_representation).find{|code| code == stage}
343
+ end
344
+ end
345
+
346
+ class MyARObject < ActiveRecord::Base
347
+ attribute :pipeline_stage,
348
+ :active_module,
349
+ possible_modules: PipelineStage.all
350
+ end
351
+
352
+ object = MyARObject.new(pipeline_stage: :InitialStage)
353
+ object.pipeline_stage&.frontend_representation #=> "INITIAL_STAGE"
354
+ object.pipeline_stage = :InNegotiations
355
+ object.pipeline_stage&.database_representation #=> "PipelineStage::InNegotiations"
356
+ ```
357
+
358
+
359
+ ## Development
360
+
361
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
362
+
363
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
364
+
365
+ ## Contributing
366
+
367
+ Bug reports and pull requests are welcome on GitHub at https://github.com/pedrorolo/active_module.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+ require_relative "modules_index"
5
+
6
+ module ActiveModule
7
+ class Base < ActiveModel::Type::Value
8
+ def initialize(possible_modules:)
9
+ @possible_modules = possible_modules
10
+ super()
11
+ end
12
+
13
+ def type
14
+ :active_module
15
+ end
16
+
17
+ def serializable?(object)
18
+ possible_module?(object)
19
+ end
20
+
21
+ def cast(value)
22
+ case value
23
+ when nil
24
+ nil
25
+ when ::Symbol
26
+ sym_to_module(value)
27
+ when ::Module
28
+ if possible_module?(value)
29
+ value
30
+ else
31
+ raise_invalid_module_value_error(value)
32
+ end
33
+ when ::String
34
+ str_to_module(value)
35
+ else
36
+ raise_invalid_module_value_error(value)
37
+ end
38
+ end
39
+
40
+ def serialize(module_instance)
41
+ module_instance && cast(module_instance).name
42
+ end
43
+
44
+ def deserialize(str)
45
+ str&.constantize
46
+ rescue NameError
47
+ nil
48
+ end
49
+
50
+ private
51
+
52
+ def sym_to_module(sym)
53
+ modules_index[sym] ||
54
+ raise_invalid_module_value_error(sym)
55
+ end
56
+
57
+ def str_to_module(str)
58
+ modules_index[str.to_sym] ||
59
+ raise_invalid_module_value_error(str)
60
+ end
61
+
62
+ def raise_invalid_module_value_error(str)
63
+ raise InvalidModuleValue.new(
64
+ str,
65
+ possible_modules: @possible_modules,
66
+ possible_symbols: modules_index.keys
67
+ )
68
+ end
69
+
70
+ def possible_modules_set
71
+ @possible_modules_set ||= Set.new(@possible_modules).freeze
72
+ end
73
+
74
+ def possible_module?(module_instance)
75
+ possible_modules_set.include?(module_instance)
76
+ end
77
+
78
+ def modules_index
79
+ @modules_index ||= ModulesIndex.new(@possible_modules)
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModule
4
+ class InvalidModuleValue < StandardError
5
+ def initialize(value,
6
+ possible_modules:,
7
+ possible_symbols:)
8
+ super(<<~ERROR_MESSAGE)
9
+ Invalid active_module value #{value.inspect}:
10
+ It must be one of these modules:
11
+ #{possible_modules.inspect}
12
+
13
+ Or one of their referring symbols
14
+ #{possible_symbols.inspect}
15
+
16
+ Or corresponding strings:
17
+ #{possible_symbols.map(&:to_s).inspect}
18
+ ERROR_MESSAGE
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModule
4
+ module ModuleRefinement
5
+ refine ::Module do
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
15
+ end
16
+
17
+ def qualified_name
18
+ "::#{name}"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,28 @@
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 ModulesIndex
8
+ using ModuleRefinement
9
+
10
+ delegate :[], to: :index
11
+ delegate :keys, to: :index
12
+
13
+ def initialize(modules)
14
+ @modules = modules
15
+ end
16
+
17
+ private
18
+
19
+ def index
20
+ @index ||=
21
+ @modules.flat_map do |module_instance|
22
+ module_instance.possible_names.map do |name|
23
+ [name.to_sym, module_instance]
24
+ end
25
+ end.to_h.freeze
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModule
4
+ module Register
5
+ module_function
6
+
7
+ def call(type_symbol = :active_module)
8
+ ActiveModel::Type.register(type_symbol, ActiveModule::Base)
9
+ ActiveRecord::Type.register(type_symbol, ActiveModule::Base)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModule
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "active_module/version"
4
+ require_relative "active_module/base"
5
+ require_relative "active_module/invalid_module_value"
6
+ require_relative "active_module/register"
7
+
8
+ module ActiveModule
9
+ module_function
10
+
11
+ def register!(type_symbol = :active_module)
12
+ Register.call(type_symbol)
13
+ end
14
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_module
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Pedro Rolo
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-12-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "<"
18
+ - !ruby/object:Gem::Version
19
+ version: '9'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: '7'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "<"
28
+ - !ruby/object:Gem::Version
29
+ version: '9'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '7'
33
+ - !ruby/object:Gem::Dependency
34
+ name: bundler
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 1.15.0
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 1.15.0
47
+ description: Module type for ActiveModel and Active Record
48
+ email:
49
+ - pedrorolo@gmail.com
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - ".rspec"
55
+ - ".rubocop.yml"
56
+ - LICENSE
57
+ - README.md
58
+ - Rakefile
59
+ - lib/active_module.rb
60
+ - lib/active_module/base.rb
61
+ - lib/active_module/invalid_module_value.rb
62
+ - lib/active_module/module_refinement.rb
63
+ - lib/active_module/modules_index.rb
64
+ - lib/active_module/register.rb
65
+ - lib/active_module/version.rb
66
+ homepage: https://github.com/pedrorolo/active_module
67
+ licenses: []
68
+ metadata:
69
+ homepage_uri: https://github.com/pedrorolo/active_module
70
+ source_code_uri: https://github.com/pedrorolo/active_module
71
+ post_install_message:
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: 3.0.0
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ requirements: []
86
+ rubygems_version: 3.5.7
87
+ signing_key:
88
+ specification_version: 4
89
+ summary: Module type for ActiveModel and Active Record
90
+ test_files: []