[TIP] A Roundabout Way to Write Routes


#1

This is posted on my blog: http://inquirylabs.com/blog/?p=25

===

Suppose your Ruby on Rails application has several different kinds of
resources that are all identified by GUIDs. For example, several
images, some blog entries, and a few user-generated pages. Now
suppose that you want to access these resources from the URL without
having to identify the resource type in the URL.

Letâ??s say this is an image:

http://localhost:3000/d1e89a06-7524-a97e-3f31-f60e59b3d888

And letâ??s say this is a blog entry:

http://localhost:3000/81f5fb29-0797-7ab3-9890-5377fa416df4

Normally, using Railsâ?? routing system, you wouldnâ??t be able to
distinguish between these two URLs: you would probably force the user
to prepend the resource type, e.g.:

http://localhost:3000/blog/81f5fb29-0797-7ab3-9890-5377fa416df4

Or, you would create a single controller to take the place of the
routing system by rendering components once the resource type is
determined.

Luckily, the routing system is pretty flexible. You can, for example,
use Regular Expressions to match things on the URL and route to
different controllers depending on the result. Now, what if we
subclass Regexp and create our own â??regular expressionâ?? The result
is a simple class that performs one function: it distinguishes
resource types for the routing system.

class ResourceMatcher < Regexp
def initialize(*args)
super(’^$’)
end

def inspect
“#{self.class}.new”
end
end

class IsBlog < ResourceMatcher
def =~(identifier)
!Blog.find_by_guid(identifier).nil?
end
end

class IsImage < ResourceMatcher
def =~(identifier)
!Image.find_by_guid(identifier).nil?
end
end

Now, in our routes:

ActionController::Routing::Routes.draw do |map|
map.connect ‘:id’,
:id => IsBlog.new,
:controller => ‘blog’,
:action => ‘show’

map.connect ‘:id’,
:id => IsImage.new,
:controller => ‘image’,
:action => ‘show’
end

The reason this works is because (IsBlog.new).is_a?(Regexp) returns
true. If you go into the internals of the routing system, you’ll see
that the ResourceMatcher class is written in just such a way that it
tricks the routing system in to thinking it’s a regular old Regexp.
Once the trickery is in place, we can use the =~ matching method to
return true or false however we please.

Duane J.
(canadaduane)


#2

OMG! WTF!? LOL!

You just solved the single biggest problem and annoyance I’ve had with
Rails to date. Thank you for that, thank you so very much.

With this, I’ll be able to implement type agnostic a.k.a. “cruft-free”
URLs in our CMS. An URL such as domain.com/section/pagename could be
anything; an article, a news page, a forum, or whatever. Up until now
I’ve had to resort to horrible hacks to handle them which wouldn’t
scale at all and was really messy to maintain. This solves everything.
(Btw, this should be in one or several wiki pages, on the topic of
type agnostic, cruft-free URLs.)

Happy happy joy joy.

Regards, and with sincere gratitude,
Tomas J.


#3

With this, I’ll be able to implement type agnostic a.k.a. “cruft-free”
URLs in our CMS. An URL such as domain.com/section/pagename could be
anything; an article, a news page, a forum, or whatever. Up until now
I’ve had to resort to horrible hacks to handle them which wouldn’t
scale at all and was really messy to maintain. This solves everything.
(Btw, this should be in one or several wiki pages, on the topic of
type agnostic, cruft-free URLs.)

Can you set the controller/action with this tip? If not, you’d be no
better off doing this vs:

map.connect ‘*path’, :controller => ‘dispatcher’, :action => ‘dispatch’

If you can’t change the controller, you’re going to the same spot
anyways. Or, you’ll end up having to add routing statements for every
content type of every section. With the catch-all statement, you’ll
have a single spot to find the section from the path and figure out
what content type to render.


rick
http://techno-weenie.net


#4

On Dec 7, 2005, at 6:50 AM, Rick O. wrote:

Can you set the controller/action with this tip? If not, you’d be no

Yes, you can change the controller. That’s the main advantage.

Duane J.
(canadaduane)


#5

Very nice, Duane! This raises the question, for me: why is the
routing code looking for Regexp’s explicitly? Perhaps we could make
it more duck-type friendly, so you don’t have to subclass Regexp to
make this work.

  • Jamis

#6
  • Duane J. (removed_email_address@domain.invalid) [051207 11:20]:

that the GenerationGenerator can’t handle (perhaps its trying to
serialize them?) In any case, either approach would work for me.

Not having looked at that particular section of the Rails guts at all,
I’m idly wondering why === wasn’t used instead of checking for a Regexp?

Rick

http://www.rickbradley.com MUPRN: 227
| got it running on
random email haiku | a Tivo? Not yet, but
| I’m working on it…


#7

On Dec 7, 2005, at 7:55 AM, Jamis B. wrote:

Very nice, Duane! This raises the question, for me: why is the
routing code looking for Regexp’s explicitly? Perhaps we could make
it more duck-type friendly, so you don’t have to subclass Regexp to
make this work.

That would be super. There are actually a couple of ways we could do
this. The best way (but a little beyond my capability at the moment)
would be to allow for Procs to determine if a match occurs. I tried
this first (using what I thought would be the intuitive way… hehe)
and it failed miserably. It seems there is something about Procs
that the GenerationGenerator can’t handle (perhaps its trying to
serialize them?) In any case, either approach would work for me.

Duane J.
(canadaduane)


#8

On Dec 7, 2005, at 10:40 AM, Rick B. wrote:

and it failed miserably. It seems there is something about Procs
that the GenerationGenerator can’t handle (perhaps its trying to
serialize them?) In any case, either approach would work for me.

Not having looked at that particular section of the Rails guts at all,
I’m idly wondering why === wasn’t used instead of checking for a
Regexp?

Here’s the code in question (from routing.rb):
def test_condition(expression, condition)
case condition
when String then “(#{expression} == #{condition.inspect})”
when Regexp then
condition = Regexp.new("^#{condition.source}$") unless /^
^.*$$/ =~ condition.source
“(#{condition.inspect} =~ #{expression})”
when Array then
conds = condition.collect do |condition|
cond = test_condition(expression, condition)
(cond[0, 1] == ‘(’ && cond[-1, 1] == ‘)’) ? cond : “(#
{cond})”
end
“(#{conds.join(’ || ')})”
when true then expression
when nil then “! #{expression}”
else
raise ArgumentError, “Valid criteria are strings,
regular expressions, true, or nil”
end
end

The reason I chose to subclass Regexp instead of String is because
for Regexp, the =~ operator is a method of the condition–for the
string, the == is a method of the expression.

This code looks fairly clean, and may be a little bit difficult to
duck type without adding too much… but maybe Jamis has some ideas :slight_smile:

Duane


#9

Good Grief Rick,

either I’m in your head or your in my head but… get out of my
head! :stuck_out_tongue:

Trevor

On 7-Dec-05, at 9:40 AM, Rick B. wrote:

and it failed miserably. It seems there is something about Procs
| got it running on
random email haiku | a Tivo? Not yet, but
| I’m working on it…


Rails mailing list
removed_email_address@domain.invalid
http://lists.rubyonrails.org/mailman/listinfo/rails

Trevor S.
http://somethinglearned.com


#10

I’ve not looked at the routing code in depth so I could be talking
out of my (…)

But the first thing that springs to mind (duck-wise) is the ===
comparison operator. Regexp instances already implement it and the
documentation for Duane’s technique would be pretty simple:
“implement def===(arg)”

Regards,
Trevor

On 7-Dec-05, at 8:14 AM, Duane J. wrote:

moment) would be to allow for Procs to determine if a match
Rails mailing list
removed_email_address@domain.invalid
http://lists.rubyonrails.org/mailman/listinfo/rails

Trevor S.
http://somethinglearned.com