Last Updated: February 25, 2016
·
5.872K
· cizambra

Rails 4 - How to use method_missing and action_missing as allies

Introduction

This is my very first post in coderwall, so if it becomes a testament, I really apologize, and I ask you to let me know if you get bored with this article.

Ghost Methods

As a Rails developer, I wanted to improve my skills in Ruby so I started to do some research on the matter and I found an excellent source of information within the book "Metaprogramming Ruby" written by Paolo Perrotta .
Inside this book I learn a lot of ruby spells that helped me to improve my programming practices and the way I was creating my stuff.

One of the spells that caught my attention was the one related with the method_missing method. If you are new in Ruby, you probably don't know the power that lies over this feature.

Disclaimer: The information that comes in the next lines is dangerous in the wrong hands. I'm not responsible for the results that the misuse of this feature can bring.

Do you know about ghost methods? If the answer is no, I will give you a quick explanation that may enlight you in the use of this feature.

Let's suppose that you have a class named Person, with a name and age. The class can be defined as the following.

class Person
  def initialize(name, age)
    @name = name
    @age  = age
   end
end

You probably will want to access to that attributes, so the next thing that is necessary to do is add a getter method for each attribute, isn't it?

 class Person
   def initialize(name, age)
       @name = name
        @age  = age
    end

    # Getter method for name attribute.
    def name; @name; end

    # Getter method for age attribute
    def age; @age; end 
end

Now you can access each attribute using the getter methods. These methods are an example of mimic methods. Perrotta defines a mimic method as a method call that disguises itself as something else.

And if you want to set those attributes? The next necessary stuff we have to do is to add setter methods that allow us to change the value of the Person attributes.

 class Person
   def initialize(name, age)
       @name = name
        @age  = age
    end

    # Getter method for name attribute.
    def name; @name; end

    # Getter method for age attribute
    def age; @age; end 

    # Setter method for name attribute.
    def name=(name); @name = name; end

    # Setter method for age attribute
    def age=(age); @age = age; end
end

Now, you can access and set the attributes from outside. Probably you knew all of this stuff. Easy, you say.

But what if you want to make your code self maintainable?. Let's suppose your class Person has been opened by somebody else and is used in another context. How can we improve our class to ensure this third person that only adding a new attribute, it can be set and get from outside. Yes, it can be easy to add a new pair of methods for a single attribute, but what if the number of new attributes increases to ten, a hundred? (yes, i'm exaggerating).
The code will become more and more unmaintainable. So, in this case, ghost methods can be our saviors.

A ghost method, in few words, is a method that doesn't exist for the class, but you can call it anyway. And how the hell can we do that? method_missing() is the answer.

Let's do the following exercise. Run this code on irb.

class Person
  def initialize(name, age, address = nil)
    @name = name
    @age  = age
    @address = nil
   end
end

puts Person.new('Camilo', 25).age # => 25

Now, try this:

p = Person.new('Camilo', 25)
p.address = 'My home' # => NoMethodError: undefined method `address=' for #<Person:0x00000001d31a70...

So, if you try to set or get new attributes from the outside, you will have problems, and Ruby will let you know by invoking Kernel#method_missing(). But we can use that in our favor.

How can we trick ruby to define attributes dynamically? When a method doesn't exist, ruby invokes Kernel#method_missing()to let you know that the method that you are trying to call is missing either in the object, the object's class or the object's superclasses.

To achieve our goal we can override Kernel#method_missing(), because our object Person inherits directly from Kernel.

class Person
   def initialize(name, age)
       @name = name
        @age  = age
    end

    # Defining ghost methods using method_missing.
    def method_missing(name, *args, &block)
        return getter($1) if name.to_s =~ /^(\w*)$/ and self.instance_variables.include?("@#{$1}".to_sym)
        return setter($1, *args)  if name.to_s =~ /^(\w*)=$/ and self.instance_variables.include?("@#{$1}".to_sym)
        super
    end

    # Naive getter method that is invoked by method_missing
    def getter(method)
        self.instance_variable_get("@#{method}")
    end

    # Naive setter method that is invoked by method_missing
    def setter(method, value)
        self.instance_variable_set("@#{method}", value)
    end
end

Having this class modified, you can try now this piece of code:

p = Person.new('Camilo', 25) 
# => #<Person:0x00000002e9ee98 @name="Camilo", @age=25>
p.name # => "Camilo"

# Adding an attribute within the object scope.
p.instance_eval do
    @address = 'My home'
end
# => "My home"
p.address # => "My home"

Ghost Actions

The thing is, days ago I found myself doing a Rails project in which I had some actions that did exactly the same with the difference that each action called it own views and objects. I was looking a way to save time and lines of code, and I found a solution: action_mising(). Here is a simple example to explain my solution better.

# project/app/controllers/pets_controller.rb
class PetsController < ApplicationController
  allowed_ghost_actions [:cats, :dogs]
  before_action :set_pet, only: [:show, :edit, :update, :destroy], except: [ :cats, :dogs]

  # GET /pets
  # GET /pets.json
  def index
    @pets = Pet.all
  end

  # GET /pets/1
  # GET /pets/1.json
  def show
  end

  # GET /pets/new
  def new
    @pet = Pet.new
  end

  # GET /pets/1/edit
  def edit
  end

  # POST /pets
  # POST /pets.json
  def create
    @pet = Pet.new(pet_params)

    respond_to do |format|
      if @pet.save
        format.html { redirect_to @pet, notice: 'Pet was successfully created.' }
        format.json { render :show, status: :created, location: @pet }
      else
        format.html { render :new }
        format.json { render json: @pet.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /pets/1
  # PATCH/PUT /pets/1.json
  def update
    respond_to do |format|
      if @pet.update(pet_params)
        format.html { redirect_to @pet, notice: 'Pet was successfully updated.' }
        format.json { render :show, status: :ok, location: @pet }
      else
        format.html { render :edit }
        format.json { render json: @pet.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /pets/1
  # DELETE /pets/1.json
  def destroy
    @pet.destroy
    respond_to do |format|
      format.html { redirect_to pets_url, notice: 'Pet was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  #
  # If the action exists and is included in the allowed actions then is dynamically loaded.
  #
  def action_missing(name)
    begin
      self.send name
    rescue 
      super
    end
  end

  def pet_response(pet_type)
    # Consider the model Pet with a :name and a :pet_type, that can be :dog or :cat
    @pets = Pet.where("pet_type = ?", pet_type.to_s.singularize) 
    respond_to do |format|
      format.html { render "pets"}
    end
  end

  # 
  # As the action will call the method, it's necessary to handle the behavior if this method doesn't exist.
  #
  def method_missing(name, *args, &block)
    return pet_response(name) if respond_to?(name, true)
    super
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_pet
      @pet = Pet.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def pet_params
      params.require(:pet).permit(:name, :pet_type)
    end
end

As you can observe, there is a class macro named allowed_ghost_actions, this class macro is defined as a class method of ApplicationController.

class ApplicationController < ActionController::Base
    # Prevent CSRF attacks by raising an exception.
    # For APIs, you may want to use :null_session instead.
    protect_from_forgery with: :exception
    @@actions = nil

    def self.allowed_ghost_actions(methods)
      @@actions = methods
    end

    def respond_to?(method, include_private = false)
      (@@actions.include?(method) if !@@actions.nil?) || super
    end
end

With that macro we told the Controller that only that methods are accepted as ghost methods, and responds_to? is in charge of get this task done.

As you can see, pets_controller includes action_missing which tries in a beginning to call the method associated with the action, using Dynamic Dispatch, if the method doesn't exist (or it is not allowed by the controller), then Rails will raise an Exception.

Finally, to make your routes to work properly, you just have to modify your routes.rb file to map the route to the ghost method, as it was a regular one.

Rails.application.routes.draw do  
  resources :pets do
    collection do
      get "cats" => "pets#cats"
      get "dogs" => "pets#dogs"
    end
  end
  root 'pets#index'
end

Later, if a new type of pet is recorded, the list of pets with that type can be easily added to your code, you only will have to add the pluralized pet name within allowed_ghost_actions and the route in routes.rb.

Hope this article has been helpful to you.