Pleased to Meet You, Won’t You Guess My Name?

January 16, 2010

While working on a little Rails project this week, I discovered something very interesting (and little-documented) in ActiveSupport: Module.model_name. It's a core extension to support Rails's handling of models as RESTful resources, as in the examples below:

# BlogPost is a subclass of ActiveRecord::Base
@blog_post = BlogPost.find(108)

# Same as render "blog_posts/blog_post", :object => @blog_post
render @blog_post

# Will call blog_post_url(@blog_post.id)
url_for @blog_post #=> "/blog_posts/108"

# Lots of Rails's tag/form helpers use this
dom_id @blog_post #=> "blog_post_108"

The model_name method (which gets inherited by pretty much everything, because it's in the Module class) returns a special ActiveSupport::ModelName object. ModelName is just a subclass of reg'lar old String, that gets initialized using the class name, then uses that name to pre-bake certain name components used by the record identifier:

Ando::BlogPost.model_name #=> "blog_post"

# Look at all these useful variations!
Ando::BlogPost.model_name.singular #=> "ando_blog_post"
Ando::BlogPost.model_name.plural #=> "ando_blog_posts"
Ando::BlogPost.model_name.collection #=> "ando/blog_posts"
Ando::BlogPost.model_name.element #=> "blog_post"
Ando::BlogPost.model_name.partial_path #=> "blog_posts/blog_post"

What's interesting here is that since this one object is responsible for 90% of Rails's resource-mapping magic, this makes it extremely simple to override part or all of that magic. Of course, it's generally a bad idea to override a default (and very widely used) bit of Rails's behavior, which is why it's especially good that this can be done on a class-by-class basis.

Right now I'm working on a little CMS project where I'm using single collection inheritance in MongoMapper to distinguish between several subclasses of my main Item class. Ordinarily, unless you want to have separate routes, controllers and views for each subclass, it would be a pain in the ass to even try doing it this way, as these days so much of Rails's syntactic sugar is based around the record identifier.

So in my base class, I did a little instance_eval'ing on the model_name object:

class Ando::Item
  include MongoMapper::Document
  set_collection_name "items"

  def self.model_name
    model_name = super

    model_name.instance_eval do
      @singular     = "ando_items".freeze
      @plural       = "ando_item".freeze
      @collection   = "items".freeze

      # Note that I'm setting my own @collection, but NOT @element
      @partial_path = "#{@collection}/#{@element}".freeze
    end

    model_name
  end

end

To explain a bit what I'm doing here:

Hopefully you can imagine how this would be useful on a project where there are ten or more subclasses of the same model.

Curiously, I didn't know this module even existed until I went looking at how Rails knew how to map MongoMapper documents this week. The original implementation of the record identifier in Rails 2.0 was a lot less flexible, and there's been so little discussion of ever needing to override this behavior that I assumed it hadn't been touched since then.