Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/pedrorolo/active_module
Modules and Classes as first-class active record values
https://github.com/pedrorolo/active_module
activemodel activerecord configuration enums gem module modules prototyping rapid-prototyping ruby ruby-gem ruby-on-rails strategy strategy-pattern
Last synced: about 1 month ago
JSON representation
Modules and Classes as first-class active record values
- Host: GitHub
- URL: https://github.com/pedrorolo/active_module
- Owner: pedrorolo
- License: mit
- Created: 2024-12-14T19:05:31.000Z (about 2 months ago)
- Default Branch: main
- Last Pushed: 2024-12-27T04:40:35.000Z (about 1 month ago)
- Last Synced: 2024-12-27T05:23:02.009Z (about 1 month ago)
- Topics: activemodel, activerecord, configuration, enums, gem, module, modules, prototyping, rapid-prototyping, ruby, ruby-gem, ruby-on-rails, strategy, strategy-pattern
- Language: Ruby
- Homepage: https://rubygems.org/gems/active_module
- Size: 63.5 KB
- Stars: 4
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# active_module
[![Gem Version](https://img.shields.io/gem/v/active_module)](https://rubygems.org/gems/active_module)
[![License: MIT](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT)
[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/pedrorolo/active_module/main.yml)](https://github.com/pedrorolo/active_module/blob/main/Rakefile)
[![100% Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/pedrorolo/active_module/blob/main/spec/spec_helper.rb)
[![Gem Total Downloads](https://img.shields.io/gem/dt/active_module?style=flat)](https://bestgems.org/gems/active_module)#### *Modules and Classes as first-class active record values!*
ActiveModel/ActiveRecord implementation of the Module attribute type.
- Allows storing a reference to a `Module` or `Class` in a `:string` database field
- Automatically casts strings and symbols into modules when creating and querying objects
- Symbols or strings refer to the modules using unqualified names
- It is safe and efficientThis is a very generic mechanism that enables many possible utilizations, for instance:
- **Composition-based polymorphism (Strategy design pattern)**
- **Rapid prototyping static domain objects**
- **Static configuration management**
- **Rich Java/C#-like enums**You can find examples of these in [Usage -> Examples](#Examples).
## TL;DR
Declare module attributes like this:
```ruby
class MyARObject < ActiveRecord::Base
attribute :module_field,
:active_module,
possible_modules: [MyModule1, MyClass, Nested::Module]
end
```Assign them like this:
```ruby
object.module_field = Nested::Module
object.module_field = :Module
object.module_field = "Module"
object.module_field #=> Nested::Module:Module
```Query them like this:
```ruby
MyARObject.where(module_field: Nested::Module)
MyARObject.where(module_field: :Module)
MyARObject.where(module_field: "Module")
object.module_field #=> Nested::Module:Module
```And compare them like this:
```ruby
object.module_field == Nested::Modulemodule MyNameSpace
using ActiveModule::Comparisonobject.module_field =~ :Module
object.module_field =~ "Module"
end
```## Installation
Add to your gemfile - and if you are using rails - that's all you need:
```ruby
gem 'active_module', "~> 0.5"
```If you are not using rails, just issue this command after loading active record
```ruby
ActiveModule.register!
```or this, if you prefer to have a better idea of what you are doing:
```ruby
ActiveModel::Type.register(:active_module, ActiveModule::Base)
ActiveRecord::Type.register(:active_module, ActiveModule::Base)
```## Usage
Add a string field to the table you want to hold a module attribute in your migrations
```ruby
create_table :my_ar_objects do |t|
t.string :module_field, index: true
end
```Now given this random module hierarchy:
```ruby
class MyARObject < ActiveRecord::Base
module MyModule1; end
module MyModule2; end
class MyClass;
module MyModule1; end
end
end
```
You can make the field refer to one of these modules/classes like this:
```ruby
class MyARObject < ActiveRecord::Base
attribute :module_field,
:active_module,
possible_modules: [MyModule1, MyModule2, MyClass, MyClass::MyModule1]
end
```Optionally, you can specify how to map your modules into the database
(the default is the module's fully qualified name):
```ruby
attribute :module_field,
:active_module,
possible_modules: [MyModule1, MyModule2, MyClass, MyClass::MyModule1]
mapping: {MyModule1 => "this is the db representation of module1"}
```And this is it! Easy!
### Assigning and querying module attributes
Now you can use this attribute in many handy ways!
For instance, you may refer to it using module literals:
```ruby
MyARObject.create!(module_field: MyARObject::MyModule1)MyARObject.where(module_field: MyARObject::MyModule1)
my_ar_object.module_field = MyARObject::MyModule1
my_ar_object.module_field #=> MyARObject::MyModule1:Module
```
But as typing fully qualified module names is not very ergonomic, you may also use symbols instead:```ruby
MyARObject.create!(module_field: :MyClass)MyARObject.where(module_field: :MyClass)
my_ar_object.module_field = :MyClass
my_ar_object.module_field #=> MyARObject::MyClass:Class
```
However, if there is the need for disambiguation, you can always use strings instead:
```ruby
MyARObject.create!(module_field: "MyClass::MyModule1")
MyARObject.where(module_field: "MyClass::MyModule1")
my_ar_object.module_field = "MyClass::MyModule1"
my_ar_object.module_field #=> MyARObject::MyClass::MyModule::Module
```### Comparing modules with strings and symbols
In order to compare modules with Strings or Symbols you'll have to use the `ActiveModule::Comparison`
refinement. This refinement adds the method `Module#=~` to the `Module` class, but this change is
only available within the namespace that includes the refinement.```ruby
module YourClassOrModuleThatWantsToCompare
using ActiveModule::Comparisondef method_that_compares
my_ar_object.module_field =~ :MyModule1
end
end
```or like this, if you don't want to use the refinement:
```ruby
ActiveModule::Comparison.compare(my_ar_object.module_field, :MyModule1)
```but in this last case it would probably make more sense to simply use a module literal:
```ruby
my_ar_object.module_field == MyClass::MyModule1
```## Examples
### Composition-based polymorphism (Strategy design pattern)
[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),
and multiple-polymorphism (by composing an object of multiple strategies).If you want to use classes this will do:
```ruby
class MyARObject < ActiveRecord::Base
attribute :strategy_class, :active_module, possible_modules: StrategySuperclass.subclassesdef strategy
@strategy ||= strategy_class.new(some_args_from_the_instance)
enddef run_strategy!(args)
strategy.call(args)
end
end
```But if you are not in the mood to define a class hierarchy for it (or if you are performance-savy),
you may use modules instead:```ruby
class MyARObject < ActiveRecord::Base
module Strategy1
def self.call
"strategy1 called"
end
endmodule Strategy2
def self.call
"strategy2 called"
end
endattribute :strategy,
:active_module,
possible_modules: [Strategy1, Strategy2]def run_strategy!(some_args)
strategy.call(some_args, other_args)
end
endMyARObject.create!(module_field: :Strategy1).run_strategy! #=> "strategy1 called"
MyARObject.create!(module_field: :Strategy2).run_strategy! #=> "strategy2 called"
```You can later easily promote these modules to classes if you need instance variables:
```ruby
class MyARObject < ActiveRecord::Base
class Strategy1
def self.call
self.new.call
enddef call
"strategy1 called"
end
endmodule Strategy2
def self.call
"strategy2 called"
end
endattribute :strategy,
:active_module,
possible_modules: [Strategy1, Strategy2]def run_strategy!(some_args)
strategy.call(some_args, other_args)
end
endMyARObject.create!(module_field: :Strategy1).run_strategy! #=> "strategy1 called"
MyARObject.create!(module_field: :Strategy2).run_strategy! #=> "strategy2 called"
```### Rapid prototyping static domain objects
```ruby
# Provider domain Object
module Provider
# As if the domain model class
def self.all
[Ebay, Amazon]
end# As if the domain model instances
module Ebay
def self.do_something!
"do something with the ebay provider config"
end
endmodule Amazon
def self.do_something!
"do something with the amazon provider config"
end
end
endclass MyARObject < ActiveRecord::Base
attribute :provider,
:active_module,
possible_modules: Provider.all
endMyARObject.create!(provider: :Ebay).provier.do_something!
#=> "do something with the ebay provider config"
MyARObject.create!(provider: Provider::Amazon).provider.do_something!
#=> "do something with the amazon provider config"
```What is interesting about this is that we can later easily promote
our provider objects into full fledged ActiveRecord objects without
big changes to our code:
```ruby
class Provider < ActiveRecord::Base
def do_something!
#...
end
endclass MyARObject < ActiveRecord::Base
belongs_to :provider
end
```Just in case you'd like to have shared code amongst the instances in the above example,
this is how you could do so:```ruby
# Provider domain Object
module Provider
# As if the domain model class
def self.all
[Ebay, Amazon]
endmodule Base
def do_something!
"do something with #{something_from_an_instance}"
end
end# As if the domain model instances
module Ebay
include Base
extend selfdef something_from_an_instance
"the ebay provider config"
end
endmodule Amazon
include Base
extend selfdef something_from_an_instance
"the amazon provider config"
end
end
end
```### Static configuration management
This example is not much different than previous one. It however stresses that the module we
refer to might be used as a source of configuration parameters that change the behaviour of
the class it belongs to:```ruby
# Provider domain Object
module ProviderConfig
module Ebay
module_functiondef url= 'www.ebay.com'
def number_of_attempts= 5
endmodule Amazon
module_functiondef url= 'www.amazon.com'
def number_of_attempts= 10
enddef self.all
[Ebay, Amazon]
end
endclass MyARObject < ActiveRecord::Base
attribute :provider_config,
:active_module,
possible_modules: ProviderConfig.alldef load_page!
n_attempts = 0
result = nil
while n_attempts < provider.number_of_attempts
result = get_page(provider.url)
if(result)
return result
else
n_attempts.inc
end
end
result
end
endMyARObject.create!(provider_config: :Ebay).load_page!
```### Rich Java/C#-like enums
This example is only to show the possibility.
This would probably benefit from using a meta programming abstraction
and there are already gems with this kind of functionality such as `enumerizable`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.
Java/C# enums allow defining methods on the enum, which are shared across all enum values:
```ruby
module PipelineStage
module_functiondef all
[InitialContact, InNegotiations, LostDeal, PaidOut]
enddef cast(stage)
self.all.map(&:external_provider_code).find{|code| code == stage} ||
self.all.map(&:database_representation).find{|code| code == stage} ||
self.all.map(&:frontend_representation).find{|code| code == stage}
endmodule Base
def external_provider_code
@external_provider_code ||= self.name.underscore
enddef database_representation
self.name
enddef frontend_representation
@frontend_representation ||= self.name.demodulize.upcase
end
endmodule InitialContact
extend Base
endmodule InNegotiations
extend Base
endmodule LostDeal
extend Base
endmodule PaidOut
extend Base
end
endclass MyARObject < ActiveRecord::Base
attribute :pipeline_stage,
:active_module,
possible_modules: PipelineStage.all
endobject = MyARObject.new(pipeline_stage: :InitialStage)
object.pipeline_stage&.frontend_representation #=> "INITIAL_STAGE"
object.pipeline_stage = :InNegotiations
object.pipeline_stage&.database_representation #=> "PipelineStage::InNegotiations"
```## Development
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.
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).
## Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/pedrorolo/active_module.