Pleased to Meet You, Won’t You Guess My Name?
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:
-
I'm hard-coding the singular, plural and collection forms, which are primarily used in routing, so Rails's URL helpers will always point back to
Ando::ItemsController
even if the object in question is a subclass. -
I'm leaving the
@element
alone; it'll have been pre-populated with the name of the current subclass. For example, in a subclass calledAndo::Photo
, the collection will beitems
but the element will bephoto
. -
Which leads me to the partial_path: because collection has been hard-coded but element has not, the partial path for my
Ando::Photo
subclass will beando/items/photo
, notando/items/item
orando/photos/photo
.
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.