Skip to content
May 6, 2011

Ruby 101: method_missing gotchas

Forgetting ‘super’ with ‘method_missing’

method_missing is a hallmark of Ruby metaprogramming. It is one of those coding techniques that you need to master if you want to move from white belt to black belt. Aside from that, it is also fun to use.

  class RadioActive
    def initialize(path)
      ...
    end

    def to_format(format)
      ...
    end
  end
  
  
  d = RadioActive.new('/path/to/uranium')
  d.to_format('xml')

It works but you want explicit methods for each format. So you tap into your Ruby skills and implement your own method_missing.

  class RadioActive
    def method_missing(name, *args)
      if name.to_s =~ /^to_(\w+)$/
        to_format($1)
      end
    end
  end
  
  
  d = RadioActive.new('/path/to/uranium')
  d.to_xml            # WORKS
  d.to_format('xml')  # WORKS
  d.undefined_method  # FAILS

Unfortunately, the last call to ‘undefined_method’ fails. Actually, you would not know it fails because Ruby will not fire any exception. In case there is an undefined method, let us see how Ruby handles it.

  >> s = 'uranium'
   => "uranium" 
  >> s.to_xml
  NoMethodError: undefined method `to_xml' for "uranium":String

There you go. But there is no need to raise the ‘NoMethodError’ in your code. Instead, simply call ‘super’ if you are not handling the method. Whether you have your own class or inheriting from another, do not forget to call ‘super’ with your ‘method_missing’

  class RadioActive
    def method_missing(name, *args)
      if name.to_s =~ /^to_(\w+)$/
        to_format($1)
      else
        super
      end
    end
  end
  
  d = RadioActive.new('/path/to/uranium')
  d.undefined_method
  # => in `method_missing': undefined method `undefined_method' for # (NoMethodError)

Calling ‘super’ is not just for ‘missing_method’. You also need to do the same for the other hook methods like ‘const_missing’, ‘append_features’, ‘method_added’.

Forgetting respond_to?

If you modify ‘method_missing’, it will also affect the behavior of ‘respond_to?’ because what you are adding in as methods do not actually exist — they are ghost methods. If you check the list of instance methods for our class, it will only show 2.

  >> RadioActive.instance_methods(false)
  => ["method_missing", "to_format"]
 
  >> d.respond_to?('to_format')
  => true
  >> d.respond_to?('to_xml')
  => false
  

Every time you modify ‘method_missing’, you also need to update ‘respond_to?’

  class RadioActive
    def respond_to?(name)
      !!(name.to_s =~ /^to_/ || super)
    end
  end
  

January 27, 2011

Preventing model explosion via Rails serialization

A great thing about ActiveRecord is you can easily add a new model to your application and play around with it as you progress. However, this power can easily be overused leading to unnecessary overhead in your code.

Consider the case where you have preferences for each user. For example, a user may opt to show or hide his email address, adjust his timezone, or language. One solution is to simply add new columns to the users table that correspond to each preference type. For example, you can have a ‘show_email’, ‘timezone’, ‘locale’ columns in the ‘users’ table, which can make your table become wide as you add more preferences options. Another option is to use a separate ‘preferences’ table.

  class User < ActiveRecord::Base
    has_many :preferences
  end

  class Preferences < ActiveRecord::Base
    belongs_to :user

    # name  - preference name
    # value - preference value
  end
  

Note there is no user interface to add or remove ‘preferences’, i.e. the kinds of preferences are fixed. Of course, in the future you may add a new kind of preference but this kind of work is better done outside of the user interface. Since that is the case, there is no need to represent ‘preferences’ as a separate model.

One better alternative is to use Rails serialization to store the different kinds and user-specific values. The code would look like this:

  class User < ActiveRecord::Base
    serialize :preferences, Hash
  end

  u = User.new
  u.preferences = {:show_email => true, :locale => :en }
  u.save

  # somewhere in your view using haml
  - if @user.preferences[:show_email]
    = @user.email
  

Using ‘serialize’ results in less code, fewer tables, and less overall complexity. However, with serialization you lose the ability to efficiently search the preferences data. The million-dollar question is do you need to query these preferences? Do you need a finder that returns all users who wants to show their email?

One issue I had with ‘serialize’ is that by using it, I expose the implementation details. In the display example above, it is obvious I had it stored as a Hash. I would rather hide this detail and present the preferences attributes as user attributes instead. I also want default values for every user.

For example:

  u = User.new  # automatically assigns the default preferences
  u.preferences
  => {:show_email => false, :locale => :en}

  u.show_email = true  # I can change it like an attribute via @user.update_attributes(params[:user])
  u.preferences
  => {:show_email => true, :locale => :en}
  

I have created a module to support this. It is not a unique problem so others may have probably released a gem or plugin to do this. (I actually never bothered to search for one.) Nevertheless, it was a good exercise in metaprogramming.

To use my implementation, simply call ‘serializeable’ with the column you want to serialize and the default values.

  class User < ActiveRecord::Base
    serializeable :preferences, :show_email => true, :locale => :en
  end
  

Below is the implementation of ‘serializeable’. The convention is to save it under your ‘lib’ folder and include it in your ‘config/application.rb’ if you are using Rails 3.

  module AttributeSerializer
  
    module ActiveRecordExtensions
      
      module ClassMethods
  
        def serializeable(serialized, serialized_accessors={})  
          serialize serialized, serialized_accessors.class
  
          serialized_attr_accessor serialized, serialized_accessors
          default_serialized_attr serialized,  serialized_accessors
        end
  
        # Creates the accessors
        def serialized_attr_accessor(serialized, accessors)
          accessors.keys.each do |k|
            define_method("#{k}") do
              self[serialized] && self[serialized][k]
            end
  
            define_method("#{k}=") do |value|
              self[serialized][k] = value
            end
          end
        end
  
        # Sets the default value of the serialized field
        def default_serialized_attr(serialized, accessors)
          method_name =  "set_default_#{serialized}"
          after_initialize method_name 
  
          define_method(method_name) do
            self[serialized] = accessors if self[serialized].nil?
          end
        end
  
      end
  
    end
  
  end
  
  class ActiveRecord::Base
    extend AttributeSerializer::ActiveRecordExtensions::ClassMethods
  end
  

ActiveRecord is both easy and powerful. It can also lead to misuse and abuse. Even though you are adding just one model, remember that it is not just the model class itself. You are also adding the database migrations, unit tests, factories, finders, and validations that go along with the model. Next time you have a new requirement, see if serialization can do a better job.

Update: Adam Cuppy converted this code into a Rails plugin while Jay added dynamic finder methods. I also moved this into a gem I called ‘fancy_serializer‘.

January 3, 2011

Ruby 101: Improving your code by defining methods dynamically

Let’s say you have a user and you want to check its role.


  class User
    attr_accessor :role
  end

  u = User.new
  u.role = 'admin'

  # somewhere in your code you check the role

  if u.role == 'admin'
    puts 'admin'
  elsif u.role == 'moderator'
    puts 'moderator'
  elsif u.role == 'guest'
    puts 'guest'
  end

Using a string value is bad code and you can improve this by using constants instead. But still, this is bad code becauses it exposes implementation details of your User class.

For our first improvement, we define methods that check the user’s role and hide the implementation of the role checking inside the User class.


  class User

    attr_accessor :role

    def is_admin?
      self.role == 'admin'
    end

    def is_moderator?
      self.role == 'moderator'
    end

    def is_guest?
      self.role == 'guest'
    end

  end

  u = User.new
  u.role = 'guest'

  if u.is_admin?
    puts 'admin'
  elsif u.is_moderator?
    puts 'moderator'
  elsif u.is_guest?
    puts 'guest'
  end

Our first improvement is definitely better than the original but there are duplicate code in the role checking. You can eliminate the duplicate code by delegating the role checking to a single method.

  class User

    attr_accessor :role

    def is_admin?
      is_role? 'admin'
    end

    def is_moderator?
      is_role? 'moderator'
    end

    def is_guest?
      is_role? 'guest'
    end

  protected

    def is_role?(name)
      self.role == name
    end

  end

Our second improvement is a classic refactoring technique and common in any modern programming language. In other words, there is nothing “Ruby” about it. Before you get bored, I will now show the Ruby version.

The Ruby version uses ‘define_method()’ to further eliminate duplicate code.


  class User

    attr_accessor :role

    def self.has_role(name)
      define_method("is_#{name}?") do
        self.role == "#{name}"
      end
    end

    has_role :admin
    has_role :moderator
    has_role :guest

  end

By using ‘define_method()’, we were able to add instance methods to our class User. You can check the new instance methods via irb.


  ruby-1.9.2-p0 > User.instance_methods.grep /^is/
  => [:is_admin?, :is_moderator?, :is_guest?, :is_a?] 

Note that ‘has_role()’ is just another method and as such you can modify it to accept several parameters, an array, or other class. For example, we can make ‘has_role’ accept a list of roles.

  class User

    attr_accessor :role

    def self.has_roles(*names)
      names.each do |name|
        define_method("is_#{name}?") do
          self.role == "#{name}"
        end
      end
    end

    has_roles :admin, :moderator, :guest

  end
October 6, 2010

How to use OpenAmplify with Ruby

The OpenAmplify API reads text you supply and returns linguistic data explaining and classifying the content. What you do with that analysis is, in the fine tradition of APIs and mashups, up to you. Some possibilities might include pairing ads with articles, creating rich tag-clouds, or monitoring the tone of forum threads.

I created a ruby gem to simplify the use of the OpenAmplify API. It’s still in the early stages but should be enough to get you started.

Output Format

In case you need a different format, OpenAmplify supports XML, JSON, RDF, CSV. It can also return the result as a fancy HTML page.

The source code is available in github: http://github.com/gregmoreno/openamplify

September 28, 2010

Using RabbitMQ and AMQP with Ruby

AMQP stands for Advanced Message Queuing Protocol. RabbitMQ is a server that implements the protocol. RabbitMQ is available on Linux, OSX, and Windows. Installation instructions for RabbitMQ are available at http://www.rabbitmq.com/install.html

I will be running RabbitMQ on Ubuntu 9.10. First, let’s install the server:

Next, we install the amqp gem by Aman Gupta. The gem can also be found at http://github.com/tmm1/amqp

Let’s now build a simple publisher code. Note the difference in the gem’s name and the file you need to use the gem.

All queues are created automatically the first time it is accessed. Make sure this is the same queue our consumers will use.

Next, the consumer code:

The ‘subscribe’ method registers with the queue telling it to call the block when a message has arrived. Alternatively, you can use ‘pop’ but this would constantly poll the server for new messages making unnecessary calls even if the queue is empty.

If you want to run AMQP on several machines, just specify the location of the broker in your publisher and consumer code:

You only need to run 1 rabbitmq server, which in my case is on Ubuntu. When you try the code in other machines, you only need the amqp gem installed.

I highly recommend Distributed Programming with Ruby by Mark Bates if you’re interested in distributed programming.

September 1, 2010

Deploy a Rails 3, Sqlite3 application in Tomcat using JRuby

and have a Ruby version running side-by-side.

A few months ago I got interested in JRuby while researching for text mining algorithms. I found some gems but they are either unmaintained or inadequate while the mature libraries I found were written in Java. No problem! JRuby to the rescue. Thank God.

Next stop, I decided to take Rails 3 and JRuby for a spin. Incidentally, I will be on a 3-city Rails tour in the Philippines this September and since there are many Filipino Java developers, they might find it interesting to see their favorite Java platform works nicely with Ruby on Rails.

Setup

I will be using the following for this tutorial:

java 1.6 + JDK
tomcat 7.0.2
rvm 1.0.1
jruby 1.5.0
ruby 1.9.2p0

Further below, I outline how to install these software. First, let’s see my current environment.

  $ more /etc/issue
  Ubuntu 9.10 \n \l
  
  $ java -version
  java version &quot;1.6.0_20&quot;
  Java(TM) SE Runtime Environment (build 1.6.0_20-b02)
  Java HotSpot(TM) Server VM (build 16.3-b01, mixed mode)
  
  $ rvm -v
  rvm 1.0.1 by Wayne E. Seguin (wayneeseguin@gmail.com) [http://rvm.beginrescueend.com/]
  
  $ jruby -v
  jruby 1.5.0 (ruby 1.8.7 patchlevel 249) (2010-05-12 6769999) (Java HotSpot(TM) Client VM 1.6.0_20) [i386-java]
  
  $ TOMCAT/bin/version.sh 
  Using CATALINA_BASE:   /usr/local/apache-tomcat-7.0.2
  Using CATALINA_HOME:   /usr/local/apache-tomcat-7.0.2
  Using CATALINA_TMPDIR: /usr/local/apache-tomcat-7.0.2/temp
  Using JRE_HOME:        /usr
  Using CLASSPATH:       /usr/local/apache-tomcat-7.0.2/bin/bootstrap.jar:/usr/local/apache-tomcat-7.0.2/bin/tomcat-juli.jar
  Server version: Apache Tomcat/7.0.2
  Server built:   Aug 4 2010 12:23:47
  Server number:  7.0.2.0
  OS Name:        Linux
  OS Version:     2.6.31-22-generic
  Architecture:   i386
  JVM Version:    1.6.0_20-b02
  JVM Vendor:     Sun Microsystems Inc.
  
  $ ruby -v
  ruby 1.9.2p0 (2010-08-18 revision 29036) [i686-linux]
  
  # install jdk and tomcat
  
  $ aptitude install curl sun-java6-bin sun-java6-jre sun-java6-jdk
  $ wget  http://apache.mobiles5.com/tomcat/tomcat-7/v7.0.2-beta/bin/apache-tomcat-7.0.2.tar.gz
  $ tar zxvf apache-tomcat-7.0.2.tar.gz
  $ mv apache-tomcat-7.0.2 /usr/local
  

Of course, these assume you want to use 7.0.2 and you want it installed at your /usr/local.

Install JRuby, Rails 3

I assume you already have rvm installed. If not, I highly recommend that you do. I can’t imagine a Ruby developer not using rvm :)

  $ rvm install jruby
  $ rvm jruby
  $ rvm gemset create railsjam
  $ rvm jruby@railsjam
  $ gem install rails

Try a sample app

I’ve created sample app for the RailsJam tour. This have several functionalities already and better than creating a Rails app from scratch.

$ git clone git://github.com/gregmoreno/railsjam.git

Update the Gemfile

You need a separate set of gems to make your Rails 3 application work with JRuby. For learning purposes, I want my Rails 3 application to work other than JRuby. To accomplish that, we need to specify what gems are needed solely by JRuby.

  source 'http://rubygems.org'
  
  gem 'rails', '3.0.0'
  
  if defined?(JRUBY_VERSION)
    gem 'jdbc-sqlite3'
    gem 'activerecord-jdbc-adapter'
    gem 'activerecord-jdbcsqlite3-adapter'
    gem 'jruby-openssl'
    gem 'jruby-rack'
    gem 'warbler'
  else
    gem 'sqlite3-ruby', :require => 'sqlite3'
  end

(A copy of this Gemfile is available at the ‘jruby’ folder of the railsjam application.)

Now, it’s time to intall the gems. You must delete ‘Gemfile.lock’. Otherwise, bundle picks up wrong version of jdbc

  $ rm Gemfile.lock  
  $ jruby -S bundle install

Prepare the database.

The first time I worked on this tutorial, I needed to specify the jdbcsqlite3 as the database adapter. However, when I tried the tutorial on the same machine with a fresh gemset, it worked pretty well with just ‘sqlite3’. Just to be sure, I modified ‘database.yml’ to check for JRuby.

  development:
    adapter: <%= defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3' %>
    database: db/development.sqlite3
    pool: 5
    timeout: 5000
  
  production:
    adapter: <%= defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3' %>
    database: /home/greg/dev/railsjam/db/development.sqlite3 
    pool: 5
    timeout: 5000

When you deploy to Tomcat, it will be on ‘production’ mode by default. Since sqlite3 is file based and for simplicity, I used the same development database.

Now, do the migration.

  $ jruby -S rake db:migrate

Deploy to Tomcat

We use ‘warble’ which is an excellent tool for packaging your Rails application. It packages everything you need to run your Rails application inside a Java container.

  $ warble
  $ cp railsjam.war  $TOMCAT/webapps
  
  # start Tomcat
  # assuming you arein $TOMCAT dir
  $ sudo ./startup.sh

Check your Rails 3 application

  # You should see the famous Rails welcome
  localhost:3000/railsjam
  
  # Play around with your application
  localhost:3000/railsjam/users

Deploy Rails 3 using Ruby 1.9.2

Without shutting down your JRuby and Tomcat version, let’s try to run our app using Ruby 1.9.2

  # In a new console
  $ rvm 1.9.2
  $ rvm gemset create railsjam
  $ rvm 1.9.2@railsjam
  $ gem install rails
  
  # Assuming you are in the ‘railsjam’ folder
  # This will install sqlite3-ruby gem
  $ bundle install
  
  $ rails server
  
  Now, go play with your Rails 3 applications
  
  # jruby + tomcat
  http://localhost:8080/railsjam/users
  
  # ruby 1.9.2
  http://localhost:3000/users

In case you encountered some problems, here are some ways to solve them. If your problem is not listed here, you can email me. I only accept Paypal :)

JRuby does not support native extensions

You did not update the Gemfile to use the jdbc version of sqlite3. You will encounter this error when you install the gems.

   
  $ bundle install
  ....
  Installing sqlite3-ruby (1.3.1) with native extensions /home/greg/.rvm/rubies/jruby-1.5.2/lib/ruby/site_ruby/1.8/rubygems/installer.rb:482:in `build_extensions': ERROR: Failed to build gem native extension. (Gem::Installer::ExtensionBuildError)
  
  /home/greg/.rvm/rubies/jruby-1.5.2/bin/jruby extconf.rb 
  WARNING: JRuby does not support native extensions or the `mkmf' library.
           Check http://kenai.com/projects/jruby/pages/Home for alternatives.
  extconf.rb:9: undefined method `dir_config' for main:Object (NoMethodError)

undefined method `attributes_with_quotes’ for class `ActiveRecord::Base’

I first encountered this problem when doing migration.

 
  $ rake db:migrate
  rake aborted!
  undefined method 'attributes_with_quotes' for class 'ActiveRecord::Base'

This is caused by an old version of your jdbc gems. In my case, sometimes bundler installs the old versions:

  Installing activerecord-jdbc-adapter (0.9.2) 
  Installing activerecord-jdbcsqlite3-adapter (0.9.2)

As of this writing, the latest version is 0.9.7

  Installing activerecord-jdbc-adapter-0.9.7-java
  Installing activerecord-jdbcsqlite3-adapter-0.9.7-java

Bundler keeps installing 0.9.2

  
  $ rm Gemfile.lock
  $ jruby -S bundle install

no such file to load — sqlite3

  $ rake db:migrate
  (in /home/greg/dev/projects/jruby/railsjam)
  rake aborted!
  no such file to load -- sqlite3

‘sqlite3′ is the default name of the database adapter but with jruby, it should be ‘jdbcsqlite3′.
But, when I tried ‘sqlite3′ with a fresh gemset and a new machine, it went well.
Anyway, just in case you run into the same problem in the future, add a condition
in your database.yml

  development:
    adapter: <%= defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3' %>
    database: db/development.sqlite3
    pool: 5
    timeout: 5000
  
  production:
    adapter: <%= defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3' %>
    database: /home/greg/dev/railsjam/db/development.sqlite3 
    pool: 5
    timeout: 5000

We’re sorry, but something went wrong.

If you see the famous Rails error message, you need to dig in Tomcat’s log files.

  $ cd /usr/local/apache-tomcat-7.0.2/logs
  $ ls -al localhost*
  
  -rw-r--r-- 1 root root 1181 2010-09-01 00:17 localhost.2010-09-01.log
  -rw-r--r-- 1 root root 1062 2010-09-01 00:18 localhost_access_log.2010-09-01.txt
  
  $ tail -f localhost.2010-09-01.log 

In the log file, you will see the errors like missing database.

  org.jruby.rack.RackInitializationException: The driver encountered an error: java.sql.SQLException: path to '/home/greg/dev/tmp/apache-tomcat-7.0.2/webapps/railsjam/WEB-INF/db/production.sqlite3': '/home/greg/dev/tmp/apache-tomcat-7.0.2/webapps/railsjam/WEB-INF/db' does not exist

August 24, 2010

Rails 3 upgrade part 4: Prototype helpers and Javascript

Rails 3 is embracing the unobtrusive Javascript (or UJS) mantra which is good because it is the right way; at the same time, it is bad because many applications will break when they upgrade to Rails 3. On the other hand, who’s expecting a smooth upgrade anyway :)

In my test application, I used jrails because I am more interested in jQuery than Prototype. But since jrails doesn’t work with Rails 3, I removed it.

When jrails was removed, I received this error:

  undefined method `observe_field' for #<#<Class:0xb6867e58>:0xb6865b6c>

Install Prototype helper plugin

observe_field’ is a Prototype helper and Rails 3 removed the the link between its Javascript helpers and Prototype. The goal in Rails 3 is for developers to use their preferred Javascript library. Also note that remote_#{method} helpers have been removed from Rails and moved to Prototype Legacy Helper plugin . To install this plugin, just do:

  rails plugin install git://github.com/rails/prototype_legacy_helper

Remove jQuery

Once the prototype_legacy_helper is installed, the missing method is gone but observe_field is not triggering. Removing jQuery fixes this problem.

Now what if you want to use jQuery instead of Prototype? It depends how dependent your application is to Prototype. I have not found a jQuery equivalent for Prototype helper plugin yet so that would be an issue like in my case. Based on this jQuery and Rails 3 tutorial, using the jQuery UJS driver looks very easy.

August 13, 2010

Rails 3 upgrade part 3: Code fixes, views, and forms

This is part 3 of my Rails 2 to Rails 3 upgrade experience. Part 1 is about the initial code upgrade and getting the application to boot while part 2 deals with routes. While Part 2 is mainly about routes, getting it work involved changes in other parts of the code which I’ll share this time. So while you are updating your routes, you may need to check this post in between changes.

Update ApplicationController

After regenerating your application with rails (i.e. rails new appname -d dbadapter), your ApplicationController would look like this:

  class ApplicationController < ActionController::Base
    protect_from_forgery
  end

There’s no need to panic because rails:upgrade:backup made a copy of the controller to application_controller.rb.rails2.

If you have a lot of helper modules, you’ll most likely have this code in your Rails 2 ApplicationController:

helper :all

If you encounter a missing method error while monkey clicking your application, you probably forgot to update your Rails 3 ApplicationController.

Update ApplicationHelper

The ApplicationHelper module was also modified by the rails upgrde. So don’t forget to update this, too.

RAILS_* constants are deprecated is not entirely true

When you run rails:upgrade:check, it will list items you need to update including deprecated code. There is no need to change these as the word ‘deprecated’ means but I encountered several “can’t convert nil into String” errors.

  rake rails:upgrade:check
  (in /mnt/hgfs/greg-mini/dev/projects/propsify)
  Deprecated constant(s)
  Constants like RAILS_ENV, RAILS_ROOT, and RAILS_DEFAULT_LOGGER are now deprecated.
  More information: http://litanyagainstfear.com/blog/2010/02/03/the-rails-module/
  
  The culprits: 
    ...

The weird part is some constants are just doing fine. In any case, here are the conversion:

  RAILS_ROOT  -> Rails.root
  RAILS_ENV -> Rails.env
  RAILS_DEFAULT_LOGGER -> Rails.logger

You can also check your environment the Ruby way:

  # before
  if RAILS_ENV == 'production'
    ...
  
  # Rails 3
  if Rails.env.production?

Output strings are automatically escaped

We should all be rejoicing that Rails is now serious about XSS protection except now your pages have become ugly with all those HTML tags. For example the code below will not give you a clickable link.

  - signup = link_to('create one here', signup_path)
  = "If you do not have an account, #{signup}."

To fix this, use the raw() helper.

  = raw "If you do not have an account, #{signup}."

Too bad for me, I got tons of views that were coded like this.

Check for ‘concat’

A popular technique to simplify your view code is to use content blocks. You create a helper that takes a block and wraps it in some HTML tags. A simple implementation would look like this:

  module LayoutHelper
    def main_column(options={}, &amp;block)
      # calls column()
    end
  
    def column(options={}, &amp;block)
      # concat is not needed in Rails 3
      concat content_tag(:div, capture(&amp;block), options)
    end
  end
  
  # in your view
  - main_column do
    = render 'form'

This works fine in Rails 2 but in Rails 3 the block gets outputted twice. concat is the way to output text in a non-output block (i.e. <% %> in erb) but it seems like erb blocks in Rails 3 do not need concat.

Helpers with blocks

Before Rails 3, form_for or fields_for use non-output syntax; it means no equals sign.

  # erb
  <% form_for @offer do |f| %>
    # ...
  <% end %>
  
  # haml
  - form_for @offer do |f|
    # ...

In Rails 3, it should now be written as an output block.

  # erb
  <%= form_for @offer do |f| %>
    # ...
  <% end %>
  
  # haml
  = form_for @offer do |f|
    = f.fields_for :items do |ff|
      # ...

The rule is if the method is expected to return a string, it should use the output syntax. If it just buffering the returned string like content_for, it should NOT have the equals sign.

August 12, 2010

Rails 3 upgrade part 2: Routes

In the previous post, I outlined the steps I took to upgrade and boot a Rails 3 application. This time, I share my experience upgrading the routes file. By the way, I forgot to mention in the last post that I’m using Rails 3 Upgrade Handbook by Jeremy McAnally.

The task rails:upgrade:routes (comes with the rails_upgrade plugin) converts your Rails 2 routes into Rails 3 format. It handles most cases but you may still need to edit the generated routes depending on your setup.

map.root

Below, I show the old route and the generated version.

  # Rails 2
  map.root :controller => 'search'
  
  # Rails 3
  match '/' => 'search#index'

The conversion is correct but since I use the named route ‘root_path’ in my application, I had to change it:

  root :to => 'search#index'

:as, :member, :any, :path_names

  # Rails 2
  map.resources :workspaces, :as => 'b', :member => { :widget => :get } do |workspace|
    # ...
  end
  
  # Rails 3
  resources :workspaces do
    # ...
  end

In Rail 3, :as is for overriding the normal naming for named routes witout affecting the path. For example, the code below will recognize the path ‘/workspaces’ and the named route becomes offices_path.

  resources :workspaces, :as => 'offices'

In Rails 2, :as affects the path. In my example, ‘/b’ routes the request to WorkspacesController. So for Rails 3 to recognize the path ‘/b’, I need to add another route.

  match 'b' => 'workspaces#index'

The rails:upgrade:routes did not convert the following member route and had to be added.

  :member => { :widget => :get } 

The new route becomes:

  resources :workspaces do
    get :widget, :on => :member
  end

In Rails 2, you can use the :any option to define a custom route that responds to any request method.

  # Rails 2
  workspace.resource :twitter_account, :member => { :authorize => :any }, :path_names => { :edit => 'request_authorization' }
  
  # generated by rails:upgrade:routes
  resource :twitter_account do
    member do
      any :authorize
    end
  end

The rails:upgrade:routes converted the :any option. However, when I booted the application, it raised an exception:

  undefined method 'any' for #<ActionDispatch::Routing::Mapper:0xb71b6fcc> (NoMethodError)

To fix this, I replaced the offending line with a match method.

  resource :twitter_account do
    match :authorize, :on => :member
  end

:path_names was also not included in the generated route so has to be added as well.

  resource :twitter_account, :path_names => { :edit => 'request_authorization' } do
    match :authorize, :on => :member
  end

Specifying a different controller

  # Rails 2
  map.resource :settings, :controller => 'users' do |settings|
    settings.resource :twitter_account, :name_prefix => nil, :member => { :authorize => :any }, :path_names => { :edit => 'request_authorization' }
  end
  
  # generated by rake:upgrade:routes
  resource :settings do
    resource :twitter_account do
      member do
        any :authorize
      end
    end
  end

To fix, just specify the controller

  resource :settings, :controller => :users do
    # ...
  end

Undefined named route helper

I encountered this exception while trying the application:

  undefined method 'edit_twitter_account_path'

In Rails 2, this is the route that created this named route:

  map.resource :settings, :controller => 'users' do |settings|
    settings.resource :twitter_account, :name_prefix => nil, :member => { :authorize => :any }, :path_names => { :edit => 'request_authorization' }
  end

This is a bit tricky for me because I cannot remember why I nested it :) Nevertheless, to fix the Rails 3 error, I moved :twitter_account outside of :settings. The correct Rails routes now look like these:

  resource :settings, :controller => :users
  resource :twitter_account, :path_names => { :edit => 'request_authorization' } do
    match :authorize, :on => :member
  end

Custom polymorphic named route helper

A long time ago, I played around with polymorphic paths. In hindsight, that is a waste of time but back then it was fun or should I say a time well wasted. I have a named route helper that takes any object and used like this:

  # in views
  link_to 'invitations', invitations_path(@voteable)
  
  # definition
  module RoutesHelper
    def invitations_path(voteable)
      send("#{voteable.class.name.underscore}_invitations_path", voteable)
    end
  
    def workspace_invitations_path(workspace)
      super(:workspace_id => workspace)
    end
  
    # ...
  end

In Rails 3, my named route helper is not being called. Thus, wrong URL is generated. I know, I know it should have been a simple polymorphic_path call but I still wonder why my method is not called. Moving on, the new ruby is:

  link_to 'invitations', polymorphic_path([@voteable, :invitations])

I cheated a bit here because I want this post to focus on routes. Along the way, I had to update non-route related code to discover the route problems. You can learn more about Rails 3 routes from this RailsGuides page.

There are still more updates to be done and I’ll share them in other posts. Just like your favorite late night infomercial, “Wait! There’s more”.

August 11, 2010

Rails 3 upgrade part 1: Booting the application

It’s time for another Rails upgrade! We all have our share of bad experiences and frustrations every time we upgrade a piece of software. Even for technical people who live and breath on the edge, upgrades are one of these things we try to avoid as much as possible. Still, there is always a sense of excitement in trying something new even if it adds problems to an already stable piece of code.

For a little background, I am upgrading a Rails app several of friends and I have written last year. The code is available at github.

In this post, I share the steps I did to boot the application. This doesn’t mean the upgrade went fine neither the app is ready to go. It only means all the required initialization are OK. In succeeding posts, I share my experiences in upgrading the app to a green state.

First, my environment.

  greg@piccolo:~/dev/projects/propsify3$ rvm info
  ruby-1.8.7-p299@propsify:
  
    system:
      uname:        "Linux piccolo 2.6.31-22-generic #61-Ubuntu SMP Wed Jul 28 01:57:06 UTC 2010 i686 GNU/Linux"
      shell:        "bash"
      version:      "4.0.33(1)-release"
  
    rvm:
      version:      "rvm 0.1.44 by Wayne E. Seguin (wayneeseguin@gmail.com) [http://rvm.beginrescueend.com/]"
  
    ruby:
      interpreter:  "ruby"
      version:      "1.8.7"
      date:         "2010-06-23"
      platform:     "i686-linux"
      patchlevel:   "2010-06-23 patchlevel 299"
      full_version: "ruby 1.8.7 (2010-06-23 patchlevel 299) [i686-linux]"
  
  greg@piccolo:~/dev/projects/propsify3$ script/about
  About your application's environment
  Ruby version              1.8.7 (i686-linux)
  RubyGems version          1.3.7
  Rack version              1.0 bundled
  Rails version             2.3.2
  Active Record version     2.3.2
  Action Pack version       2.3.2
  Active Resource version   2.3.2
  Action Mailer version     2.3.2
  Active Support version    2.3.2
  Application root          /mnt/hgfs/greg-mini/dev/projects/propsify
  Environment               development
  Database adapter          postgresql
  Database schema version   20100113032723
  
  
  greg@piccolo:~/dev/projects/propsify3$ gem list
  
  *** LOCAL GEMS ***
  
  actionmailer (2.3.2)
  actionpack (2.3.2)
  activerecord (2.3.2)
  activeresource (2.3.2)
  activesupport (2.3.2)
  geokit (1.5.0)
  json (1.4.5)
  mime-types (1.16)
  oauth (0.4.1)
  pg (0.9.0)
  rails (2.3.2)
  rake (0.8.7)
  RedCloth (4.2.2)
  twitter_oauth (0.3.2)
  
  greg@piccolo:~/dev/projects/propsify3$ ls vendor/gems/
  authlogic-2.1.3  geokit-1.5.0  haml-2.2.16  macaddr-1.0.0  twitter_oauth-0.3.2  uuid-2.1.0
  
  greg@piccolo:~/dev/projects/propsify3$ ls vendor/plugins/
  acts_as_commentable        geokit-rails     is_taggable   thinking-sphinx      will_paginate
  declarative_authorization  gravatar-plugin  jrails        validates_date_time
  exception_notification     haml             subdomain-fu  vote_fu
  

Step 1: Install rails 3

gem install rails –pre

Step 2: Install the plugin tool

script/plugin install git://github.com/rails/rails_upgrade.git

Step 3: Show upgrade checklist

rake rails:upgrade:check

This task lists the items you should watch out for when doing the upgrade. You don’t need to fix everything right away (some are deprecation notice) but review the checklist nevertheless.

Step 4: Generate the new routes

rake rails:upgrade:routes

This task reads the current config/routes.rb and outputs a Rails 3 version.
Don’t worry, it doesn’t override your routes file. Keep this in a safe place for later use.

IMPORTANT: I actually didn’t realize I did the right thing until after the actual code upgrade. When I tried generating the new routes after the code change, it outputted an empty block. I have no idea if this is unique to my case but just to be sure, generate the routes beforehand and keep a copy.

Step 5: Create Gemfiles

rails:upgrade:gems

Next is to generate the file ‘Gemfile’. In Rails 2, the gems you need are listed in config/environment.rb while in Rails 3 the gems are listed in the Gemfile. Gemfile is used by the program ‘bundler’ to manage the gems required by your application. Unfortunately, this task didn’t include the gems I listed in environment.rb so I have to add it later.

Step 6: Backup your files

rails:upgrade:backup

I hope you are working on another branch (or a copy) but just in case you are not, run this task to make copies of the files that will be affected during the upgrade.

Now comes the juicy part.

Step 7: Generate the Rails 3 app on top of your Rails 2 app

rails new propsify3 -d postgresql

Run this command in your app’s parent folder. In my case, my app’s name and pathname is ‘propsify3′ and I am using postgresql as my database. This command created and replaced a bunch of files. Since you’ve backed-up everything, there’s nothing to worry.

Step 8: Move code from environment.rb to application.rb

Your new config/environment.rb file looks like it went through a rigorous diet. You can leave this file for now. What is important now is you move the initializer code from your config/environment.rb.rails2 to config/application.rb. These are the config.* lines except the config.gem which goes to Gemfile.

Step 9: Convert the new routes

You can still use the existing routes until 3.1 but since there’s a tool to help you migrate, I suggest doing it. At this point, when I tried the rails:upgrade:routes, no routes were generated. So make sure you generate the routes before Step 7.

Step 10: Delete new_rails_defaults.rb

rm config/initializers/new_rails_defaults

Step 11: Upgrade the plugins and gems

Many plugins are now available as gems. Check your plugins and gems at http://railsplugins.org. In my case, the following plugins were converted to gems:

acts_as_commentable
declarative_authorization
haml
will_paginate
thinking-sphinx

Unfortunately, the plugins below are not yet ready for Rails 3. I removed them for now and all code that references them.

jrails
subdomain-fu
vote_fu

IMPORTANT: In your Gemfile, make sure you check specify the right version that is compatible with Rails 3. Some gems are still in the pre-release version and will not be downloaded if you don’t specify a version in your Gemfile. For example, this is a snippet from my Gemfile:

  gem 'pg'
  gem 'acts_as_commentable'
  gem 'declarative_authorization'
  gem 'haml'
  gem 'thinking-sphinx', '2.0.0.rc1', :require => 'thinking_sphinx'
  gem 'will_paginate', '3.0.pre2'
  gem 'uuid'
  gem 'geokit'

Step 12: Update initialization code

After step 10 you are good to go, if you’re lucky. In my case, I had to remove some patches and change code to boot the application.

ActiveSupport::CoreExtensions::Date::Conversions::DATE_FORMATS.merge!(date_time_formats)

This fails in Rails 3 because core extensions have been moved out of their modules and are now included in classes they extend. For example, to fix the date format problem do:

Date::DATE_FORMATS.merge!(date_time_formats)

Step 13: Boot the app

rails server

Yay! If you are wondering what happened to script/server command, Rails went the “Merb way” and consolidated the script/* commands into the rails script.

By now, you should see the famous Rails’ “Welcome aboard” message in your browser.

Step 14: Remove public/index.html

Now, you can try if your application is working.

There are still more work to do like moving to the ActiveRecord/ActiveRelation API and removing the deprecation notices. Before moving on, I still need to fix the problems in my routes and unsupported gems which I will tackle in my next post.

Follow

Get every new post delivered to your Inbox.