URL Routing

Often it is desirable to describe a resource hierarchy by matching URL segments in the request, this is commonly referred to as “URL routing”.

A Python-based Domain Specific Language is used to specify and match routing paths, string literal components are matched for structure while plain callable components match segment values and are stored by name for use in the handler, assuming all the components match; this makes it trivial to create new functions to match path components.

In order to promote better reuse of resources—by composing and nesting them—it is only possible to specify relative routes.

Router basics

Router.route will match a URL route exactly, meaning that every route component must match the respective segment in the URL path; eg. route('foo') will only match a relative URL path of exactly one segment that must be the string foo.

Router.subroute will partially match a URL route, meaning that once every route component has matched its respective segment in the URL path the route will be a match, regardless of whether there are URL path segments left over. This is useful for the case where you wish to match enough of the URL to know that you should delegate to another resource.

A route handler may return any of the values that ISpinneretResource.locateChild supports.

Routes are intended to be used as method decorators and may be stacked to have multiple routes serviced by the same handler.

Call Router.resource to produce an IResource suitable for composing with other parts of Spinneret or Twisted Web.

Special routes

There are two routes—particularly in the case of nested routers—that may not be obvious at first: The null root and the empty route.

Assuming we had the following hierarchy:

class Root(object):
    router = Router()

    def bar(self, request, params):
        return SubRoot().router.resource()

class SubRoot(object):
    router = Router()

In the case of a request for the resource at /bar/ we can match that by declaring a route in SubRoot with @router.route('/') or @router.route('') (the empty route.) If the request was instead for the resource at /bar (note the absence of the trailing /) we can match that with @router.route() (the null route.)

Matcher basics

txspinneret.route contains some basic matchers such as Any (which is a synonym for Text) and Integer. These matchers are simple factory functions that take some parameters and produce a callable that takes the IRequest and the segment being matched, as bytes, returning a 2-tuple of the parameter name and the processed matched value (or None if there is no match.) Writing your own matchers to suit your needs is encouraged.

Reducing router resource boilerplate

When using routers as resources (such as when nesting routers) it becomes common to write return SomeRouter(...).router.resource(). The routedResource decorator can be used to wrap a router class into a callable that returns a resource.

An example router

from collections import namedtuple
from twisted.web.static import Data
from txspinneret.route import Router, Any, routedResource

class UserResource(object):
    router = Router()

    def __init__(self, user):
        self.user = user

    def getFriend(self, name):
        return self.user.friends[name]

    def name(self, request, params):
        return Data(self.user.name, 'text/plain')

    @router.subroute('friend', Any('name'))
    def friend(self, request, params):
        return UserResource(self.getFriend(params['name']))

def start():
    User = namedtuple(b'User', ['name', 'friends'])
    bob = User('bob', {})
    chuck = User('chuck', {'bob': bob})
    default = User('default', {'bob': bob, 'chuck': chuck})
    return UserRouter(default)

(Source: user_router.py)

Putting this in a file called user_router.py and running twistd -n web --class=user_router.start you’ll find it reacts as below:

$ curl http://localhost:8080/
$ curl http://localhost:8080/friend/chuck/friend/bob/