Thursday, December 30, 2010

Review of Crafting Rails Applications, by José Valim

José Valim is one of the newest members of the Rails core team. Apart from this he has also developed some good gems, Devise, Responders, and SimpleForm, that I use for almost every project.

And now he has written a book, a really good book, about advanced Rails programming techniques, called Crafting Rails Applications.

The book is only 180 pages long, and contains 7 chapters. I wish more books was this thin. The books is written in a clear concise way. José's lightweight test-driven approach clearly introduces every problem it intends to solve, and then solves the problem by introducing a new concept in the Rails framework. Most of the solutions are only a few lines of code, showing the power of Rails and also showing the power of having intimate knowledge of the framework. By hooking into the framework at appropriate places, José is able to provide elegant solutions to a range of problems.

I highly recommend the book to anyone interested in Rails.

If this was an ordinary book, this review could have ended here, but since the book is full of things I really wanted to know but didn't take the time to learn I decided to internalize them by describing parts of the book in my own words.

Chapter 1, Creating our own renderer

In this chapter we learn how to add a new renderer Rails. We add a renderer that handles generating PDF files with Prawn, allowing us to write code that looks like this.

class HomeController < ApplicationController 
  def index
    respond_to do |format| 
      format.html
      # This is the new capability, pdf rendering.
      format.pdf { render :pdf => "contents" }
    end 
  end
end

To allow the following we first have to register pdf as a mime type. We can do this with:

require "action_controller"
# Register a new mime type
Mime::Type.register "application/pdf", :pdf

# Registering the type allows us to write
respond_to do |format|
  format.pdf
end

With that out of the way all we have to do is to register a renderer, that will handle the actual rendering of the registered mime type. This is done with:

require "prawn" 

# Adds a new renderer to ActionController.Renderers
ActionController::Renderers.add :pdf do |filename, options|
  pdf = Prawn::Document.new pdf.text render_to_string(options) 
  send_data(pdf.render, :filename => "#{filename}.pdf",
      :type => "application/pdf", :disposition => "attachment")
end

This is all that is needed to add a rendering of a new type to Rails. José also goes into details about other points of interest where the rendering stack can be customized.

Chapter 2, Easy models with Active Model

Active Model is a Rails module that abstracts common behavior needed by different kinds om models such as Active Record, Active Resource and third party modules like Mongoid. Apart from giving developers of new models utility functionality. Active Model also provides tests for being Active Model compliant. To be compliant means to play well with the Rails view and controllers.

José implements a model for creating sending mail. In doing so he introduces us to a bunch of Active Model modules that will do our work for us.

Here are the modules in alphabetical order:

ActiveModel::AttributeMethods

When you include ActiveModel::AttributeMethods in your model, you get access to helper methods that will help you define additional methods for dealing with the attributes of your model. An example

class Tapir
  include ActiveModel::AttributeMethods
   
  # Define the attributes 
  attr_accessor 'name', 'color'

  # We want to create clear methods for every attribute
  attribute_method_prefix 'clear_'

  # We also want to have query methods for every attribute
  attribute_method_suffix '?'

  # Define the attribute methods that will call the protected methods below
  define_attribute_methods ['name', 'color']

protected
  # The name of this method corresponds to the prefix above
  def clear_attribute(attribute)
    send("#{attribute}=", nil)
  end

  # The name of this method corresponds to the suffix above
  def attribute?(attribute)
    send(attribute).present?
  end
end

   t = Tapir.new
 => #<Tapir:0x00000102769b90> 
> t.name?
 => false 
> t.name= 'kalle'
 => "kalle" 
> t.name?
 => true 
> t.clear_name
 => nil 
> t.name?
 => false 
> t.name

This is the gist of attribute methods, there is also an affix method that will allow you to add both a suffix and a prefix to your attributes. It is also worth taking a look inside the implmentation of this module, since the methods are defined on first usage through method_missing.

ActiveModel::Callbacks

ActiveModel::Callbacks provides methods for creating before_, after_, and `around_' callbacks to your model methods. An example:

class Tapir
  extend ActiveModel::Callbacks

  define_model_callbacks :snort

  def snort
    _run_snort_callbacks do
      snort_snort_snort
    end
  end

end

Then in inheriting classes I can use the callbacks as such:

class MountainTapir < Tapir
  before_snort :do_before_snort

  def before_snort
    i_am_so_cold
  end
end

ActiveModel::Conversion

This module assists with converting to, and representing a specific model instance. It contains three methods.

  • to_model, used for converting to an active model compliant model, in the default case it just returns self.
  • to_param, used for representing a model in routing.
  • to_key, used for representing a model from in a dom page.

ActiveModel::Naming and ActiveModel::Translation

ActiveModel::Naming contains one method model_name, that returns a subclass of String, that handles pluralization, etc. It has methods such as plural, human.

ActiveModel::Translation deals with I18N by adding functionality to human that handles the name lookup by keys using the preferred I18n.backend.

ActiveModel::Validations

The last, but not least of the models, ActiveModel::Validations, deals with validations. José demostrates how naming convetions and Rails constant lookup allow us to add new validators to Rails with ease. An example:

module Validators
  class TapirKindValidator < ActiveModel::EachValidator 
    def validate_each(record, attribute, value)
      unless [:baird, :mountain, :malayan, :lowland].include? value
        record.errors.add(attribute, :invalid, options) 
      end
    end
  end
end


If I now include my new validator wherever I want to use it. I can write code like:

class Tapir
  include Validators
  
  attr_accessor :kind
  validates :kind, :tapir_kind => true
end

t = Tapir.new :kind => :mexican
t.valid? 
# => false
t.errors[:kind] 
# ["is invalid"]


This example is unnecessarily complicated, since the validation could be fixed with:

class Tapir
    attr_accessor :kind
    validates_inclusion_of :kind, :in =>  [:baird, :mountain, :malayan, :lowland]
end

append_view_path

There is one more interesting thing in this chapter, append_view_path. It can be used to add a directory inside a gem to the view lookup path. This is essential to be able to deliver custom Rails components, Railties, with default views.

Chapter 3, Building a template management system

In the third chapter of the book,José takes on view resolvers. A resolver is responsible for looking up the appropriate template for a given request. The traditional resolver looks up the templates by path in the file system, but José guides us through creating a resolver that looks up the template in a database. Perfect for the core of a CMS system. I'm going to alter José's resolver to do the lookup in MongoDB instead.

# Most of this code is borrowed directly from Jos&eacute; Valim
# Only the Mongoid Part is written by me :)
require 'mongoid'
require 'singleton'

class MongoTemplate
  include Mongoid::Document
  field :body
  field :path
  field :locale
  field :format
  field :handler
  field :partial, :type => Boolean
  field :updated_at, :type => Date
    
  validates :body, :path, :presence => true
  validates :format, :inclusion => Mime::SET.symbols.map(&:to_s) 
  validates :locale, :inclusion => I18n.available_locales.map(&:to_s) 
  validates :handler, :inclusion => 
    ActionView::Template::Handlers.extensions.map(&:to_s)

  class Resolver < ActionView::Resolver 
    include Singleton
  protected
    def find_templates(name, prefix, partial, details)
      conditions = {
        :path => normalize_path(name, prefix),
        :locale => normalize_array(details[:locale]).first, 
        :format => normalize_array(details[:formats]).first,
        :handler.in => normalize_array(details[:handlers]), 
        :partial => partial || false
      }
   
      MongoTemplate.where(conditions).map do |record| 
        initialize_template(record)
      end 
    end
    
    # Normalize name and prefix, so the tuple ["index", "users"] becomes 
    # "users/index" and the tuple ["template", nil] becomes "template". 
    def normalize_path(name, prefix)
      prefix.present? ? "#{prefix}/#{name}" : name
    end
   
    # Normalize arrays by converting all symbols to strings.
    def normalize_array(array)
      array.map(&:to_s)
    end

    # Initialize an ActionView::Template object based on the record found.
    def initialize_template(record) 
      source = record.body 
      identifier = "MongoTemplate - #{record.id} - #{record.path.inspect}" 
      handler = ActionView::Template.registered_template_handler(record.handler)
      details = { 
        :format => Mime[record.format], 
        :updated_at => record.updated_at, 
        :virtual_path => virtual_path(record.path, record.partial)
      }
      ActionView::Template.new(source, identifier, handler, details)
    end
    
    # Make paths as "users/user" become "users/_user" for partials.
    def virtual_path(path, partial) 
      return path unless partial 
      if index = path.rindex("/")
        path.insert(index + 1, "_")
      else
        "_#{path}"
      end 
    end
  end
end

A Resolver is implemented by extending ActionView::Resolver and implementing the method find_templates(name, prefix, partial, details) There are a lot of extra information in the book, about caching etc. Obviously Rails caches the templates since it would be too slow to create a template every time. That is why we have the template as a singleton and that is why we clear the cache in the after_save hook above. The hook in Mongoid works exactly the same as in Active Record. Thank you Active Model!

Chapter 4, Multipart Emails with Markdown and Erb

In this chapter José walks us through three topics. The template handler API, Rails generators and Railties. I'm going to skip over the last two in this chapter. Generators are a large topic and they deserve a blog post of their own. José does a great job desribing how they work. The same goes for Railties and Rails Engines, they are definitely worthy of a blog post of their own.


# 
# Again most of the code is stolen from Jos&eacute; :)
#
module ScrambleHandler
  
  # Lookup the ERb handler
  def self.erb_handler
    @@erb_handler ||= ActionView::Template.registered_template_handler(:erb)
  end

  def self.call(template)
    # Call the erb handler, then call the new scrambler method.
    source = erb_handler.call(template)
    if template.formats.include?(:html)
      "Scrambler.from_html(begin;#{source};end).to_s"
    else
      "Scrambler.from_text(begin;#{source};end).to_s"
    end
  end

end

# Register our new handler, it handles index.html.scramble, show.html.scramble, ...
ActionView::Template.register_template_handler :scramble, ScrambleHandler

# This module scrambles text.
# Scrambler.from_text('Once upon a time a beautiful tapir came.')
# -> Ocne upon a time a baeuuitfl tpiar came.
module Scrambler
  def self.from_html html
    doc = Nokogiri::HTML(html) 
    doc.xpath('//text()').each do |node|
      node.content = from_text(node.content)
    end
    doc.to_html
  end

  def self.from_text text
    tokenize(text).map{|token| scramble(token)}.join('') 
  end

  def self.tokenize text
    return nil unless text
    text.scan(/(\p{Word}+|\W+|)/um).flatten
  end

  def self.scramble word
    return word if word =~ /\W/
    return word if word.size < 4
    arr = word.split(//)
    arr[0] + arr[1...-1].shuffle.join('') + arr[-1]
  end
end

Chapter 5, Publishing and subscribing to your applications events

In this chapter José uses the Notifications API to store notifcations in a MongoDB. He also shows how to use Rack middleware to configure what request to log. In order to configure the middleware he uses a Rails Engine. I'm just going to show how the Notifications API works here, and I'll let you read the rest in the book.

# Subscribe to all events and print them to the console
ActiveSupport::Notifications.subscribe do |*args|
  p ActiveSupport::Notifications::Event.new(*args)
end

# Instrument the rendering of Foo
ActiveSupport::Notifications.instrument(:render, :extra => :information) do
  render :text => "Foo"
end


Chapter 6, DRY controllers with Responders

In this chapter José goes through the implementation of responders. This implementation allows you to DRY up your controllers by factoring out common behavior.

def index
  @users = User.all
  respond_to do |format| 
    format.html # index.html.erb 
    format.xml { render :xml => @users }
  end
end

# becomes

def index 
  @users = User.all 
  respond_with(@users)
end


# and
def create 
  @user = User.new(params[:user])
  respond_to do |format| 
  if @user.save
    format.html { redirect_to(@user, :notice => 'User was successfully created.') }
    format.xml { render :xml => @user, :status => :created, :location => @user }
  else
    format.html { render :action => "new" } 
    format.xml { render :xml => @user.errors, :status => :unprocessable_entity }
  end 
end

# becomes
def create 
  @user = User.new(params[:user]) 
  flash[:notice] = 'User was successfully created.' if @user.save 
  respond_with(@user)
end

In this chapter we also learn how to replace the default generators with our own customized generators.

Chapter 7, Translatable app with I18n and Redis

In the final chapter José shows us how the I18N backed system works. He does this by creating a backend the uses Redis instead of the default YAML files. He also developes a simple Sinatra application that he hooks into the Rails routing with the mount method.

# Mount the Sinatra Translator::App at the /translator path.
mount Translator::App, :at => "/translator"

Rack applications normally get the full path inside ENV['PATH_INFO'], but when Rails mounts an application it removes the prefix before it sends it on /translator/en/pt/ becomes /en/pt/. The additional path is sent on in ENV['SCRIPT_NAME'] = '/translator'.

We also get a brief overview of Devise, the state-of-the-art authentication solution written by José himself.

Conclusion

One last thing, the book I have reviewed is a beta book, but the quality of it is higher than most published books I have seen. Congratulations to José and to the pragmatic bookshelf! There is not much more to say, get the book.

No comments:

Post a Comment