I'm trying my hands on metaprogramming after a long pause. I found a few questions but could not get an input to solve my problem so I hope someone can enlighten me.
In a rails 5 app, I am trying to write a concern that provides a class method to set configuration options. With those options, I want to define instance methods.
module Base64Attachable
extend ActiveSupport::Concern
class_methods do
attr_reader :base64_attachable_property
private
def base64_attachable(property)
#base64_attachable_property = property
end
end
included do
# ?
end
end
The concern above is used inside a User model:
class User < ApplicationRecord
include Base64Attachable
base64_attachable :image
end
In my understanding, the concern sets up the class method that is being called in the user model. However I do not seem to be able to get the base64_attachable_property inside the included block to define further methods based on the value of it. I thought I would find anything I need in self.class inside the included block, but that's not the case.
The aim in this case is to use define_method to define setters, getters and other methods for image in the user model.
What am I missing here?
The included block is run at the moment the concern is included on the class, the base64_attachable :image line is not run yet.
I'd suggest you follow what official gems does. Check ActiveStorage for example https://github.com/rails/rails/blob/530f7805ed5790af1d472a041bc74089dc183f47/activestorage/lib/active_storage/attached/model.rb#L35. It defines the methods that depends on that property right inside the class method (it uses class_eval, but I guess you can use define_method too):
def has_one_attached(name, dependent: :purge_later)
generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}
#active_storage_attached_#{name} ||= ActiveStorage::Attached::One.new("#{name}", self)
end
def #{name}=(attachable)
attachment_changes["#{name}"] =
if attachable.nil?
ActiveStorage::Attached::Changes::DeleteOne.new("#{name}", self)
else
ActiveStorage::Attached::Changes::CreateOne.new("#{name}", self, attachable)
end
end
CODE
has_one :"#{name}_attachment", ......etc....
Related
I need to know if I can include a module to an instantiated model.
What works today :
in the controller
#m = MyModel.create(params)
in the model
class Doc < ActiveRecord::Base
after_save :set_include
def set_include
if bool
self.class.send(:include, Module1)
else
self.class.send(:include, Module2)
end
end
end
and this works, but I'm afraid that self.class actually include the module for the class model an not the instantiated model
In this case, this will work.
The module methods are call after the object is saved.
But in many case, the controller will call some modules methods.
I thought of called the method set_include (up there) in a before_action of the controller.
But I really thinks that is not a good idea...
Any idea how I can really do that with in a good way ?
thanks !
Answer to your direct question is no. Your code only appears to be working and is actually not modifying instance of a class, but the class itself. So all instances of it will be getting this "benefit". Probably not what you wanted. Let me demonstrate with simple ruby example: https://repl.it/BnLO
What you can do instead is use extend with instance like: https://repl.it/BnLO/2
Applied to your code it would be:
class Doc < ActiveRecord::Base
after_save :set_include
def set_include
if bool
extend(Module1)
else
extend(Module2)
end
end
end
Also, self is not necessary. https://repl.it/BnLO/3
You need to use instance class (a.k.a eigenklass):
def set_include
singleton_class.instance_eval do
include bool ? Module1 : Module2
end
end
However the fact that you want to do this is suspicious and might lead to a disaster. So the question is: what are you really trying to achieve here - there surely is the better way of doing so.
I need some help with my plugin. I want to extend ActiveRecord::Base with a method that initializes another method that can be called in the controller.
It will look like this:
class Article < ActiveRecord::Base
robot_catch :title, :text
...
end
My attempt at extending the ActiveRecord::Base class with robot_catch method looks like following. The function will initialize the specified attributes (in this case :title and :text) in a variable and use class_eval to make the robot? function available for the user to call it in the controller:
module Plugin
module Base
extend ActiveSupport::Concern
module ClassMethods
def robot_catch(*attr)
##robot_params = attr
self.class_eval do
def robot?(params_hash)
# Input is the params hash, and this function
# will check if the some hashed attributes in this hash
# correspond to the attribute values as expected,
# and return true or false.
end
end
end
end
end
end
ActiveRecord::Base.send :include, Plugin::Base
So, in the controller, this could be done:
class ArticlesController < ApplicationController
...
def create
#article = Article.new(params[:article])
if #article.robot? params
# Do not save this in database, but render
# the page as if it would have succeeded
...
end
end
end
My question is whether if I am right that robot_catch is class method. This function is to be called inside a model, as shown above. I wonder if I am extending the ActiveRecord::Base the right way. The robot? function is an instance method without any doubt.
I am using Rails 3.2.22 and I installed this plugin as a gem in another project where I want to use this functionality.
Right now, it only works if I specifically require the gem in the model. However, I want it the functionality to be included as a part of ActiveRecord::Base without requiring it, otherwise I'd have to require it in every model I want to use it, not particularly DRY. Shouldn't the gem be automatically loaded into the project on Rails start-up?
EDIT: Maybe callbacks (http://api.rubyonrails.org/classes/ActiveSupport/Callbacks/ClassMethods.html) would be a solution to this problem, but I do not know how to use it. It seems a bit obscure.
First, I would suggest you make sure that none of the many many built in Rails validators meet your needs.
Then if that's the case, what you actually want is a custom validator.
Building a custom validator is not as simple as it might seem, the basic class you'll build will have this structure:
class SpecialValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
# Fill this with your validation logic
# Add to record.errors if validation fails
end
end
Then in your model:
class Article < ActiveRecord::Base
validates :title, :text, special: true
end
I would strongly suggest making sure what you want is not already built, chances are it is. Then use resources like this or ruby guides resources to continue going down the custom validator route.
Answer
I found out the solution myself. Bundler will not autoload dependencies from a gemspec that my project uses, so I had to require all third party gems in an engine.rb file in the lib/ directory of my app in order to load the gems. Now everything is working as it should.
Second: the robot_catch method is a class method.
I am just getting my hands on Concerns in Rails and try to implement a simple logging for ActiveRecord classes. In there I want to define the field that should go into the log and have the log written automatically after save.
What I have is this:
#logable.rb (the concern)
module Logable
extend ActiveSupport::Concern
#field = nil
module ClassMethods
def set_log_field(field)
#feild = field
end
end
def print_log
p "LOGGING: #{self[#index.to_s]}"
end
end
#houses.rb (the model using the concern)
class House < ActiveRecord::Base
include Logable
after_save :print_log
set_log_field :id
end
Unfortunately the call to set_log_field does not have an effect - or rather the given value does not make it to print_log.
What am I doing wrong?
Thanks for your help!
You probably mean this (btw, why not Loggable?):
# logable.rb
module Logable
extend ActiveSupport::Concern
# Here we define class-level methods.
# Note, that #field, defined here cannot be referenced as #field from
# instance (it's class level!).
# Note also, in Ruby there is no need to declare #field in the body of a class/module.
class_methods do
def set_log_field(field)
#field = field
end
def log_field
#field
end
end
# Here we define instance methods.
# In order to access class level method (log_field), we use self.class.
included do
def print_log
p "LOGGING: #{self.class.log_field}"
end
end
end
Update You also asked about what's the difference between methods in included block and those within method body.
To make a short resume there is seemingly no difference. In very good approximation you can consider them the same. The only minor difference is in dependency management. Great illustration of it is given in the end of ActiveSupport::Concern documentation. It worth reading, take a look!
I have several models that share a concern. Each model passes in a hash, which is meant to handle minor differences in the way they use the concern. I pass the hash in through a class method like so:
add_update_to :group, :user
The full code for the concern is:
module Updateable
extend ActiveSupport::Concern
attr_accessor :streams
module ClassMethods
def add_updates_to(*streams)
#streams = streams
end
end
module InstanceMethods
def update_streams
#streams.collect{|stream| self.public_send(stream)}
end
end
included do
has_one :update, :as => :updatable
after_create :create_update_and_history
end
private
def create_update_and_history
update = self.create_update(:user_id => User.current.id)
self.update_streams.each do |stream|
stream.histories.create(:update_id => update.id)
end
end
end
Most of this code works, but I'm having trouble passing the hash from the class to an instance. At the moment, I'm trying to achieve this effect by creating a virtual attribute, passing the hash to the attribute, and then retrieving it in the instance. Not only does this feel hacky, it doesn't work. I'm assuming it doesn't work because #streams is an instance variable, so the class method add_update_to can't actually set it?
Whatever the case, is there a better way to approach this problem?
You could probably use class variables here, but those are pretty reviled in the Ruby community due to their unpredictable nature. The thing to remember is that classes in Ruby are actually also instances of classes, and can have their own instance variables that are only accessible to themselves, and not accessible to their instances (if that is in any way clear).
In this case, you are defining behavior, and not data, so I think neither instance nor class variables are appropriate. Instead, I think your best bet is to define the instance methods directly within the class method, like this:
module Updateable
extend ActiveSupport::Concern
module ClassMethods
def add_updates_to(*streams)
define_method :update_streams do
streams.collect {|stream| public_send(stream) }
end
end
end
end
BTW, there is no hash involved here, so I'm not sure what you were referring to. *streams collects your arguments into an Array.
I am PHP dev and at the moment I am learning Rails (3) and of course - Ruby. I don't want to believe in magic and so I try to understand as much as I can about things that happen "behind" Rails. What I found interesting are the method calls like has_one or belongs_to in ActiveRecord models.
I tried to reproduce that, and came with naive example:
# has_one_test_1.rb
module Foo
class Base
def self.has_one
puts 'Will it work?'
end
end
end
class Model2 < Foo::Base
has_one
end
Just running this file will output "Will it work?", as I expected.
While searching through rails source I found responsible function: def has_one(association_id, options = {}).
How could this be, because it is obviously an instance (?) and not a class method, it should not work.
After some researching I found an example that could be an answer:
# has_one_test_2.rb
module Foo
module Bar
module Baz
def has_one stuff
puts "I CAN HAS #{stuff}?"
end
end
def self.included mod
mod.extend(Baz)
end
end
class Base
include Bar
end
end
class Model < Foo::Base
has_one 'CHEEZBURGER'
end
Now running has_one_test_2.rb file will output I CAN HAS CHEEZBURGER. If I understood this well - first thing that happens is that Base class tries to include Bar module. On the time of this inclusion the self.included method is invoked, which extends Bar module with Baz module (and its instance has_one method). So in the essence has_one method is included (mixed?) into Base class. But still, I don't fully get it. Object#extend adds the method from module but still, I am not sure how to reproduce this behaviour using extend. So my questions are:
What exactly happened here. I mean, still don't know how has_one method become class method? Which part exactly caused it?
This possibility to make this method calls (which looks like configuration) is really cool. Is there an alternative or simpler way to achieve this?
You can extend and include a module.
extend adds the methods from the module as class methods
A simpler implementation of your example:
module Bar
def has_one stuff
puts "I CAN HAS #{stuff}?"
end
end
class Model
extend Bar
has_one 'CHEEZBURGER'
end
include adds the methods from the module as instance methods
class Model
include Bar
end
Model.new.has_one 'CHEEZBURGER'
Rails uses this to dynamically add methods to your class.
For example you could use define_method:
module Bar
def has_one stuff
define_method(stuff) do
puts "I CAN HAS #{stuff}?"
end
end
end
class Model
extend Bar
has_one 'CHEEZBURGER'
end
Model.new.CHEEZBURGER # => I CAN HAS CHEEZBURGER?
I commend you for refusing to believe in the magic. I highly recommend you get the Metaprogramming Ruby book. I just recently got it and it was triggering epiphanies left and right in mah brainz. It goes over many of these things that people commonly refer to as 'magic'. Once it covers them all, it goes over Active Record as an example to show you that you now understand the topics. Best of all, the book reads very easily: it's very digestible and short.
Yehuda went through some alternatives on way to Rails3: http://yehudakatz.com/2009/11/12/better-ruby-idioms/
Moreover, you can use a (usually heavily abused, but sometimes quite useful) method_missing concept:
class Foo
def method_missing(method, *arg)
# Here you are if was some method that wasn't defined before called to an object
puts "method = #{method.inspect}"
puts "args = #{arg.inspect}"
return nil
end
end
Foo.new.abracadabra(1, 2, 'a')
yields
method = :abracadabra
args = [1, 2, "a"]
Generally, this mechanism is quite often used as
def method_missing(method, *arg)
case method
when :has_one
# implement has_one method
when :has_many
# ...
else
raise NoMethodError.new
end
end