Expanding on my question here (ruby/rails: extending or including other modules), using my existing solution, what's the best way to determine if my module is included?
What I did for now was I defined instance methods on each module so when they get included a method would be available, and then I just added a catcher (method_missing()) to the parent module so I can catch if they are not included. My solution code looks like:
module Features
FEATURES = [Running, Walking]
# include Features::Running
FEATURES.each do |feature|
include feature
end
module ClassMethods
# include Features::Running::ClassMethods
FEATURES.each do |feature|
include feature::ClassMethods
end
end
module InstanceMethods
def method_missing(meth)
# Catch feature checks that are not included in models to return false
if meth[-1] == '?' && meth.to_s =~ /can_(\w+)\z?/
false
else
# You *must* call super if you don't handle the method,
# otherwise you'll mess up Ruby's method lookup
super
end
end
end
def self.included(base)
base.send :extend, ClassMethods
base.send :include, InstanceMethods
end
end
# lib/features/running.rb
module Features::Running
module ClassMethods
def can_run
...
# Define a method to have model know a way they have that feature
define_method(:can_run?) { true }
end
end
end
# lib/features/walking.rb
module Features::Walking
module ClassMethods
def can_walk
...
# Define a method to have model know a way they have that feature
define_method(:can_walk?) { true }
end
end
end
So in my models I have:
# Sample models
class Man < ActiveRecord::Base
# Include features modules
include Features
# Define what man can do
can_walk
can_run
end
class Car < ActiveRecord::Base
# Include features modules
include Features
# Define what man can do
can_run
end
And then I can
Man.new.can_walk?
# => true
Car.new.can_run?
# => true
Car.new.can_walk? # method_missing catches this
# => false
Did I write this correctly? Or is there a better way?
If I understand your question correctly, you can use Module#include?:
Man.include?(Features)
For example:
module M
end
class C
include M
end
C.include?(M) # => true
Other ways
Checking Module#included_modules
This works, but it's a bit more indirect, since it generates intermediate included_modules array.
C.included_modules.include?(M) # => true
since C.included_modules has a value of [M, Kernel]
Checking Module#ancestors
C.ancestors.include?(M) #=> true
since C.ancestors has a value of [C, M, Object, Kernel, BasicObject]
Using operators like <
The Module class also declares several comparison operators:
Module#<
Module#<=
Module#==
Module#>=
Module#>
Example:
C < M # => true
Related
There's an ActiveRecord model, which has it's own (basically, included from the other ActiveRecord's module) #changed? and #change methods. And there's a module Observable which also has it's own changed? and change definitions.
I need to define a custom module, which automatically includes Observable module and performs some underlying logic, but the problem is, that when I try to alias and undef original Observable method, it also undefs methods from other modules, which is critical.
Is there any elegant way to solve this? As I don't really want to implemet a custom Observable module.
Here's an example code:
require 'observer'
# Trying to undef Observable's #changed and #changed?
# But really, when included, it also undefs methods from
# other modules included by original class
module TryingToRewriteChanged
include ::Observable
alias triggerable_changed? changed?
alias triggerable_changed changed
undef_method :changed?
undef_method :changed
end
# Custom module which has some logic in .included
module Triggerable
def self.included(obj)
obj.class_eval do
include TryingToRewriteChanged
# ... And other magic
end
end
end
# Mock for some ActiveRecord module with
# #changed and #changed? definitions
module ActiveRecord
module SomeActiveRecordModule
def changed
puts 'original changed'
end
def changed?
puts 'original changed?'
end
end
end
# Mock for ActiveRecord::Base class
module ActiveRecord
class Base
include SomeActiveRecordModule
end
end
# Example model, which need to include Triggerable module
class SomeModel < ActiveRecord::Base
include Triggerable
end
# ActiveRecord's #changed is no more available
SomeModel.new.changed
# -> undefined method `changed'
https://repl.it/repls/KeyQuickwittedAsiantrumpetfish
Thank you.
It you try to print ancestors of model, it will show
SomeModel.ancestors # [SomeModel, TryingToRewriteChanged, Observable, Triggerable, ActiveRecord::Base, ActiveRecord::SomeActiveRecordModule, Object, JSON::Ext::Generator::GeneratorMethods::Object, Kernel, BasicObject]
Hence, when calling SomeModel.new.changed, it will call changed of Observable. And this method already undef_method, it will throw exception as the document: https://apidock.com/ruby/Module/undef_method
Prevents the current class from responding to calls to the named
method. Contrast this with remove_method, which deletes the method
from the particular class; Ruby will still search superclasses and
mixed-in modules for a possible receiver.
There is 2 way you can use to resolve this issue:
1 - Prepend ActiveRecord::SomeActiveRecordModule before TryingToRewriteChanged in inheritance chain.
# Mock for ActiveRecord::Base class
module ActiveRecord
class Base
include Triggerable
include SomeActiveRecordModule
end
end
# Example model, which need to include Triggerable module
class SomeModel < ActiveRecord::Base
end
ref: https://repl.it/repls/ProudGuiltyMorpho
But using this way, you have to accept that Triggerable will be included in all ActiveRecord subclasses which may larger scope than your expectation.
2 - Implement changed and changed? methods in SomeModel to call corresponding methods in SomeActiveRecordModule explicitly. Using some techniques of metaprogramming may help to shorten the code.
class SomeModel < ActiveRecord::Base
include Triggerable
def changed
ActiveRecord::SomeActiveRecordModule.instance_method('changed').bind(self).call
end
def changed?
ActiveRecord::SomeActiveRecordModule.instance_method('changed?').bind(self).call
end
end
ref: https://repl.it/repls/ContentTameIsabellinewheatear
Found pretty ugly but working solution, the main magic happens under the TryingToRewriteChanged module:
require 'observer'
# Create aliases for #changed and changed? methods from
# Observable - basically, renaming them.
# And then call top-level methods of the first includer when
# receiving #changed / changed?
module TryingToRewriteChanged
include ::Observable
alias triggable_changed changed
alias triggable_changed? changed?
[:changed, :changed].each do |method|
define_method(method) do |*args|
return super(*args) unless origin_method_present?(method)
call_origin_method(method, *args)
end
end
private
def call_origin_method(name, *args)
method(name).super_method.super_method.call(*args)
end
def origin_method_present?(name)
method(name).super_method&.super_method&.name == name
end
end
# Custom module which has some logic in .included
module Triggerable
def self.included(obj)
obj.class_eval do
include TryingToRewriteChanged
end
end
end
# Mock for some ActiveRecord module with
# #changed and changed? definitions
module ActiveRecord
module SomeActiveRecordModule
def changed
puts 'original changed'
end
def changed?
puts 'original changed?'
end
end
end
# Mock for ActiveRecord::Base class
module ActiveRecord
class Base
include SomeActiveRecordModule
end
end
# Example model, which need to include Triggerable module
class SomeModel < ActiveRecord::Base
include Triggerable
end
# ActiveRecord's #changed is no more available
SomeModel.new.changed
https://repl.it/repls/ThirstyFlawedXanthareel
I have the following code to represent different Value Objects in Ruby. The only thing that changes between different classes is the INITIALIZATION_ATTRIBUTES array, which represents the list of attributes of the value object. I can't find a way to DRY this code. I tried to use a Module and accessing the included classes' Constants, but I run into the weird Constant lookup behavior described here. Essentially, the Module code is evaluated multiple times and it interprets the constant of the lastly evaluated class and applies its values to all the Value Object classes.
Is there any better alternative? I also tried with a base class, but I couldn't make it work.
module Values
class MaintenanceRegimeSerializer
INITIALIZATION_ATTRIBUTES = [:distance_between_services, :months_between_services]
def self.load(json)
json ||= '{}'
hash = JSON.parse json, symbolize_names: true
self.new(*INITIALIZATION_ATTRIBUTES.map {|key| hash[key]})
end
def self.dump(obj)
unless obj.is_a?(self)
raise ::ActiveRecord::SerializationTypeMismatch,
"Attribute was supposed to be a #{self}, but was a #{obj.class}. -- #{obj.inspect}"
end
obj.to_json
end
attr_reader *INITIALIZATION_ATTRIBUTES
define_method :initialize do |*args|
raise ArgumentError unless INITIALIZATION_ATTRIBUTES.length == args.length
INITIALIZATION_ATTRIBUTES.each_with_index do |attribute, index|
instance_variable_set "##{attribute}", args[index]
end
end
end
end
This can be done by layering two modules. The outer module will provide the functionality to initialize the inner module. Because class attributes are used, which are unique for every including class, one including class' attributes can not conflict with another including class' attributes.
module Values
module MaintenanceRegimeSerializer
extend ActiveSupport::Concern
class_methods do
def acts_as_maintenance_regime_serializer(attributes)
# include the inner module
# thereby adding the required methods and class attributes
include JsonMethods
# set the class variables made available by including the inner module
self.serializer_attributes = attributes
end
end
module JsonMethods
extend ActiveSupport::Concern
included do
class_attribute :serializer_attributes
def initialize(*args)
raise ArgumentError unless self.class.serializer_attributes.length == args.length
self.class.serializer_attributes.each_with_index do |attribute, index|
instance_variable_set "##{attribute}", args[index]
end
end
end
class_methods do
def load(json)
json ||= '{}'
hash = JSON.parse json, symbolize_names: true
new(*serializer_attributes.map {|key| hash[key]})
end
def dump(obj)
unless obj.is_a?(self)
raise ::ActiveRecord::SerializationTypeMismatch,
"Attribute was supposed to be a #{self}, but was a #{obj.class}. -- #{obj.inspect}"
end
obj.to_json
end
end
end
end
end
# in the including class
class SomeClass
# This might also be put into an initializer patching ActiveRecord::Base
# to avoid having to call this in every class desiring the regime serializer functionalit
include Values::MaintenanceRegimeSerializer
acts_as_maintenance_regime_serializer([:distance_between_services,
:months_between_services])
end
# in another including class
class SomeOtherClass
include Values::MaintenanceRegimeSerializer
acts_as_maintenance_regime_serializer([:foo,
:bar])
end
I have an ActiveRecord class called User. I'm trying to create a concern called Restrictable which takes in some arguments like this:
class User < ActiveRecord::Base
include Restrictable # Would be nice to not need this line
restrictable except: [:id, :name, :email]
end
I want to then provide an instance method called restricted_data which can perform some operation on those arguments and return some data. Example:
user = User.find(1)
user.restricted_data # Returns all columns except :id, :name, :email
How would I go about doing that?
If I understand your question correctly this is about how to write such a concern, and not about the actual return value of restricted_data. I would implement the concern skeleton as such:
require "active_support/concern"
module Restrictable
extend ActiveSupport::Concern
module ClassMethods
attr_reader :restricted
private
def restrictable(except: []) # Alternatively `options = {}`
#restricted = except # Alternatively `options[:except] || []`
end
end
def restricted_data
"This is forbidden: #{self.class.restricted}"
end
end
Then you can:
class C
include Restrictable
restrictable except: [:this, :that, :the_other]
end
c = C.new
c.restricted_data #=> "This is forbidden: [:this, :that, :the_other]"
That would comply with the interface you designed, but the except key is a bit strange because it's actually restricting those values instead of allowing them.
I'd suggest starting with this blog post: https://signalvnoise.com/posts/3372-put-chubby-models-on-a-diet-with-concerns Checkout the second example.
Think of concerns as a module you are mixing in. Not too complicated.
module Restrictable
extend ActiveSupport::Concern
module ClassMethods
def restricted_data(user)
# Do your stuff
end
end
end
I have concern in which I store constants:
module Group::Constants
extend ActiveSupport::Concern
MEMBERSHIP_STATUSES = %w(accepted invited requested
rejected_by_group rejected_group)
end
And another concern that I wish to use these constants:
module User::Groupable
extend ActiveSupport::Concern
include Group::Constants
MEMBERSHIP_STATUSES.each do |status_name|
define_method "#{status_name}_groups" do
groups.where(:user_memberships => {:status => status_name})
end
end
end
Unfortunately, this results in a routing error:
uninitialized constant User::Groupable::MEMBERSHIP_STATUSES
It looks like the first concern isn't loading properly in the second concern. If that's the case, what can I do about it?
It appears this behavior is by design, as explained nicely over here.
What you'll need to do in this case is not have Group::Constants extend from ActiveSupport::Concern since that will block its implementation from being shared with other ActiveSupport::Concern extending modules (although it will be ultimately shared in a class that includes the second module):
module A
TEST_A = 'foo'
end
module B
extend ActiveSupport::Concern
TEST_B = 'bar'
end
module C
extend ActiveSupport::Concern
include A
include B
end
C::TEST_A
=> 'foo'
C::TEST_B
=> uninitialized constant C::TEST_B
class D
include C
end
D::TEST_A
=> 'foo'
D::TEST_B
=> 'bar'
In short, you'll need to make Group::Constants a standard module and then all will be well.
If you want to keep everything in one file, and if you can stomach a bit of boilerplate, you could break up your module into a "concern" bit and a "non-concern" bit:
module A
FOO = [22]
def self.included base
base.include Concern
end
module Concern
extend ActiveSupport::Concern
class_methods do
def foo_from_the_perspective_of_a_class_method_in_A
{lexical: FOO, instance: self::FOO}
end
end
end
end
module B
extend ActiveSupport::Concern
include A
FOO += [33]
def foo_from_the_perspective_of_an_instance_method_in_B
FOO
end
end
class C
include B
end
C.foo_from_the_perspective_of_a_class_method_in_A
=> {:lexical=>[22], :instance=>[22, 33]}
C.new.foo_from_the_perspective_of_an_instance_method_in_B
=> [22, 33]
C::FOO
=> [22, 33]
In an effort to reduce code duplication in my little Rails app, I've been working on getting common code between my models into it's own separate module, so far so good.
The model stuff is fairly easy, I just have to include the module at the beginning, e.g.:
class Iso < Sale
include Shared::TracksSerialNumberExtension
include Shared::OrderLines
extend Shared::Filtered
include Sendable::Model
validates_presence_of :customer
validates_associated :lines
owned_by :customer
def initialize( params = nil )
super
self.created_at ||= Time.now.to_date
end
def after_initialize
end
order_lines :despatched
# tracks_serial_numbers :items
sendable :customer
def created_at=( date )
write_attribute( :created_at, Chronic.parse( date ) )
end
end
This is working fine, now however, I'm going to have some controller and view code that's going to be common between these models as well, so far I have this for my sendable stuff:
# This is a module that is used for pages/forms that are can be "sent"
# either via fax, email, or printed.
module Sendable
module Model
def self.included( klass )
klass.extend ClassMethods
end
module ClassMethods
def sendable( class_to_send_to )
attr_accessor :fax_number,
:email_address,
:to_be_faxed,
:to_be_emailed,
:to_be_printed
#_class_sending_to ||= class_to_send_to
include InstanceMethods
end
def class_sending_to
#_class_sending_to
end
end # ClassMethods
module InstanceMethods
def after_initialize( )
super
self.to_be_faxed = false
self.to_be_emailed = false
self.to_be_printed = false
target_class = self.send( self.class.class_sending_to )
if !target_class.nil?
self.fax_number = target_class.send( :fax_number )
self.email_address = target_class.send( :email_address )
end
end
end
end # Module Model
end # Module Sendable
Basically I'm planning on just doing an include Sendable::Controller, and Sendable::View (or the equivalent) for the controller and the view, but, is there a cleaner way to do this? I 'm after a neat way to have a bunch of common code between my model, controller, and view.
Edit: Just to clarify, this just has to be shared across 2 or 3 models.
You could pluginize it (use script/generate plugin).
Then in your init.rb just do something like:
ActiveRecord::Base.send(:include, PluginName::Sendable)
ActionController::Base.send(:include, PluginName::SendableController)
And along with your self.included that should work just fine.
Check out some of the acts_* plugins, it's a pretty common pattern (http://github.com/technoweenie/acts_as_paranoid/tree/master/init.rb, check line 30)
If that code needs to get added to all models and all controllers, you could always do the following:
# maybe put this in environment.rb or in your module declaration
class ActiveRecord::Base
include Iso
end
# application.rb
class ApplicationController
include Iso
end
If you needed functions from this module available to the views, you could expose them individually with helper_method declarations in application.rb.
If you do go the plugin route, do check out Rails-Engines, which are intended to extend plugin semantics to Controllers and Views in a clear way.