0 comments

A Pattern for RESTful URLs


I recently decided that I didn't like the way that URLs on the blog were formatted.  For example, the link to show the entry before this is:

/showpost?id=aglhZGFtY2Jsb2dyLQsSCUJsb2dJbmRleCIJYWRhbWNibG9nDAsSBFBvc3QiC2FkYW1jYmxvZzE2DA

and that is bad on a number of levels.  First, the post-specific data is the AppEngine Datastore ID of the entity that holds the post.  While it is usefully unique and a quick index to the data, it is also terribly ugly and utterly unhelpful to either human readers or search engines.  It  needs to be a slug.  That's well-and-good, as I have been working on a Sluggable mixin class to go along with the two other tools in my CMS belt, Taggable and Commentable.  I'll write more about Sluggable when it is ready to be released.

Secondly, the ID is passed in to the showpost handler as a GET parameter, and I'd rather have it be more RESTful, something like:

/showpost/acts_as_urlnameable-instructions

or even

/showpost/aglhZGFtY2Jsb2dyLQsSCUJsb2dJbmRleCIJYWRhbWNibG9nDAsSBFBvc3QiC2FkYW1jYmxvZzE2DA

since I don't have Sluggable ready.  Now, it occured to me that it would be reasonably easy to change the code up to have the RESTful-style URLs, but then I would be breaking any existing links to posts.  So, I needed to be able to switch over to the new-style while keeping the old-style available.  I came up with a pretty decent approach, I think.

The first step is that I needed to change the mapping  in the WSGIApplication setup.  You'll notice that I have removed all of the other mappings for the sake of brevity, but it used to look like this:

def main():
    application = webapp.WSGIApplication(
        [
         ('/showpost', ShowPost)
        ])
    wsgiref.handlers.CGIHandler().run(application)

In order to handle the RESTful pattern, I changed to a regular expression:

def main():
    application = webapp.WSGIApplication(
        [
         (r'^/showpost{1}(/.*)?', ShowPost)
        ])
    wsgiref.handlers.CGIHandler().run(application)

That will match both the desired new format and the must-be-tolerated old format.  Now that the mapping is set up to call the correct function, I have to go about modifying the ShowPost function.  This is how it looks:

class ShowPost(SmartHandler):
    def get(self):
        from post import Post
       
        postid = self.request.get('id')
        if postid is not None and len(postid) > 0:
            try:
                post = Post.get(postid)

Not bad, but modifying it to account for the new format while keeping the old format will be ugly, and I'll end up repeating the code in any other request-handling methods, so I'm going to abstract it a bit and put it into SmartHandler, the customized version of RequestHandler that I use.  I added the following instance method to the SmartHandler class:

def expects_request_id(self, *look_for):
    "Searches the request Uri for an embedded resource id."
       
    import string
       
    # First preference is to find it in the request Uri.  Assumption is
    # that it is the last element in a multi-element path.
    path_parts = string.split(self.request.path, "/")
       
    # Empty elements are meaningless, so delete them
    cleaned_path_parts = []
    for each_part in path_parts:
        if len(each_part) > 0:
            cleaned_path_parts.append(each_part)
       
    found_id = None
       
    if len(cleaned_path_parts) > 1:
        found_id = cleaned_path_parts[-1]
    else:
        # There is only one element in the path, so we will look
        # for id info in the GET & POST arguments.  Candidate argument
        # names are passed in through *look_for
        for each_arg_name in look_for:
            if each_arg_name in self.request.arguments():
                found_id = self.request.get(each_arg_name)
                break
               
    if found_id is None:
        raise NoIDFound
    else:
        self.requested_id = found_id
           
    return found_id

And I call it in ShowPost like this:

class ShowPost(SmartHandler):
    def get(self, *args):
        from post import Post
       
        try:
            self.expects_request_id("id")
       
            try:
                post = Post.get(self.requested_id)
                #
                # code snipped for brevity
                #
            except db.BadKeyError:
                # Render an error page here..."Sorry, but the post that you requested isn't there."
        except NoIDFound:
            # Render an error page: "Sorry, but when requesting a post, you have to specify the id of the Post."

You can see that expects_request_id has a declarative feel to it, and it seamlessly allows me to handle new-style and old-style URLs.  It assumes that any request id information is the last element in a multi-element path, and if it is a single-element path, it looks for a URL parameter that we pass in.  In this case, the parameter name is id, but it could be any string, and it could even be many different strings:

self.expects_request_id("id", "postid", "post")

would allow me to honor many different parameters.

I hope that this pattern and this code is useful.  I'll be happy to answer any questions about it, and I'm always deeply grateful for any suggestions and comments.

There are no comments.