Human-Readable ActiveResource URLs

I’ve got URLs on the brain this week. I started NearbyGamers using Rails 1.1 with just gamers and tags. I upgraded to Rails 1.2 (and liked it) , and added discussions after I updated to Rails 1.2. I was able to use ActiveResource for Discussions with Posts as a nested resource. I’m really happy with this code, as it’s very tidy.

On Sunday I updated gamers and tags to use ActiveResource. It wasn’t hard, mostly just renames (the ‘view’ action became ‘show’, for example) and removing url_for hashes (:controller =\> 'gamers', :action =\> 'view', :handle =\> @gamer.handle) with much nicer named routes (gamer_path(@gamer)). A gripe I have about ActiveResource is that it has poor URLs, with internal record IDs leaking out. I’ve had nice URLs (/gamers/Harkins and /tags/RPGs) for gamers and tags and I wanted to keep them.

Joshua Schacter gave a presentation on building and growing del.icio.us that included the gems (among many other gems):

  • Dont expose your unique id’s to the outside world (php?id=1 etc.) people can scrape through everything very easily. This is why del.icio.us uses MD5 hashes of links instead.
  • URLs are prime real estate - respect them

It turns out this is not just possible but easy, though I blew an hour or so finding it (Rails docs still need love). In my Gamer and Tag objects, I just define a to_param method and the URL builder picks it up:

class Gamer < ActiveRecord::Base def to_param self.handle end end class Tag < ActiveRecord::Base def to_param self.name end end

# and over in config/routes.rb I do the usual: map.resources :gamers map.resources :tags, :memger => { :history => :get, :variantes => :get } # and now gamer_path(@gamer) and tag_path(@tag) work in my views

My controller looks like:

class GamersController < ApplicationController before_filter :load_gamer, :except => [ :index ]

# The regular index, show, edit, and update actions here

private def load_gamer @gamer = Gamer.find_by_handle(params[:id], :include => [:location, :tags]) raise ::ActionController::RoutingError, “Recognition failed for #{request.path}” if @gamer.nil? # canonicalize case redirect_to gamer_path(@gamer) and return if @gamer.handle != params[:id] end

And every action that needs it has the @gamer object loaded from the URL param, with 404 errors handled and mistaken case fixed. There’s a patch that will let you rename the :id parameter. I can’t say I mind if it’s not accepted; I don’t mind calling it :id instead of :handle or :name because I think of it as the public id of the object.

Come back Thursday, I’ll tell you how I made my discussion URLs even more useful.