Ruby: hooks for methods - ruby-on-rails

I want to find a gem or to write a code that implements hooks for methods.
class A
include SomeModule
before_hook :meth, lambda { puts 'bla' }
def meth
puts 'meth'
end
end
# A.new.meth => "bla\nmeth\n"
I am using Rails and I know about callbacks and filters but
meth isn't an action
I don't wand to change how I method call
Help me please...
UPDATE
I find a gem for automating this code:
include ActiveSupport::Callbacks
define_callbacks :meth_callback
set_callback :meth_callback, :before do |object|
# my code
end
def meth_with_callback
run_callbacks(:meth_callback) { meth }
end
alias_method_chain :meth, :callback

You can use ActiveModel::Callbacks
define_model_callbacks :create
def create
run_callbacks :create do
# do your thing here
end
end
You can even write a little helper method to hide this run_callbacks line. It may look like this:
hooked_method :create do
# do your thing here
end

Related

Run block defined on class within instance's scope

I would like to create something similar to ActiveRecord validation: before_validate do ... end. I am not sure how could I reference attributes of class instance from the block given. Any idea?
class Something
attr_accessor :x
def self.before_validate(&block)
#before_validate_block = block
end
before_validate do
self.x.downcase
end
def validate!
# how should this method look like?
# I would like that block would be able to access instance attributes
end
end
#3limin4t0r's answer covers mimicing the behavior in plain ruby very well. But if your are working in Rails you don't need to reinvent the wheel just because you're not using ActiveRecord.
You can use ActiveModel::Callbacks to define callbacks in any plain old ruby object:
class Something
extend ActiveModel::Callbacks
define_model_callbacks :validate, scope: :name
before_validate do
self.x.downcase
end
def validate!
run_callbacks :validate do
# do validations here
end
end
end
Featurewise it blows the socks off any of the answers you'll get here. It lets define callbacks before, after and around the event and handles multiple callbacks per event.
If validations are what you really are after though you can just include ActiveModel::Validations which gives you all the validations except of course validates_uniqueness_of which is defined by ActiveRecord.
ActiveModel::Model includes all the modules that make up the rails models API and is a good choice if your are declaring a virtual model.
This can be achieved by using instance_eval or instance_exec.
class Something
attr_accessor :x
# You need a way to retrieve the block when working with the
# instance of the class. So I've changed the method so it
# returns the +#before_validate_block+ when no block is given.
# You could also add a new method to do this.
def self.before_validate(&block)
if block
#before_validate_block = block
else
#before_validate_block
end
end
before_validate do
self.x.downcase
end
def validate!
block = self.class.before_validate # retrieve the block
instance_eval(&block) # execute it in instance context
end
end
How about this?
class Something
attr_accessor :x
class << self
attr_reader :before_validate_blocks
def before_validate(&block)
#before_validate_blocks ||= []
#before_validate_blocks << block
end
end
def validate!
blocks = self.class.before_validate_blocks
blocks.each {|b| instance_eval(&b)}
end
end
Something.before_validate do
puts x.downcase
end
Something.before_validate do
puts x.size
end
something = Something.new
something.x = 'FOO'
something.validate! # => "foo\n3\n"
This version allows us to define multiple validations.

Ruby / Rails meta programing, how to define instance and class methods dynamically?

I am trying to make my life simpler inside of a large production Rails 6.0 website. I have a bunch of data that I serve from Redis as denormalized hashes, because Rails, with all the includes and associations is very very slow.
To keep things DRY, I'd like to use a Concern (or module) that can be included within ApplicationRecord that allows me to dynamically define the collection methods for the data I want to store.
This is what I have so far:
class ApplicationRecord < ActiveRecord::Base
include DenormalizableCollection
# ...
end
# The model
class News < ApplicationRecord
denormalizable_collection :most_popular
# ...
end
# The Concern
module DenormalizableCollection
extend ActiveSupport::Concern
class_methods do
def denormalizable_collection(*actions)
actions.each do |action|
# define News.most_popular
define_singleton_method "#{action}" do
collection = Redis.current.get(send("#{action}_key"))
return [] unless collection.present?
JSON.parse(collection).map { |h| DenormalizedHash.new(h) }
end
# define News.set_most_popular
define_singleton_method "set_#{action}" do
Redis.current.set(send("#{action}_key"), send("#{action}_data").to_json)
end
# define News.most_popular_data, which is a method that returns an array of hashes
define_singleton_method "#{action}_data" do
raise NotImplementedError, "#{action}_data is required"
end
# define News.most_popular_key, the index key to use inside of redis
define_singleton_method "#{action}_key" do
"#{name.underscore}_#{action}".to_sym
end
end
end
end
end
This works, but I doesn't seems right because I cannot also define instance methods, or ActiveRecord after_commit callbacks to update the collection inside of Redis.
I'd like to add something like the following to it:
after_commit :set_#{action}
after_destroy :set_#{action}
But obviously these callbacks require an instance method, and after_commit :"self.class.set_most_popular" causes an error to be thrown. So I had wanted to add an instance method like the following:
class News
# ...
def reset_most_popular
self.class.send("set_most_popular")
end
end
I have been reading as many articles as I can and going through the Rails source to see what I'm missing - as I know I'm defo missing something!
The key here is to use class_eval to open up the class you are calling denormalizable_collection on.
A simplified example is:
class Foo
def self.make_method(name)
class_eval do |klass|
klass.define_singleton_method(name) do
name
end
end
end
make_method(:hello)
end
irb(main):043:0> Foo.hello
=> :hello
module DenormalizableCollection
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def denormalizable_collection(*actions)
actions.each do |action|
generate_denormalized_methods(action)
generate_instance_methods(action)
generate_callbacks(action)
end
end
private
def generate_denormalized_methods(action)
self.class_eval do |klass|
# you should consider if these should be instance methods instead.
# define News.most_popular
define_singleton_method "#{action}" do
collection = Redis.current.get(send("#{action}_key"))
return [] unless collection.present?
JSON.parse(collection).map { |h| DenormalizedHash.new(h) }
end
# define News.most_popular
# define News.set_most_popular
define_singleton_method "set_#{action}" do
Redis.current.set(send("#{action}_key"), send("#{action}_data").to_json)
end
# define News.most_popular_data, which is a method that returns an array of hashes
define_singleton_method "#{action}_data" do
raise NotImplementedError, "#{action}_data is required"
end
# define News.most_popular_key, the index key to use inside of redis
define_singleton_method "#{action}_key" do
"#{name.underscore}_#{action}".to_sym
end
end
end
def generate_callbacks(action)
self.class_eval do
# Since callbacks call instance methods you have to pass a
# block if you want to call a class method instead
after_commit -> { self.class.send("set_#{action}") }
after_destroy -> { self.class.send("set_#{action}") }
end
end
def generate_instance_methods(action)
class_eval do
define_method :a_test_method do
# ...
end
end
end
end
end
Note here that I'm not using ActiveSupport::Concern. Its not that I don't like it. But in this case it adds an additional level of metaprogramming thats enough to make my head explode.
Have you tried something like:
class_methods do
def denormalizable_collection(*actions)
actions.each do |action|
public_send(:after_commit, "send_#{action}")
...
end
end
end

Run custom callback without run_callback

I am trying to create around initialize callback for benchmarks.
class BaseProcessor
extend ActiveModel::Callbacks
define_callbacks :initialize
set_callback :initialize, :around, :run_benchmark
protected
def run_benchmark
#benchmark = Benchmark.realtime do
yield
end
end
end
Then other classes are inherited from this BaseProcessor
class Child < BaseProcessor
def initialize
run_callbacks :initialize do
# some stuff
end
end
end
So in every child I have to invoke run_callbacks. So my question is: can I avoid it?
If I understand you correctly. Something like this should work even without around callbacks.
class BaseProcessor
def self.new(*args)
Benchmark.realtime do
super
end
end
end

Invoke a method before running another method in Rails

I have a Model, which has method_1 to method_10. I also have ModelObserver.
I would like to notifiy ModelObserver before invoking method1 to method_9, but not method_10.
Is there a DRY way to write this, instead of repeating notify_observers(:after_something) in all 9 methods?
Add a file called monkey_patches.rb in config/initializers dirctory.
class Object
def self.method_hook(*args)
options = args.extract_options!
return unless (options[:before].present? or options[:after].present?)
args.each do |method_name|
old_method = instance_method(method_name) rescue next
define_method(method_name) do |*args|
# invoke before callback
if options[:before].present?
options[:before].is_a?(Proc) ? options[:before].call(method_name, self):
send(options[:before], method_name)
end
# you can modify the code to call after callback
# only when the old method returns true etc..
old_method.bind(self).call(*args)
# invoke after callback
if options[:after].present?
options[:after].is_a?(Proc) ? options[:after].call(method_name, self):
send(options[:after], method_name)
end
end
end
end
end
The patch enables you to add before and after callbacks on an instance method of a class. A hook can be:
The name of an instance method which accepts one parameter
A lambda accepting two parameters
Multiple hooks can be registered on a same method. The method being hooked should come before the hook.
E.g:
class Model < ActiveRecord::Base
def method1
end
def method2
end
def method3
end
def method4
end
def update_cache
end
# instance method name as `after` callback parameter
method_hook :method1, :method2, :after => :update_cache
# lambda as `before` callback parameter
method_hook :method1, :method2,
:before => lambda{|name, record| p name;p record}
# lambda as `after` callback parameter
method_hook :method3, :method4,
:after => lambda{|name, record|
Model2.increment_counter(:post_count, record.model2_id)}
end
How about something like this?
def notify; puts "Was notified."; end
def method1; end
def method2; end
def method3; end
def original
notify
method1
notify
method2
method3
end
def dry
[:method1, :method2].each do |m|
notify
send(m)
end
method3
end
original
dry

Rails - alias_method_chain with a 'attribute=' method

I'd like to 'add on' some code on a model's method via a module, when it is included. I think I should use alias_method_chain, but I don't know how to use it, since my 'aliased method' is one of those methods ending on the '=' sign:
class MyModel < ActiveRecord::Base
def foo=(value)
... do stuff with value
end
end
So this is what my module looks right now:
module MyModule
def self.included(base)
base.send(:include, InstanceMethods)
base.class_eval do
alias_method_chain 'foo=', :bar
end
end
module InstanceMethods
def foo=_with_bar(value) # ERROR HERE
... do more stuff with value
end
end
end
I get an error on the function definition. How do get around this?
alias_method_chain is a simple, two-line method:
def alias_method_chain( target, feature )
alias_method "#{target}_without_#{feature}", target
alias_method target, "#{target}_with_#{feature}"
end
I think the answer you want is to simply make the two alias_method calls yourself in this case:
alias_method :foo_without_bar=, :foo=
alias_method :foo=, :foo_with_bar=
And you would define your method like so:
def foo_with_bar=(value)
...
end
Ruby symbols process the trailing = and ? of method names without a problem.

Resources