Showing posts with label ruby. Show all posts
Showing posts with label ruby. Show all posts

Thursday, July 17, 2008

callback chains and metaclasses

For whatever reason (don't ask) our Rails app needed to be able to define callbacks on objects. As it stands now, you can only define callbacks on classes. Well, metaclasses are classes that pertain to a single specific object, so I figured I could add the callbacks there and everything will work.

Please excuse my extremely contrived example.

p = Post.find(...)
if p.contains_porn?
  class << p
    before_save :filter_content
  end
end
p.save # should trigger filter_content

Well it doesn't work.

After digging through the ActiveSupport source code, I saw that it only looks for callback chains in the object's class, not metaclass. That's not too hard to fix...

module ActiveSupport
  module Callbacks
    def run_callbacks(kind, options = {}, &block)
      callback_chain_method = "#{kind}_callback_chain"
      # Meta class inherits Class so we don't have to merge it in 1.9
      if RUBY_VERSION >= '1.9'
        metaclass.send(callback_chain_method).run(self, options, &block)
      else
        callbacks = self.class.send(callback_chain_method) | metaclass.send(callback_chain_method)
        callbacks.run(self, options, &block)
      end
    end
  end
end

Credit to Josh Peek for the Ruby 1.9 fix

Ok, so that got it working... with one major caveat: you cannot serialize objects that use callbacks. Ruby cannot serialize objects that have metaclasses and the run_callbacks method creates a metaclass whether you use it or not.

That's fine with me, I don't really like serializing objects anyways, but apparently it's a major problem for other people...

http://github.com/rails/rails/commit/e0846c8417093853f4f7f62732983e990c28d669
http://rails.lighthouseapp.com/projects/8994/tickets/575-callbacks-don-t-work-from-extended-modules

Josh Peek suggested a solution where we store the callback chains on the objects themselves, so object specific callbacks would be done like this...

p = Post.find(...)
p.before_save :filter_content if p.contains_porn?
p.save # should trigger filter_content

That seems like a very simple, clean and rational solution. I'll look into coding it up later.

Sunday, May 18, 2008

Metaprogramming Fun in Ruby

My friend, who is just learning Ruby, asked how he can automatically wrap some code around each user defined method in a class. The requirement (from his bosses) is to log when each method is being called.

To be more concrete, we want to factor out lines 3, 5, 9 and 11 from the following snippet.

    1 class AutoLogger
    2   def method_a
    3     puts "start logging method_a"
    4     puts "calling method_a"
    5     puts "end logging method_a"
    6   end
    7   
    8   def method_b
    9     puts "start logging method_b"
   10     puts "calling method_b"
   11     puts "end logging method_b"
   12   end
   13 end

The problem really boils down to two problems. First, how to redefine a method to be the original method body wrapped between the logging code. Second, how do we specify which methods in a class should be wrapped.

Each solution (except for the first one) relies on using Module#define_method to redefine the method in question. Sometimes we pass in a block that defines the original method, other times we make an UnboundMethod out of the original method and call it from the block that makes the new method body, binding it to self which at execution time is an instance of our class.

My friend said ideally, he wants every "user defined" method to automatically be wrapped.

The first solution I came up with was using method_missing. This solution has the drawback of having to refactor all your code to call log_some_method instead of some_method.

    1 class AutoLogger
    2   
    3   def method_missing(name, *args)
    4     name = name.to_s
    5     if name[0,4] == 'log_'
    6       method_name = name[4..-1]
    7       puts "start logging #{method_name}"
    8       self.send(method_name.to_sym, *args)
    9       puts "end logging #{method_name}"
   10     else
   11       super
   12     end
   13   end
   14   
   15   def method_a
   16     puts "calling method_a"
   17   end
   18   
   19   def method_b(arg1, arg2)
   20     puts "calling method_b(#{arg1}, #{arg2})"
   21   end
   22   
   23 end
   24 
   25 o = AutoLogger.new
   26 o.method_a
   27 o.method_b("test", 1)
   28 o.log_method_a
   29 o.log_method_b("test", 1)
   30 
   31 ##########
   32 # OUTPUT #
   33 ##########
   34 # calling method_a
   35 # calling method_b(test, 1)
   36 # start logging method_a
   37 # calling method_a
   38 # end logging method_a
   39 # start logging method_b
   40 # calling method_b(test, 1)
   41 # end logging method_b

With this approach, you can see that the original methods are kept intact and are callable. You have to call the special prefixed version of them to get the logging.

The next solution uses a special method to define methods instead of the def keyword.

    1 class AutoLogger
    2   
    3   def self.def_with_logging(method_name, &block)
    4     define_method(method_name) do |*args|
    5       puts "start logging #{method_name}"
    6       yield *args
    7       puts "end logging #{method_name}"
    8     end
    9   end
   10   
   11   def_with_logging(:method_a) do
   12     puts "calling method_a"
   13   end
   14   
   15   def_with_logging(:method_b) do |arg1, arg2|
   16     puts "calling method_b(#{arg1}, #{arg2})"
   17   end
   18   
   19 end
   20 
   21 o = AutoLogger.new
   22 o.method_a
   23 o.method_b("test", 1)
   24 
   25 ##########
   26 # OUTPUT #
   27 ##########
   28 # start logging method_a
   29 # calling method_a
   30 # end logging method_a
   31 # start logging method_b
   32 # calling method_b(test, 1)
   33 # end logging method_b

Again, this solution requires the programmer to refactor a lot of existing code (rewriting all your methods using def_with_logging instead of def).

The third solution is more "Rails-like" by adding a method to the class itself which can be called to add logging to existing methods.

    1 class AutoLogger
    2   @@inited = false
    3   
    4   def self.do_logging_for(*method_names)
    5     @@method_names ||= []
    6     @@method_names += method_names
    7   end
    8   
    9   do_logging_for :method_a, :method_b
   10   
   11   def initialize
   12     unless @@inited
   13       @@method_names.each do |method_name|
   14         self.class.class_eval do
   15           method = instance_method(method_name)
   16           define_method(method_name) do |*args|
   17             puts "start logging #{method_name}"
   18             method.bind(self).call(*args)
   19             puts "end logging #{method_name}"
   20           end
   21         end
   22       end
   23       @@inited = true
   24     end
   25   end
   26   
   27   def method_a
   28     puts "calling method_a"
   29   end
   30   
   31   def method_b(arg1, arg2)
   32     puts "calling method_b(#{arg1}, #{arg2})"
   33   end
   34   
   35 end
   36 
   37 o = AutoLogger.new
   38 o.method_a
   39 o.method_b("test", 1)
   40 
   41 ##########
   42 # OUTPUT #
   43 ##########
   44 # start logging method_a
   45 # calling method_a
   46 # end logging method_a
   47 # start logging method_b
   48 # calling method_b(test, 1)
   49 # end logging method_b

Notice how most of the work is deferred to initialize. This is so we can call do_logging_for before we define the methods it works on.

The fourth approach takes the least work because it does not require the programmer to do anything special or extra to get the desired effect. It all happens automatically.

    1 class AutoLogger
    2   
    3   def self.method_added(method_name)
    4     
    5     # so we don't get stuck in infinite recursion
    6     @@seen_methods ||= {}
    7     return if @@seen_methods.has_key?(method_name)
    8     @@seen_methods[method_name] = true
    9     
   10     method = instance_method(method_name)
   11     define_method(method_name) do |*args|
   12       puts "start logging #{method_name}"
   13       method.bind(self).call(*args)
   14       puts "end logging #{method_name}"
   15     end
   16   end
   17   
   18   def method_a
   19     puts "calling method_a"
   20   end
   21   
   22   def method_b(arg1, arg2)
   23     puts "calling method_b(#{arg1}, #{arg2})"
   24   end
   25   
   26 end
   27 
   28 o = AutoLogger.new
   29 o.method_a
   30 o.method_b("test", 1)
   31 
   32 ##########
   33 # OUTPUT #
   34 ##########
   35 # start logging method_a
   36 # calling method_a
   37 # end logging method_a
   38 # start logging method_b
   39 # calling method_b(test, 1)
   40 # end logging method_b

Class#method_added is a callback that gets fired, well, when you think it does. The interesting thing to note here is that we can go into infinite recursion because of how we redefinite each method. When a method is a defined the callback is triggered, in the callback we redefine the method which causes the callback to be triggered again.

That's it. Pretty neat what you can do with Ruby, huh?

Monday, January 21, 2008

Rail Plugin: param_protected

It is a Ruby on Rails plugin that provides param_protected and param_accessible methods on controllers analogous to the attr_protected and attr_accessible methods for models.

It is a very simple -- all it does it filter out specified parameters from a request.

Why?


Good question... you can ready about why attr_protected sucks here, or you can just read my following little rant...

What's the goal of attr_protected? To protect us from user input, not from ourselves. When I used attr_protected, I had to refactor tons of code in models, controllers and tests (that already worked well) to not use the mass attribute setters.

Was this massive code refactoring really worth the protection from the very few places were I do something like:

User.new(params[:user])

or

User.update_attributes(params[:user])

Truth of the matter, I was hardly ever passing params (or a subset thereof) to a mass attribute setter. So no, it wasn't worth the massive refactoring job.

Installation


git clone git://github.com/cjbottaro/param_protected.git vendor/plugins/param_protected

Usage


class UsersController < ApplicationController
  param_protected :user_id
end

class AccountController < ApplicationController
  param_accessible :account_id
end

param_protected is used to blacklist and param_accessible is used to whitelist.

You can give it an array of param names to filter:

param_protected [:user_id, :some_other_param]

param_protected and param_accessible are both just before filters, so the usual :only and :except arguments can be used:

param_protected :user_id, :only => :some_action
param_protected :user_id, :only => [:some_action, :another_action]
param_protected :user_id, :except => :some_action
param_protected :user_id, :except => [:some_action, :another_action]

You can protect nested params also (removes params[:user][:user_id]):

param_protected 'user/user_id'

param_protected is aware of array params and handles them properly.

Caveats (IMPORTANT!!!!)


Because param_protected is really a before filter (uses prepend_before_filter), you must take special care to ensure that it runs before any of your other before filters!! If it is not, some of your before filters might have access to some params they shouldn't.

Tests


rake test should work (from the plugin's root dir), though it's far from comprehensive.

Documentation


Please see the README for usage instructions and examples.

Monday, October 22, 2007

migration_izzle

Managing migration version numbers in Rails among multiple developers using the same database is a pain in the ass. You know the drill, you svn up, create a migration, try to run db:migrate but low and behold, the db version is already 10 versions above your migration's.

This plugin offers a simple solution: create and manage a "history" table that records which migrations have and have not been run and modify rake db:migrate to behave accordingly.

For documentation, installation and usage instructions, please see the migration_izzle github page (scroll down).

Sunday, October 21, 2007

Better AppConfig Plugin for Rails

There are plenty of "AppConfig" plugins for Rails already out there, so why did I make one? Three reasons:

  • Break the config files out into different files, one for each environment plus a shared common one.
  • Allows for nested sections and preserves object member notation for them.
  • Allows for lists (arrays).

Installation


git clone git://github.com/cjbottaro/app_config.git vendor/plugins/app_config

Usage


See the README on github (scroll down on the page).

Tests


All the unit tests should pass.

cd RAILS_ROOT/vendor/plugins/app_config
rake test