Cannot use instance variables in prepended module - ruby-on-rails

I want to be able to include my module in ActiveRecord::Base so that the has_folder_attachments method is available to my Rails AR classes.
I'm doing this to extend the original module's function to support AR hooks; however the variables #physical_path and #dice are both nil and I don't understand why.
module FolderAttachments
module ClassMethods
def has_folder_attachments(physical_path, excludes: [])
#physical_path = physical_path
super
end
end
def self.prepended(base)
class << base
prepend ClassMethods
end
end
attr_reader :physical_path
end
module ActiveRecord
class Base
prepend FolderAttachments
attr_reader :dice
# This should run after the module method
def self.has_folder_attachments(*args)
#dice = true
end
end
end
class Damned < ActiveRecord::Base
has_folder_attachments :for_real
end
damn = Damned.new
puts damn.physical_path # => nil
puts damn.dice # => nil

You are mixing instance and (meta)class context when using the two variables. Both variables are set their values in methods that are run in the class context (more precisely in the context of the metaclass). Thus, you cannot access these variables (and their attr_readers) in an instance context.
For the attr_readers to work, you have to move them to the class context and access them from there:
module FolderAttachments
module ClassMethods
...
attr_reader :physical_path
end
end
module ActiveRecord
class Base
...
class << self
attr_reader :dice
end
end
end
damn = Damned.new
damn.class.physical_path # => :for_real
damn.class.dice # => true
Or you may also add instance-level readers that delegate to the class-level readers so that you can access them also in instance context:
module FolderAttachments
module ClassMethods
...
attr_reader :physical_path
end
def physical_path
self.class.physical_path
end
end
module ActiveRecord
class Base
...
class << self
attr_reader :dice
end
def dice
self.class.dice
end
end
end
damn = Damned.new
damn.physical_path # => :for_real
damn.dice # => true

Related

Share data between methods

I have this module, which gets included in a class:
module MyModule
def self.included base
base.extend ClassMethods
end
module ClassMethods
def my_module_method data
include MyModule::InstanceMethods
after_save :my_module_process
attr_accessor :shared_data
shared_data = data
# instance_variable_set :#shared_data, data
end
end
module InstanceMethods
private
def my_module_process
raise self.shared_data.inspect
# raise instance_variable_get(:#shared_data).inspect
end
end
end
I want to use the data (parameter) passed to my_module_method within my_module_process. I've used attr_accessor as well as instance variables, but either of them return nil.
Since you're using rails, your module can be greatly simplified by making it a AS::Concern
module MyModule
extend ActiveSupport::Concern
included do
# after_save :my_module_process # or whatever
cattr_accessor :shared_data
end
module ClassMethods
def my_module_method(data)
self.shared_data = data
end
end
def my_module_process
"I got this shared data: #{self.class.shared_data}"
end
end
The key points here are:
cattr_accessor, which is similar to attr_accessor, but defines class-level methods
self.class.shared_data to access that class-level data from the instances.
Usage:
class Foo
include MyModule
end
f = Foo.new
f.my_module_process # => "I got this shared data: "
Foo.my_module_method({foo: 'bar'})
f.my_module_process # => "I got this shared data: {:foo=>\"bar\"}"
I've used attr_accessor as well as instance variables, but either of them return nil.
In ruby, it is super-important to know what is self at any given moment. This is what defines the methods and instance variables available to you. As an exercise, I offer you to find out, why user.name returns nil here (and how to fix it).
class User
#name = 'Joe'
def name
#name
end
end
user = User.new
user.name # => nil

Set class variable from module

I want to have separate logs for my app. I created the following module:
module MyApp
module MyLog
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def logger
##logger ||= Logger.new("#{Rails.root}/log/#{self.name.underscore}.log")
end
end
end
end
Then, in any of my models, I can add:
include MyApp::MyLog
and use it as (log file will appear in .../log/cat.log):
Cat.logger.info 'test'
I tried to use this method included on Cat and Dog models, and I have this result:
Cat.new.logger
# => #<Logger:0x007fe4516cf0b0 #progname=nil, ... #dev=#<File:/.../log/cat.log>, ...
Dog.new.logger
# => #<Logger:0x007fe4516cf0b0 #progname=nil, ... #dev=#<File:/.../log/cat.log>, ... (the same)
If I try to use my logger for Dog model first, I will have a log file with the name dog (/dog.log).
How can I set class variable ##logger from a module for each class with the correct initialized logger?
Do not use class variable, use instance_variable that is attached to the class.
module MyApp
module MyLog
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def logger
#logger ||= Logger.new("#{Rails.root}/log/#{self.name.underscore}.log")
end
end
end
end
Example:
module A
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def logger
puts #logger
#logger ||= name
end
end
end
class B
include A
end
class C
include A
end
B.logger
#
B.logger
# B
C.logger
#
B.logger
# B
C.logger
# C
First time you call the method it is nil, thus the empty line, second time you call method the value equals to class name, B, and if called on new class it is again nil, check also this answer
Ruby class instance variable vs. class variable

How to access a class method (or variable) in an instance method using concern?

I need to access a class method (defined in ClassMethods) in an instance method inside a concern.
My brain is melted and I'm sure that is a simple thing that I'm doing wrong.
I need to access comparable_opts inside comparison. How can I do it?
Follow snippets below:
Concern
# app/models/concerns/compare.rb
module Compare
extend ActiveSupport::Concern
attr_accessor :comparable_opts
module ClassMethods
attr_reader :arguable_opts
def comparable_opts
##comparable_opts
end
private
def default_opts
#default_opts ||= {fields: [:answers_count,
:answers_correct_count,
:answers_correct_rate,
:users_count]}
end
def compare(opts={})
#comparable_opts = default_opts.merge(opts)
end
end
def comparison
end
end
Model
# app/models/mock_alternative.rb
class MockAlternative < ActiveRecord::Base
include Compare
belongs_to :mock, primary_key: :mock_id, foreign_key: :mock_id
compare fields: [:answers_count, :question_answers_count, :question_answers_rate],
with: :mock_aternative_school
def question_answers_rate
self[:answers_count].to_f/self[:question_answers_count].to_f
end
end
Solution:
I've just used cattr_accessor in my method compare. Thank everyone.
module Compare
extend ActiveSupport::Concern
module ClassMethods
attr_reader :arguable_opts
def comparison_klass
"ActiveRecord::#{comparable_opts[:with].to_s.classify}".constantize
end
private
def default_opts
#default_opts ||= {fields: [:answers_count,
:answers_correct_count,
:answers_correct_rate,
:users_count]}
end
def compare(opts={})
cattr_accessor :comparable_opts
self.comparable_opts = default_opts.merge(opts)
end
end
def comparison
comparable_opts
end
end

Class_eval not working inside each block

I have defined a module to extend ActiveRecord.
In my case I have to generate instance methods with the symbols given as arguments to the compound_datetime class method. It works when class_eval is called outside the each block but not inside it; in the latter case I get an undefined method error.
Does anyone know what I am doing wrong?
module DateTimeComposer
mattr_accessor :attrs
##attrs = []
module ActiveRecordExtensions
module ClassMethods
def compound_datetime(*attrs)
DateTimeComposer::attrs = attrs
include ActiveRecordExtensions::InstanceMethods
end
end
module InstanceMethods
def datetime_compounds
DateTimeComposer::attrs
end
def self.define_compounds(attrs)
attrs.each do |attr|
class_eval <<-METHODS
def #{attr.to_s}_to()
puts 'tes'
end
METHODS
end
end
define_compounds(DateTimeComposer::attrs)
end
end
end
class Account < ActiveRecord::Base
compound_datetime :sales_at, :published_at
end
When I try to access the method:
Account.new.sales_at_to
I get a MethodError: undefined method sales_at_to for #<Account:0x007fd7910235a8>.
You are calling define_compounds(DateTimeComposer::attrs) at the end of the InstanceMethods module definition. At that point in the code, attrs is still an empty array, and self is the InstanceMethods module.
This means no methods will be defined, and even if they were, they would be bound to InstanceMethods's metaclass, making them class methods of that module, not instance methods of your Account class.
This happens because method calls inside the InstanceMethods module definition are evaluated as they are seen by the ruby interpreter, not when you call include ActiveRecordExtensions::InstanceMethods. An implication of this is that it is possible to run arbitrary code in the most unusual of places, such as within a class definition.
To solve the problem, you could use the included callback provided by ruby, which is called whenever a module is included in another:
module InstanceMethods
# mod is the Class or Module that included this module.
def included(mod)
DateTimeComposer::attrs.each do |attr|
mod.instance_eval <<-METHODS
def #{attr.to_s}_to
puts 'tes'
end
METHODS
end
end
end
As an additional suggestion, you should be able to achieve the same result by simply defining the methods when compound_datetime is called, thus eliminating the dependence on the attrs global class variable.
However, if you must have access to the fields which were declared as compound datetime, you should use class instance variables, which are unique to each class and not shared on the hierarchy:
module ClassMethods
def compound_datetime(*attrs)
#datetime_compounds = attrs
attrs.each do |attr|
instance_eval <<-METHODS
def #{attr.to_s}_to
puts 'tes'
end
METHODS
end
end
def datetime_compounds; #datetime_compounds; end;
end
class Account < ActiveRecord::Base
compound_datetime :sales_at, :published_at
end
class AnotherModel < ActiveRecord::Base
compound_datetime :attr1, :attr2
end
Account.datetime_compounds
=> [ :sales_at, :published_at ]
AnotherModel.datetime_compounds
=> [ :attr1, :attr2 ]

Initialize a Ruby class depending on what modules are included

I'm wondering what is the best way to initialize a class in ruby depending on modules included. Let me give you an example:
class BaseSearch
def initialize query, options
#page = options[:page]
#...
end
end
class EventSearch < BaseSearch
include Search::Geolocalisable
def initialize query, options
end
end
class GroupSearch < BaseSearch
include Search::Geolocalisable
def initialize query, options
end
end
module Search::Geolocalisable
extend ActiveSupport::Concern
included do
attr_accessor :where, :user_location #...
end
end
What I don't want, is having to initialize the :where and :user_location variables on each class that include the geolocalisable module.
Currently, I just define methods like def geolocalisable?; true; end in my modules, and then, I initialize these attributes (added by the module) in the base class:
class BaseSearch
def initialize query, options
#page = options[:page]
#...
if geolocalisable?
#where = query[:where]
end
end
end
class EventSearch < BaseSearch
#...
def initialize query, options
#...
super query, options
end
end
Is there better solutions? I hope so!
Why not override initialize in the module? You could do
class BaseSearch
def initialize query
puts "base initialize"
end
end
module Geo
def initialize query
super
puts "module initialize"
end
end
class Subclass < BaseSearch
include Geo
def initialize query
super
puts "subclass initialize"
end
end
Subclass.new('foo') #=>
base initialize
module initialize
subclass initialize
Obviously this does require everything that includes your modules to have an initialize with a similar signature or weird stuff might happen
See this code :
module Search::Geolocalisable
def self.included(base)
base.class_eval do
attr_accessor :where, :user_location #...
end
end
end
class EventSearch < BaseSearch
include Search::Geolocalisable
end

Resources