Wrap Ruby methods at run-time to intercept method calls - ruby-on-rails

I have a parent class that is similar to ActiveRecord, and I'm trying to open the class on load in our test suite. I want to wrap the initialize method so that it maintains a list of all of the subclasses that are initialized so I can make sure that all of the data for those classes is cleaned up between tests. Running a full wipe between tests winds up being too inefficient (plus I'm just interested in what the code would like to do this is).
My goal is to insert a new initialize method in the inheritance tree and just call super. All the while maintaining some list of all of the instantiated classes inside the parent class.
My attempt so far:
class NewActiveRecord
##__instantiated_classes = Set.new
def initialize(*args)
##__instantiated_classes.add(self.class)
super *args
end
def self.reset_tracking
##__instantiated_classes = Set.new
end
def self.get_instantiated_classes
##__instantiated_classes.to_a
end
end
class ActiveSupport::TestCase
teardown do
NewActiveRecord.get_instantiated_classes.each {|c| c.destroy_all}
NewActiveRecord.reset_tracking
end
end
Basically I want to wrap all methods that are called on subclasses of some parent to send their class to some predefined object their class

Did some exploring today and came up with the following solution. I'm still not sure how to unspy something yet though:
module Spy
def self.on_instance_method(mod, method, &block)
mod.class_eval do
# Stash the old method
old_method = instance_method(method)
# Create a new proc that will call both our block and the old method
proc = Proc.new do
block.call if block
old_method.bind(self).call
end
# Bind that proc to the original module
define_method(method, proc)
end
end
def self.on_class_method(mod, method, &block)
mod.class_eval do
# Stash the old method
old_method = singleton_method(method)
# Create a new proc that will call both our block and the old method
proc = Proc.new do
block.call if block
old_method.call
end
# Bind that proc to the original module
define_singleton_method(method, proc)
end
end
end
Usage from my tests looks like:
count = 0
Spy.on_instance_method(FakeClass, :value) { count += 1 }
fake = FakeClass.new(6)
fake.value.must_equal 6
count.must_equal 1

Related

ruby monkey patching on the fly

Is there a way to implement monkey patching while an object is being instantiated?
When I call:
a = Foo.new
Prior to the instance being instantiated, I would like to extend the Foo class based on information which I will read from a data store. As such, each time I call Foo.new, the extension(s) that will be added to that instance of the class would change dynamically.
tl;dr: Adding methods to an instance is possible.
Answer: Adding methods to an instance is not possible. Instances in Ruby don't have methods. But each instance can have a singleton class, where one can add methods, which will then be only available on the single instance that this singleton class is made for.
class Foo
end
foo = Foo.new
def foo.bark
puts "Woof"
end
foo.bark
class << foo
def chew
puts "Crunch"
end
end
foo.chew
foo.define_singleton_method(:mark) do
puts "Widdle"
end
foo.mark
are just some of the ways to define a singleton method for an object.
module Happy
def cheer
puts "Wag"
end
end
foo.extend(Happy)
foo.cheer
This takes another approach, it will insert the module between the singleton class and the real class in the inheritance chain. This way, too, the module is available to the instance, but not on the whole class.
Sure you can!
method_name_only_known_at_runtime = 'hello'
string_only_known_at_runtime = 'Hello World!'
test = Object.new
test.define_singleton_method(method_name_only_known_at_runtime) do
puts(string_only_known_at_runtime)
end
test.hello
#> Hello World!
Prior to the instance being instantiated, I would like to extend
Given a class Foo which does something within its initialize method:
class Foo
attr_accessor :name
def initialize(name)
self.name = name
end
end
And a module FooExtension which wants to alter that behavior:
module FooExtension
def name=(value)
#name = value.reverse.upcase
end
end
You could patch it via prepend:
module FooPatcher
def initialize(*)
extend(FooExtension) if $do_extend # replace with actual logic
super
end
end
Foo.prepend(FooPatcher)
Or you could extend even before calling initialize by providing your own new class method:
class Foo
def self.new(*args)
obj = allocate
obj.extend(FooExtension) if $do_extend # replace with actual logic
obj.send(:initialize, *args)
obj
end
end
Both variants produce the same result:
$do_extend = false
Foo.new('hello')
#=> #<Foo:0x00007ff66582b640 #name="hello">
$do_extend = true
Foo.new('hello')
#=> #<Foo:0x00007ff66582b280 #name="OLLEH">

Start method just once for addition / removal of association elements

I have a Composition model which has a has_and_belongs_to_many :authors.
I need to fire a method after a composition changed its authors, although, since it involves the creation of a PDF file (with the name of the authors), I want to call this method only once, regardless of the number of authors added / removed.
Of course I can add / remove existing authors from the composition, so a before_save / after_save won't work here (somehow it recognizes new authors added to the composition, but not existing ones).
So I tried using after_add / after_remove, but the callbacks specified here will be invoked for every author item added to / removed from the composition.
Is there a way to have a method called only once for every "batch action" of adding / removing items from this kind of relationship?
Here's what a service might look like:
class UpdateCompositionAuthorsService
attr_accessor *%w(
args
).freeze
class << self
def call(args={})
new(args).call
end
end # Class Methods
#======================================================================================
# Instance Methods
#======================================================================================
def initialize(args={})
#args = args
assign_args
end
def call
do_stuff_to_update_authors
generate_the_pdf
end
private
def do_stuff_to_update_authors
# do your 'batch' stuff here
end
def generate_the_pdf
# do your one-time logic here
end
def assign_args
args.each do |k,v|
class_eval do
attr_accessor k
end
send("#{k}=",v)
end
end
end
You would call it something like:
UpdateCompositionAuthorsService.call(composition: #composition, authors: #authors)
I got sick of remembering what args to send to my service classes, so I created a module called ActsAs::CallingServices. When included in a class that wants to call services, the module provides a method called call_service that lets me do something like:
class FooClass
include ActsAs::CallingServices
def bar
call_service UpdateCompositionAuthorsService
end
end
Then, in the service class, I include some additional class-level data, like this:
class UpdateCompositionAuthorsService
SERVICE_DETAILS = [
:composition,
:authors
].freeze
...
def call
do_stuff_to_update_authors
generate_the_pdf
end
...
end
The calling class (FooClass, in this case) uses UpdateCompositionAuthorsService::SERVICE_DETAILS to build the appropriate arguments hash (detail omitted).
I also have a method called good_to_go? (detail omitted) that is included in my service classes, so my call method typically looks like:
class UpdateCompositionAuthorsService
...
def call
raise unless good_to_go?
do_stuff_to_update_authors
generate_the_pdf
end
...
end
So, if the argument set is bad, I know right away instead of bumping into a nil error somewhere in the middle of my service.

Dynamically defining instance method within an instance method

I have a several classes, each of which define various statistics.
class MonthlyStat
attr_accessor :cost, :size_in_meters
end
class DailyStat
attr_accessor :cost, :weight
end
I want to create a decorator/presenter for a collection of these objects, that lets me easily access aggregate information about each collection, for example:
class YearDecorator
attr_accessor :objs
def self.[]= *objs
new objs
end
def initialize objs
self.objs = objs
define_helpers
end
def define_helpers
if o=objs.first # assume all objects are the same
o.instance_methods.each do |method_name|
# sums :cost, :size_in_meters, :weight etc
define_method "yearly_#{method_name}_sum" do
objs.inject(0){|o,sum| sum += o.send(method_name)}
end
end
end
end
end
YearDecorator[mstat1, mstat2].yearly_cost_sum
Unfortunately define method isn't available from within an instance method.
Replacing this with:
class << self
define_method "yearly_#{method_name}_sum" do
objs.inject(0){|o,sum| sum += o.send(method_name)}
end
end
...also fails because the variables method_name and objs which are defined in the instance are no longer available. Is there an idomatic was to accomplish this in ruby?
(EDITED: I get what you're trying to do now.)
Well, I tried the same approaches that you probably did, but ended up having to use eval
class Foo
METHOD_NAMES = [:foo]
def def_foo
METHOD_NAMES.each { |method_name|
eval <<-EOF
def self.#{method_name}
\"#{method_name}\".capitalize
end
EOF
}
end
end
foo=Foo.new
foo.def_foo
p foo.foo # => "Foo"
f2 = Foo.new
p f2.foo # => "undefined method 'foo'..."
I myself will admit it's not the most elegant solution (may not even be the most idiomatic) but I've run into similar situations in the past where the most blunt approach that worked was eval.
I'm curious what you're getting for o.instance_methods. This is a class-level method and isn't generally available on instances of objects, which from what I can tell, is what you're dealing with here.
Anyway, you probably are looking for method_missing, which will define the method dynamically the first time you call it, and will let you send :define_method to the object's class. You don't need to redefine the same instance methods every time you instantiate a new object, so method_missing will allow you to alter the class at runtime only if the called method hasn't already been defined.
Since you're expecting the name of a method from your other classes surrounded by some pattern (i.e., yearly_base_sum would correspond to a base method), I'd recommend writing a method that returns a matching pattern if it finds one. Note: this would NOT involve making a list of methods on the other class - you should still rely on the built-in NoMethodError for cases when one of your objects doesn't know how to respond to message you send it. This keeps your API a bit more flexible, and would be useful in cases where your stats classes might also be modified at runtime.
def method_missing(name, *args, &block)
method_name = matching_method_name(name)
if method_name
self.class.send :define_method, name do |*args|
objs.inject(0) {|obj, sum| sum + obj.send(method_name)}
end
send name, *args, &block
else
super(name, *args, &block)
end
end
def matching_method_name(name)
# ... this part's up to you
end

Ruby: Alter class static method in a code block

Given the Thread class with it current method.
Now inside a test, I want to do this:
def test_alter_current_thread
Thread.current = a_stubbed_method
# do something that involve the work of Thread.current
Thread.current = default_thread_current
end
Basically, I want to alter the method of a class inside a test method and recover it after that.
I know it sound complex for another language, like Java & C# (in Java, only powerful mock framework can do it). But it's ruby and I hope such nasty stuff would be available
You might want to take a look at a Ruby mocking framework like Mocha, but in terms of using plain Ruby it can be done using alias_method (documentation here) e.g.
beforehand:
class Thread
class << self
alias_method :old_current, :current
end
end
then define your new method
class Thread
def self.current
# implementation here
end
end
then afterwards restore the old method:
class Thread
class << self
alias_method :current, :old_current
end
end
Update to illustrate doing this from within a test
If you want to do this from within a test you could define some helper methods as follows:
def replace_class_method(cls, meth, new_impl)
cls.class_eval("class << self; alias_method :old_#{meth}, :#{meth}; end")
cls.class_eval(new_impl)
end
def restore_class_method(cls, meth)
cls.class_eval("class << self; alias_method :#{meth}, :old_#{meth}; end")
end
replace_class_method is expecting a class constant, the name of a class method and the new method definition as a string. restore_class_method takes the class and the method name and then aliases the original method back in place.
Your test would then be along the lines of:
def test
new_impl = <<EOM
def self.current
"replaced!"
end
EOM
replace_class_method(Thread, 'current', s)
puts "Replaced method call: #{Thread.current}"
restore_class_method(Thread, 'current')
puts "Restored method call: #{Thread.current}"
end
You could also write a little wrapper method which would replace a method, yield to a block and then ensure that the original method was reinstated afterwards e.g.
def with_replaced_method(cls, meth, new_impl)
replace_class_method(cls, meth, new_impl)
begin
result = yield
ensure
restore_class_method(cls, meth)
end
return result
end
Inside your test method this could then be used as:
with_replaced_method(Thread, 'current', new_impl) do
# test code using the replaced method goes here
end
# after this point the original method definition is restored
As mentioned in the original answer, you can probably find a framework to do this for you but hopefully the above code is interesting and useful anyway.

Make all subclasses of ActiveRecord::Base methods say their name

For cruft-removal purposes I would like to log whenever a method from one of my AR models is called.
I can get get all those classes with something like this:
subclasses = [] ; ObjectSpace.each_object(Module) {|m| subclasses << m if m.ancestors.include? ActiveRecord::Base } ; subclasses.map(&:name)
But then I need a list of only the methods defined on those classes (instance and class methods), and a way to inject a logger statement in them.
The result would be the equivalent of inserting this into every method
def foo
logger.info "#{class.name} - #{__method__}"
# ...
end
def self.foo
logger.info "#{name} - #{__method__}"
# ...
end
How can I do that without actually adding it to every single method?
Some awesome meta perhaps?
If you want only the methods defined in the class you can do this:
>> Project.instance_methods
=> ["const_get", "validates_associated", "before_destroy_callback_chain", "reset_mocha", "parent_name", "inspect", "slug_normalizer_block", "set_sequence_name", "require_library_or_gem", "method_exists?", "valid_keys_for_has_and_belongs_to_many_association=", "table_name=", "validate_find_options_without_friendly", "quoted_table_name" (another 100 or so methods)]
Only the methods defined in your class
>> Project.instance_methods(false)
=> ["featured_asset", "category_list", "before_save_associated_records_for_slugs", "asset_ids", "primary_asset", "friendly_id_options", "description", "description_plain"]
You should be using Aspect Oriented Programming pattern for this. In Ruby Aquarium gem provides the AOP DSL.
Create a log_method_initializer.rb in config/initializers/ directory.
require 'aquarium'
Aspect.new(:around, :calls_to => :all_methods,
:in_types => [ActiveRecord::Base] ) do |join_point, object, *args|
log "Entering: #{join_point.target_type.name}##{join_point.method_name}"
result = join_point.proceed
log "Leaving: #{join_point.target_type.name}##{join_point.method_name}"
result
end
Every method calls of classes inherited from ActiveRecord::Base will be logged.
You have
AR::Base.instance_methods
and
AR::Base.class_eval "some string"
so you can probably use them to put a header on every existing method.
For instance method call you can use this proxy pattern:
class BlankSlate
instance_methods.each { |m| undef_method m unless m =~ /^__/ }
end
class MyProxy < BlankSlate
def initialize(obj, &proc)
#proc = proc
#obj = obj
end
def method_missing(sym, *args, &block)
#proc.call(#obj,sym, *args)
#obj.__send__(sym, *args, &block)
end
end
Example:
cust = Customer.first
cust = MyProxy.new(cust) do |obj, method_name, *args|
ActiveRecord::Base.logger.info "#{obj.class}##{method_name}"
end
cust.city
# This will log:
# Customer#city
This is inspired from: http://onestepback.org/index.cgi/Tech/Ruby/BlankSlate.rdoc
You will need to find a way to apply this pattern on ActiveRecord::Base object creation.
For Aquarium, seems like adding method_options => :exclude_ancestor_methods does the trick.
I had the stack too deep problem as well.
Source
http://andrzejonsoftware.blogspot.com/2011/08/tracing-aspect-for-rails-application.html

Resources