Pat Shaughnessy

Ribadesella, Spain

Getting started with Ruby metaprogramming

February 20, 2010 · 2 comments

The Rails auto_complete plugin was my first exposure to Ruby metaprogramming. It’s code was simple enough for a Rails beginner like me to understand, but also just complex enough for me to learn something new. Specifically, I ran into metaprogramming when I took a close look at the “auto_complete_for” method and tried to figure out how it worked. I won’t spend any time here explaining what the auto_complete plugin does or what it’s used for, beyond to say that if you add this line to one of your controllers:

class CategoriesController < ApplicationController

  auto_complete_for :category, :name

  etc…


… a method called “auto_complete_for_category_name” will be automatically generated in that controller that will return a list of category records that have a name matching a given search query. This is very cool, and is a typical example of Ruby on Rails magic: you add one line to a class in your application and suddenly an entire feature or behavior is added, customized to the data and objects in your app!

This sort of thing is really what makes Ruby on Rails so amazing… but how does it work? Let’s take a look at the implementation of auto_complete_for method:

def auto_complete_for(object, method, options = {})
  define_method("auto_complete_for_#{object}_#{method}") do
    find_options = { 
      :conditions => [
                       "LOWER(#{method}) LIKE ?",
                       '%' + params[object][method].downcase + '%'
                     ],
      :order => "#{method} ASC",
      :limit => 10 }.merge!(options)

    @items = object.to_s.camelize.constantize.find(:all, find_options)

    render :inline => "<%= auto_complete_result @items, '#{method}' %>"
  end
end

You can find this code in the lib/auto_complete.rb file inside the rails/auto_complete repository on github. So what the heck does all of this mean? Let’s take a step-by-step look at this code, and see if we can figure it out.

To get started, let’s use the category/name example I used in my last article, and also that Ryan Bates used in his Auto-Complete Association screencast on the auto_complete plugin:

auto_complete_for :category, :name


Here we are passing “:category” into auto_complete_for as the value for “object,” and “:name” as the value for “method.” Now let’s repeat the auto_complete_for code, but substitute object with :category:

def auto_complete_for(:category, method, options = {})
  define_method("auto_complete_for_#{:category}_#{method}") do
    find_options = { 
      :conditions => [
                       "LOWER(#{method}) LIKE ?",
                       '%' + params[:category][method].downcase + '%'
                     ],
      :order => "#{method} ASC",
      :limit => 10 }.merge!(options)

    @items = :category.to_s.camelize.constantize.find(:all, find_options)

    render :inline => "<%= auto_complete_result @items, '#{method}' %>"
  end
end

In the code snippet above I’ve highlighted the places where the symbol :category appears. You can see that it’s used in a few different places, but the most important line is near the bottom: @items = :category.to_s.camelize.constantize… etc. Let’s evaluate each of the method calls on this one line, one at a time. The first call is “:category.to_s”. The “to_s” method name means “to string” and will convert the target object (the object you call to_s on) to a string. This means that the :category symbol will be converted to a string. So now let’s display the string ‘category’ in place and see what we are left with:

def auto_complete_for(:category, method, options = {})
  define_method("auto_complete_for_#{:category}_#{method}") do
    find_options = { 
      :conditions => [
                       "LOWER(#{method}) LIKE ?",
                       '%' + params[:category][method].downcase + '%'
                     ],
      :order => "#{method} ASC",
      :limit => 10 }.merge!(options)

    @items = 'category'.camelize.constantize.find(:all, find_options)

    render :inline => "<%= auto_complete_result @items, '#{method}' %>"
  end
end

You can see ‘category’ highlighted above where I’ve evaluated the to_s method. Now the next method call is “camelize” – what does this mean? The camelize method is one of a series of functions that Rails provides in the “ActiveSupport” gem, one of the components of the Rails framework. It converts the given string to camel case, for example “office_code” to “OfficeCode.” Since Ruby class names are written using camel case, this is often very useful for obtaining a class name from a string. In our example, the string “category” is converted into “Category” with an upper case “C” …

def auto_complete_for(:category, method, options = {})
  define_method("auto_complete_for_#{:category}_#{method}") do
    find_options = { 
      :conditions => [
                       "LOWER(#{method}) LIKE ?",
                       '%' + params[:category][method].downcase + '%'
                     ],
      :order => "#{method} ASC",
      :limit => 10 }.merge!(options)

    @items = 'Category'.constantize.find(:all, find_options)

    render :inline => "<%= auto_complete_result @items, '#{method}' %>"
  end
end

Now let’s take a look at the next method call on that same line of code: “constantize.” This converts a string into an actual Ruby constant, and returns an error if that constant doesn’t exist. In our example, the string “Category” is converted into the Ruby class Category:

def auto_complete_for(:category, method, options = {})
  define_method("auto_complete_for_#{:category}_#{method}") do
    find_options = { 
      :conditions => [
                       "LOWER(#{method}) LIKE ?",
                       '%' + params[:category][method].downcase + '%'
                     ],
      :order => "#{method} ASC",
      :limit => 10 }.merge!(options)

    @items = Category.find(:all, find_options)

    render :inline => "<%= auto_complete_result @items, '#{method}' %>"
  end
end

Now we can see that the complex line above evaluates to a simple ActiveRecord find call. The power of Ruby metaprogramming has enabled us to write a single helper function “auto_complete_for” that takes any symbol or string as an argument (e.g. :category), and performs a SQL query on the corresponding ActiveRecord class. The amazing part of this for me is how flexible and easy to use Ruby is: helper methods like camelize and constantize make it very easy to extract common bits of code you might write over and over again in your app, and generalize them into a single method that will apply to any target class. This would be possible in any programming language but it’s amazing just how easy it is to do with Ruby.

Let’s continue to simplify the auto_complete_for code by substituting a value for the “method” parameter – in our example method will become the symbol “:name” :

def auto_complete_for(:category, :name, options = {})
  define_method("auto_complete_for_#{:category}_#{:name}") do
    find_options = { 
      :conditions => [
                       "LOWER(#{:name}) LIKE ?",
                       '%' + params[:category][:name].downcase + '%'
                     ],
      :order => "#{:name} ASC",
      :limit => 10 }.merge!(options)

    @items = Category.find(:all, find_options)

    render :inline => "<%= auto_complete_result @items, '#{:name}' %>"
  end
end

Again you can see that the :name symbol is used in a few different places. Most commonly here the symbol is inserted in a string using this syntax: “#{:name}”. This is the standard Ruby #{} string interpolation operator, which is also implicitly converting the symbol into a string before inserting it into the surrounding string value, just as if we called to_s as above. So let’s replace “#{:name}” and “#{:category}” with the strings “name” and “category,” and then also insert them into the surrounding string values:

def auto_complete_for(:category, :name, options = {})
  define_method("auto_complete_for_category_name") do
    find_options = { 
      :conditions => [
                       "LOWER(name) LIKE ?",
                       '%' + params[:category][:name].downcase + '%'
                     ],
      :order => "name ASC",
      :limit => 10 }.merge!(options)

    @items = Category.find(:all, find_options)

    render :inline => "<%= auto_complete_result @items, 'name' %>"
  end
end

And now let’s replace the last parameter to auto_complete_for “options” with it’s default value since we aren’t providing that in our auto_complete_for :category, :name call:

def auto_complete_for(:category, :name, {})
  define_me thod("auto_complete_for_category_name") do
    find_options = { 
      :conditions => [
                       "LOWER(name) LIKE ?",
                       '%' + params[:category][:name].downcase + '%'
                     ],
      :order => "name ASC",
      :limit => 10 }.merge!({})

    @items = Category.find(:all, find_options)

    render :inline => "<%= auto_complete_result @items, 'name' %>"
  end
end

You can see that options was used in only one place: …merge!(options). Using merge with hashes is another extremely common technique in Ruby coding; it adds the key/value pairs from the hash you provide as a parameter to the hash you call merge on. Merge! is a variation on this which directly modifies the target object, as opposed to only returning the merged hash. Merge is very useful for metaprogramming because, as in this example, it makes it easy to allow the user/client code of a method to customize a hash of options that is passed into some other function. Here merge was used to allow the caller of auto_complete_for to pass in additional find options to the Category.find call. In our case since we didn’t pass in a value for options, it is set to {} and then has no effect on the find_options hash. So let’s just remove the call to merge!({}), which is a NOP anyway:

def auto_complete_for(:category, :name, {})
  define_method("auto_complete_for_category_name") do
    find_options = { 
      :conditions => [
                       "LOWER(name) LIKE ?",
                       '%' + params[:category][:name].downcase + '%'
                     ],
      :order => "name ASC",
      :limit => 10 }

    @items = Category.find(:all, find_options)

    render :inline => "<%= auto_complete_result @items, 'name' %>"
  end
end

Now the code is looking simpler and simpler. Let’s finish this example by evaluating the “define_method” call at the top. As you might guess, define_method is how you can dynamically create a new method in a class in Ruby. In auto_complete_for it’s used to add a new method to the controller in which you included the call to auto_complete_for, called “auto_complete_for_category_name.” In other words, adding the auto_complete_for :category, :name line to the CategoriesController was equivalent to adding this method definition to the class:

def auto_complete_for_category_name
  find_options = { 
    :conditions => [
                     "LOWER(name) LIKE ?",
                     '%' + params[:category][:name].downcase + '%'
                   ],
    :order => "name ASC",
    :limit => 10 }

  @items = Category.find(:all, find_options)

  render :inline => "<%= auto_complete_result @items, 'name' %>"
end

Now we’ve seen the real value of metaprogramming: the author of the auto_complete plugin was able to provide a simple helper method, auto_complete_for, which dynamically added this fairly complex method to your controller. The beauty and power of Ruby and Rails is just how easy this was to do: the generated function is tailored to use the Category model just as if you had written it yourself. When I first came across this code I was impressed not only by the power that this sort of metaprogramming provided, but also by how easy it was to do this using Ruby on Rails. Helper modules and methods, such as constantize in ActiveSupport, make it very, very easy to convert from symbols to strings to constants and back again… which is exactly what you need to do to write a general method that can be dynamically generated in this way.

Last week I asserted that calling auto_complete_for in your controller like this was equivalent to this code from Ryan Bates’ Auto-Complete Association screencast:

class CategoriesController < ApplicationController

  def index
    @categories =
      Category.find(:all, :conditions => ['name LIKE ?', "%#{params[:search]}%"])
  end

etc...


Now you can see how this is true: the code Ryan wrote was a simple call to Category.find, with conditions that search for a name like the name provided in the “search” parameter. Looking at the simplified auto_complete_for_category_name method above, you can see that it calls Category.find in the same way. However, the auto_complete_for version is a bit different in that it:

  • Looks for a parameter called “category[name]” instead of “search,” and
  • Converts it to lower case, and
  • Passes a couple of other SQL options into Category.find: order by name ascending, and limit 10, and
  • Makes a call to render :inline to return the result to the browser without the need for a normal view code file.

Auto_complete_for does essentially the same thing that Ryan explained in his screen cast, but the elegance of Ruby metaprogramming allows the users of the auto_complete plugin to implement this search feature without writing any code at all.

2 comments Tags:·

Creating associations to existing data part 2: belongs_to with auto_complete

February 13, 2010 · 3 comments

In my last post I started a series on how to write Rails forms that associate a new record with existing data. This sort of requirement comes up for me over and over again at my day job, and so I decided to support scaffolding for these forms in View Mapper.

Today I’ll continue by showing how to use the auto_complete plugin to select an existing record – exactly what Ryan Bates discussed in his screen cast Auto-Complete Association. Using the same Category/Product example, this form would allow the user to create a new product record, and associate it with an existing category tag:

To create scaffolding like this in your app with View Mapper, just run this command:

script/generate scaffold_for_view product name:string bar_code:integer
                --view belongs_to_auto_complete:category

View Mapper will validate you have a line “has_many :products” in your category model, that you have a category model to begin with, and also that the auto_complete plugin is installed before proceeding to generate this form. Also, View Mapper assumes the parent model, “Category” in this example, has a “name” column and will use that value to identify each category in the auto complete list. You can indicate to use a different parent model field instead with this syntax:

script/generate scaffold_for_view product name:string bar_code:integer
                --view belongs_to_auto_complete:category[display_name]

For more details on View Mapper, see the example below where I create a sample app from scratch.

Code review: model

Since Ryan explains auto complete association so well in his screen cast, I won’t repeat all of that information here. Instead, let’s take a look at the code View Mapper generates and compare it to what Ryan showed. First, in the product model Ryan has a “category_name” virtual attribute:

def category_name
  category.name if category
end
def category_name=(name)
  self.category = Category.find_or_create_by_name(name) unless name.blank?
end

This allows the view to display the category for each product easily, and also supports creating new categories on the fly when you submit a new product. The View Mapper scaffolding is a bit simpler and uses “find_by_name” instead of “find_or_create_by_name” since it assumes the category records already exist. Also, Ryan’s code uses “unless name.blank?” to avoid creating empty categories, while the View Mapper scaffolding assumes a blank category name indicates a product without a category, and allows the user to clear the category when editing an existing product. Either approach can make sense depending on the business requirements of your application. Here’s the View Mapper model code:

class Product < ActiveRecord::Base
  belongs_to :category
  def category_name
    category.name if category
  end
  def category_name=(name)
    self.category = Category.find_by_name(name)
  end
end

Code review: controller

In the controller code, the View Mapper scaffolding differs from Ryan’s solution more dramatically. To return the list of matching categories to the auto_complete plugin, Ryan adds this code to the categories controller to query the category records that have a name field that match a given search parameter:

def index
  @categories =
    Category.find(:all, :conditions => ['name LIKE ?', "%#{params[:search]}%"])
end

This makes a lot of sense: the categories controller should be used to generate a list of categories. However, for View Mapper I chose to use the products controller instead since the scaffolding generator already generates that code file, and to avoid the need for creating or modifying the categories controller also. View Mapper just adds this one line to the products controller to return the list of category names:

auto_complete_for :category, :name

This simple call actually achieves exactly the same thing as the Category.find call above. In my next post, I’ll take a look at the Ruby metaprogramming used by auto_complete_for and show how it automatically generates a method that executes the same SQL query.

Code review: view

Finally in the view we see a call to “text_field_with_auto_complete” to use the Prototype javascript library’s auto_complete feature. Here’s Ryan’s view code from the screen cast:

<%= text_field_with_auto_complete :product,
                                  :category_name,
                                  { :size => 15 },
                                  { :url => formatted_categories_path(:js),
                                    :method => :get,
                                    :param_name => 'search' }
%>

“:url => formatted_categories_path(:js)” calls the categories controller code above when the user starts to type in the text field, and the “:param_name =>‘search’” option passes the user’s text in as the search parameter. Ryan’s solution also uses a view file called “index.js.erb” to return the list of completion options in the proper format – this is called by the index action when the categories controller receives the “/categories.js” request:

<%= auto_complete_result @categories, :name %>

By contrast, View Mapper’s call to text_field_with_auto_complete looks like this:

<%= text_field_with_auto_complete :product,
                                  :category_name,
                                  {},
                                  { :method => :get,
                                    :url => '/auto_complete_for_category_name',
                                    :param_name => 'category[name]' }
%>

This is very similar, but uses “category[name]” as the search parameter and sets the AJAX url to “auto_complete_for_office_code”, since this is what “auto_complete_for” expects.

Ryan’s approach is more elegant since it follows the REST model for the Ajax URL and controller code: the categories controller is used to handle category related requests, and its index action is used to return the list of category values. The scaffolding code View Mapper generates uses the auto_complete plugin the way it was originally intended with the “auto_complete_for” function, but is a bit ugly in that the products controller returns the category values, and uses a custom action method name instead of the normal “index” action. There’s no need for the index.js.erb file since auto_complete_for renders the response inline.

The advantage of the View Mapper approach is that there’s no need for the categories controller at all, and also you don’t need to code the “index” action or the index.js.erb view file yourself. If you plan to have a categories controller in your application anyway, you might want to change the text_field_with_auto_complete call to use that controller instead.

Detailed example

To make sure you can get a working example on your computer, let’s run through a step by step example:

$ rails sample_app
      create  
      create  app/controllers
      create  app/helpers
      create  app/models
      create  app/views/layouts
etc...

First, let’s create a model to represent our existing data called “Office” that will have two attributes: “display_name” and “code:”

$ cd sample_app/
$ ./script/generate model office code:string display_name:string
      exists  app/models/
      exists  test/unit/
      exists  test/fixtures/
      create  app/models/office.rb
      create  test/unit/office_test.rb
      create  test/fixtures/offices.yml
      create  db/migrate
      create  db/migrate/20100212193446_create_offices.rb
$ rake db:migrate
(in /Users/pat/rails-apps/sample_app)
==  CreateOffices: migrating ==================================================
-- create_table(:offices)
   -> 0.0031s
==  CreateOffices: migrated (0.0034s) =========================================

And now let’s create a few sample office records:

$ ./script/console 
Loading development environment (Rails 2.3.5)
>> Office.create :display_name => 'Boston', :code => 'BOS'
>> Office.create :display_name => 'Boise', :code => 'BOI'
>> Office.create :display_name => 'Barcelona', :code => 'BAR'
>> exit

Now you can install View Mapper – you’ll need version 0.3.3 for the “belongs_to_auto_complete” view:

$ gem sources -a http://gemcutter.org
http://gemcutter.org added to sources
$ sudo gem install view_mapper
Successfully installed view_mapper-0.3.3
1 gem installed
Installing ri documentation for view_mapper-0.3.3...
Installing RDoc documentation for view_mapper-0.3.3...

And now we can just run View Mapper’s “scaffold_for_view” generator to create the scaffolding code. Let’s try creating a new model called “Employee” that will belong to one of the existing offices:

$ ./script/generate scaffold_for_view employee name:string
                    --view belongs_to_auto_complete:office
       error  The auto_complete plugin does not appear to be installed.
$ ./script/plugin install git://github.com/rails/auto_complete.git
Initialized empty Git repository in /Users/pat/rails-apps/sample_app/vendor/plugins/auto_complete/.git/
remote: Counting objects: 13, done.
remote: Compressing objects: 100% (12/12), done.
remote: Total 13 (delta 2), reused 0 (delta 0)
Unpacking objects: 100% (13/13), done.
From git://github.com/rails/auto_complete
 * branch            HEAD       -> FETCH_HEAD

Trying again:

$ ./script/generate scaffold_for_view employee name:string
                    --view belongs_to_auto_complete:office
     warning  Model Office does not contain a has_many association for Employee.

Editing app/models/office.rb:

1 class Office < ActiveRecord::Base
2   has_many :employees
3 end

Trying a third time:

$ ./script/generate scaffold_for_view employee name:string
                    --view belongs_to_auto_complete:office
     warning  Model Office does not have a name attribute.

This time we get a warning that our existing model doesn’t have a “name” attribute (we chose “display_name” instead). To make this a bit more interesting, let’s use the “code” attribute for the auto_complete options. You can specify this to View Mapper with this syntax:

$ ./script/generate scaffold_for_view employee name:string
                    --view belongs_to_auto_complete:office[code]
      exists  app/models/
      exists  app/controllers/
      exists  app/helpers/
      create  app/views/employees
      exists  app/views/layouts/
      exists  test/functional
etc...

Now we just run rake db:migrate again, start our server and we’re done!

$ rake db:migrate
(in /Users/pat/rails-apps/sample_app)
==  CreateEmployees: migrating ================================================
-- create_table(:employees)
   -> 0.0034s
==  CreateEmployees: migrated (0.0036s) =======================================

3 comments Tags:··