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.