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 returnsself
.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é 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é :) # 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