Hey guys,
I've been working on extracting Merb's router into a rack middleware
library as well as adding a bunch of features to try to get "mountable
apps" working in Merb 1.1. I've been working with Josh Peek on routing
in general so that we can get the best solution done for Rails 3.
Quite a lot of the code and concepts are drawn from his rack-mount
project.
This is a proof of concept that I am submitting to the mailing list
for feedback. All the code is at:
http://github.com/carllerche/rack-router
Abstract
========
Conceptually, rack-router allows you to create a two way map between
HTTP requests and Rack applications. It is built as a piece of
middleware that takes in a set of routes. When a request comes in, the
router will compare that request against the set of routes until it
finds one that matches. It then calls the associated rack app. It can
also generate URL's that you can use to link to other mountable apps.
It also goes quite a bit further and attempts to make reusing rack
applications completely painless (what we are tentatively calling
"mountable apps").
Disclaimer
==========
rack-router is not for most rack middleware and small applications. It
is for any rack based application that needs to generate URIs or allow
other applications to generate URIs that point to it
The Problem
===========
There really are quite a number of problems that we are trying to
solve here.
* Be able to take any Rack application, and have it behave the same no
matter which HTTP space it lives in.
For example, I build a blog rack application. I would like to take
this blog and use it in one rails app and have it live at /blog and
then I would like to add it to a Sinatra application and have it live
at /news. Obviously, the blog application has a set of routes that it
needs to work. I want the routes to work the same no matter which path
prefix it lives in. Conversely, I need to be able to generate links in
the blog application that will be correct no matter where the blog
lives.
* Be able to create dependencies between Rack applications.
Continuing with the blog example, the actual blog gem could expose
multiple rack applications that can be mounted at different spots. For
example, my blog application exposes two rack applications. BlogSite
and BlogAdmin. The first is the actual public facing blog and the
second is the admin section. My end application requirements are that
the admin section is at
http://admin.example.org and that the main
site is at
http://example.org. The BlogAdmin has to be able to say "I
need the actual blog to exist, but I don't know where it will live".
Somehow, when the BlogAdmin is generating URLs that point to BlogSite,
they have to take into account where BlogSite ends up being mounted.
Another example of this is cloudkit. I've been talking a lot with Jon
Crosby about this router library. Currently in cloudkit, he has an
oauth middleware that cloudkit depends on that he has to vendor and
cannot release as a separate gem because he assumes that all the oauth
routes exist at /oauth. Ideally, cloudkit would be able to say, "I
depend on Rack::OAuth", and then just call a method to generate an
OAuth route and it will just work.
* Be able to specify interfaces that can be implemented differently by
different libraries
Most applications depends on an authentication strategy that
includes :login, :logout, and :signup, but there could be many
different ways to implement this (aka, it isn't necessarily a single
library that is the end all solution). I would like there to be a way
to define an "authentication interface" that requires there to exist a
certain set of routes (say, :login, :logout, :signup) so that, if I
build a blog rack app, I can just say "generate the login route here"
and it just works. This would also be a good thing for OAuth, OpenID,
etc... apps.
* NO MATTER WHAT, the mounted app should not have to worry about the
fact that is mounted or not
* The solution should be Rack specific, and not depend on any rack
compatible framework
Proposed Solution
=================
The rack-router library solves these problems. Besides basic routing
that supports all of Rails' and Merb's routing features (example 1),
it allows you to mount any rack application and isolate it in a URI
(or arbitrary value of the request) space. After a route matches and
before calling the associated Rack application, the router will update
PATH_INFO and SCRIPT_NAME to reflect the mounted location. Any
captures specified in the route will be extracted to a hash that is
available at env['rack_router.params'] (example 2). It will also keep
track of where each router is mounted and provide a url(route_name,
options = {}) method that will generate a URL that matches the route
(example 3).
+ Example 1: Basic Routing
use Rack::Router do |route|
route.map "/users", :to => Users
route.map "/posts", :to => Posts, :name => :posts
end
my_app.url(:posts) # => /posts
Here, my_app is an instance of the Rack::Router and it provides a
url method that generates the routes.
-----
+ Example 2: Accessing captures in the rack app
-----
+ Example 3: Mounting routers
# Blog application
blog = Rack::Router do |route|
route.map "/posts", :to => Posts, :name => :posts
end
# Main application
use Rack::Router do |route|
route.map "/users", :to => Users
route.map "/blog", :to => blog, :name => :blog
end
blog.url(:posts) # => /blog/posts
# It also provides access to child routers
my_app.blog.url(:posts) # => /blog/posts
The point here is that both request recognition and URL generation
are abstracted so that the rack application does not need to worry
about where it lives.
-----
Besides this, the Rack::Router supports the ability to lazily provide
access to rack application dependencies so that URLs can be generated
to them correctly (example 4)
+ Example 4: Application dependencies
module Blog
class Admin
include Rack::Router::Routable
def initialize
prepare :dependencies => { Site => :blog } do |route|
route.map "/posts", :to => Posts, :name => :posts
end
end
end
class Site
include Rack::Router::Routable
def initialize
prepare do |route|
route.map "/posts", :to => Posts, :name => :posts
end
end
end
end
admin = Blog::Admin.new
blog = Blog::Site.new
use Rack::Router do |route|
route.map "/admin", :to => admin
route.map "/blog", :to => blog
end
admin.blog.url(:posts) # => /blog/posts
my_app.admin.url(:posts) # => /admin/posts
So, the Blog::Admin app requires Blog::Site, once it is mounted, the
router has access to it so the application can generate correct URLs.
Other Features
==============
The actual DSL for defining the route is completely pluggable. The DSL
I used above is a pretty simple and low level DSL that I implemented
mostly to write specs. A router that was built using Rails' DSL can
work with one using Merb's. Sinatra would even be able to use this
quite easily.
Rack::Router
What now?
=========
This is still just a proof of concept. I coded it to make it as
readable as possible and did not worry about speed at all. I would
like to get as much feedback as possible before worrying about speed.
Josh Peek has quite a good number of optimizations that can easily
apply to the code base.
So, tell me what you think, does solve problems, would you use this?
Currently, rack-router only generates paths, but I am planning on
adding support for generating full URIs soon as well as getting
information on the HTTP method required for the route.
Outstanding Concerns
====================
Currently, the ability to generate routes doesn't seem as abstracted
as it could be. There is no concept of generating routes currently in
the rack spec, so it's been implemented somewhat arbitrarily. Josh had
an idea of some kind of interface that any rack application could
implement to have it compatible with generating routes given an
arbitrary mount point.
Also, the API for generating mounted app routes (aka, my_app.blog.url
(:posts)) feels slightly strange. My goal is to keep the routes fully
namespaced for each application. I don't want to namespace it with a
symbol name prefix (aka, :blog_posts).
As of now, the problem of "Being able to specify routing interfaces"
is not really handled too elegantly. Ideas for this particular problem
are appreciated.
I also have a bunch of notes in the README. I'm still unsure as to the
best implementation for all this, so please pipe up.