rails, rest, and a bit of metaprogramming

06 August 2010
Matrix

Meta-programming invokes a high level of power and control over the programming language. Some, such as Wikipedia defines it as:

Metaprogramming is the writing of computer programs that write or manipulate other programs (or themselves) as their data or that do part of the work during compile time that is otherwise done at run time. In many cases, this allows programmers to get more done in the same amount of time as they would take to write all the code manually.

Pretty much it's about doing it on the fly, while the program is executing! This is all over the place in Rails. It is how it compute all the macros... such as associations, callbacks, filters, etc... I'm obviously not going to cover everything about metaprogramming on this article. There are already many books out there that cover this topic in greater detail. What I'm wrinting, is about one of the features that excites me the most... Modules, include and extend.

Modules

Like classes, modules are bundles of methods and constants.

Unlike classes, modules don’t have instances; instead, you specify that you want the functionality of a particular module to be added to the functionality of a class, or of a specific object.

Modules let you put your code in your own namespace so it doesn’t interfere with anyone else’s. Plus, it allows you have a number of options for mixing your functionality into other code.

Module methods can become class methods or instance methods depending on the context into which they are mixed. This is very useful and is much better than plain old inheritance... However, you can use that, too!

include

  module Foo
    def bar
      baz
    end
  end
  class Qux
    include Foo
  end
  u = Qux.new
  u.bar          # => baz
  Qux.bar        # => NoMethodErr

Including a module in a class definition mixes the module’s instance methods into those of the class. They become available as instance methods of the class. Although, very important you cannot override existing methods in the class this way; you must use aliases instead.

Mixed-in modules behave rather like superclasses.

A module mixed into a class may create instance variables in objects of that class. If you do this, you must avoid using variable names that may conflict with existing variables in the target class.

extend

Extend makes the methods of a module available in the receiver class. If you send :extend to an object, the methods of the module being extended are made available as instance methods on the object. On the other hand if you send :extend within a class definition, the module’s methods become class methods.

Object#extend.

  module Foo
    def bar
      baz
    end
  end
  class Qux
  end
  u = Qux.new
  u.bar           # => NoMethodError because Foo not
                  # yet mixed into Qux
  u.extend(Foo)   # u is an object
  u.bar           # => baz because bar is now available
                  # as an instance method on u
  Qux.bar         # => NoMethodError
  class << obj
    include Mod
  end
  module Foo
    def bar
      baz
    end
  end
  class Quux
    extend Foo
  end
  Quux.bar       # => baz because bar is now available
  e = Quux.new   # as a class method on Foo
  e.bar          # => NoMethodError

By the same token:

  v = Qux.new
  v.bar          # => NoMethodError
  class << v
    include Foo
  end
  v.bar          # baz

Class#extend.

  module Foo
    def bar
      baz
    end
  end
  class Quux
    extend Foo
  end
  Quux.bar       # => baz because bar is now available
                 # as a class method on Everything
  e = Quux.new
  e.bar          # => NoMethodError

Sorry, I went a bit crazy with the metasyntactic Variables ;)

Calling extend is equivalent to calling self.extend; in a class definition self is the class, so the module’s methods are added to the class.

Metasyntactic Variables

Okay, lets use this!

Thanks to this logic, creating plugins and adding functionality to our Rails apps is very simple. Here I created a module which DRYs out a whole lot our controllers.

Just drop the following module in your rails lib directory: ( Gist )

  module RestControllerMethods
    def self.included(base)
      base.send :before_filter, :build_obj, :only => [ :new, :create ]
      base.send :before_filter, :load_obj, :only => [ :show, :edit, :update, :destroy ]
    end

    def index
      self.instance_variable_set('@' + self.controller_name,
        scoper.find(:all))
    end

    def create
      if @obj.save
        flash[:notice] = "The #{cname.humanize.downcase} has been created."
        redirect_to redirect_url
      else
        render :action => 'new'
      end
    end

    def update
      if @obj.update_attributes(params[cname])
        flash[:notice] = "The #{cname.humanize.downcase} has been updated."
        redirect_to redirect_url
      else
        render :action => 'edit'
      end
    end

    def destroy
      @result = @obj.destroy
      respond_to do |f|
        f.html do
          if @result
            flash[:notice] = "The #{cname.humanize.downcase} has been deleted."
            redirect_to redirect_url
          else
            render :action => 'show'
          end
        end

        f.js do
          render :update do |page|
            if @result
              page.remove "#{@cname}_#{@obj.id}"
            else
              page.alert "Errors deleting #{@obj.class.to_s.downcase}: #{@obj.errors.full_messages.to_sentence}"
            end
          end
        end
      end
    end

    protected

      def cname; @cname ||= controller_name.singularize end

      def set_obj; @obj ||= self.instance_variable_get('@' + cname) end

      def load_obj; @obj = self.instance_variable_set('@' + cname,  scoper.find(params[:id])) end

      def scoper; Object.const_get(cname.classify) end

      def redirect_url; { :action => 'index' } end

      def build_obj
        @obj = self.instance_variable_set('@' + cname,
          scoper.is_a?(Class) ? scoper.new(params[cname]) : scoper.build(params[cname]))
      end    
    end

Now just include the module in any of your controllers and you would not have to declair any of the REST actions... They already have been included thanks to our module!

  class SurveysController < ApplicationController
    include  RestControllerMethods
  end

Also, you can extend the actions functionality so they can have more complex functionality. Here I'm extending the "new" action so it will accept nested attributes, defined in the Survey model...

This code is from Railscasts Episode #197: Nested Model Form Part 2. I'm only adding the RestControllerMethods module functionality.

  class Survey < ActiveRecord::Base
    has_many :questions, :dependent => :destroy
    accepts_nested_attributes_for :questions, :reject_if => λ { |a| a[:content].blank? }, :allow_destroy => true
  end
  class SurveysController < ApplicationController
    include  RestControllerMethods

    def new
      3.times do
        question = @survey.questions.build
        4.times { question.answers.build }
      end
    end
  end
Ninja Girl

This is just as a proof of concept... if you really want this sort of functionality in your controllers you should use Jose Valim inherited_resources plugin which has much more functionality plus properly tested code!

The source code for this project can be found on my github page.



blog comments powered by
Disqus