O'Reilly logo

RESTful Web Services by Sam Ruby, Leonard Richardson

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Controller Code

Now we come to the heart of the application: the code that converts incoming HTTP requests into specific actions on the database. I’m going to define a base class called ApplicationController, which contains common code, including almost all of the tricky code. Then I’ll define the six controller classes I promised earlier.

Each controller class will implement some actions: methods that are called to handle a HTTP request. Rails defines a list of standard actions that correspond to methods from HTTP’s uniform interface. I mentioned these earlier: the index action is invoked in response to GET for a “list” type resource, and so on. Those are the actions I’ll be defining, though many of them will delegate to other actions with nonstandard names.

There’s a lot of code in this application, but relatively little of it needs to be published in this book. Most of the low-level details are in Rails, the plugins, and the atom-tools gem. I can express my high-level ideas almost directly in code. Of course, my reliance on external code occasionally has downsides, like the fact that some of my representations don’t contain links.

What Rails Doesn’t Do

There’s one feature I want for my service that isn’t built into Rails or plugins, and there’s another that goes against Rails’s path of least resistance. I’m going to be implementing these features myself. These two items account for much of the tricky code in the service.

Conditional GET

Wherever possible, a web service should send the response headers Last-Modified and ETag along with a representation. If the client makes future requests for the same resource, it can make its requests conditional on the representation having changed since the last GET. This can save time and bandwidth; see Conditional GET” in Chapter 8 for more on this topic.

There are third-party Rails controllers that let the programmer provide values for Last-Modified and ETag. Core Rails doesn’t do this, and I don’t want to bring in the additional complexity of a third-party controller. I implement a fairly reusable solution for Last-Modified in Example 7-9.

param[:id] for things that aren’t IDs

Rails assumes that resources map to ActiveRecord objects. Specifically, it assumes that the URI to a “list item” resource identifies a row in a database table by ID. For instance, it assumes the client will request the URI /v1/users/4 instead of the more readable URI /v1/users/leonardr.

The client can still request /users/leonardr, and the controller can still handle it. This just means that the username will be available as params[:id] instead of something more descriptive, like params[:username].

If a URI contains more than one path variable, then when I define that URI in routes.rb I get to choose the params name for all but the last one. The last variable always gets put into params[:id], even if it’s not an ID. The URI /v1/users/leonardr/tags/food has two path variables, for example. params[:username], named back in Example 7-3, has a value of “leonardr”. The tag name is the one that gets put into params[:id]. I’d rather call it params[:tag], but there’s no good way to do that in Rails. When you see params[:id] in the code below, keep in mind that it’s never a database ID.

The ApplicationController

This class is the abstract superclass of my six controllers, and it contains most of the common functionality (the rest will go into the ActiveRecord model classes). Example 7-7 starts by defining an action for the single most common operation in this service: fetching a list of bookmarks that meet some criteria.

Example 7-7. app/controllers/application.rb

# app/controllers/application.rb
require 'digest/sha1'
require 'digest/md5'
require 'rubygems'
require 'atom/feed'

class ApplicationController < ActionController::Base

  # By default, show 50 bookmarks at a time.
  @@default_limit = 50

  ## Common actions

  # This action takes a list of SQL conditions, adds some additional
  # conditions like a date filter, and renders an appropriate list of
  # bookmarks. It's used by BookmarksController, RecentController,
  # and TagsController.
  def show_bookmarks(conditions, title, feed_uri, user=nil, tag=nil)
    errors = []

    # Make sure the specified limit is valid. If no limit is specified,
    # use the default.
    if params[:limit] && params[:limit].to_i < 0
      errors << "limit must be >=0"
    params[:limit] ||= @@default_limit
    params.delete(:limit) if params[:limit] == 0  # 0 means "no limit"

    # If a date filter was specified, make sure it's a valid date.
    if params[:date]
        params[:date] = Date.parse(params[:date])
      rescue ArgumentError
        errors << "incorrect date format"

    if errors.empty?
      conditions ||= [""]
      # Add a restriction by date if necessary.
      if params[:date]
        conditions[0] << " AND " unless conditions[0].empty?
        conditions[0] << "timestamp >= ? AND timestamp < ?"
        conditions << params[:date]
        conditions << params[:date] + 1

      # Restrict the list to bookmarks visible to the authenticated user.
      Bookmark.only_visible_to!(conditions, @authenticated_user)

      # Find a set of bookmarks that matches the given conditions.
      bookmarks = Bookmark.custom_find(conditions, tag, params[:limit])
      # Render the bookmarks however the client requested.
      render_bookmarks(bookmarks, title, feed_uri, user)
      render :text => errors.join("\n"), :status => "400 Bad Request"

The show_bookmarks method works like any Rails action: it gets query parameters like limit from params, and verifies them. Then it fetches some data from the database and renders it with a view. A lot of my RESTful action methods will delegate to this method. If the RESTful action specifies no conditions, show_bookmarks will fetch all the bookmarks that match the date and tag filters, up to the limit. Most of my actions will impose additional conditions, like only fetching bookmarks posted by a certain user.

The main difference between show_bookmarks and a traditional Rails action is in the view. Most Rails actions define the view with an ERb template like show.rhtml: a combination of HTML and Ruby code that works like JSP templates or PHP code. Instead, I’m passing the job off to the render_bookmarks function (see Example 7-8). This function uses code-based generators to build the XML and Atom documents that serve as representations for most of my application’s resources.

Example 7-8. application.rb continued: render_bookmarks

  # This method renders a list of bookmarks as a view in RSS, Atom, or
  # ActiveRecord XML format. It's called by show_bookmarks
  # above, which is used by three controllers. It's also used
  # separately by UriController and BookmarksController.
  # This view method supports conditional HTTP GET.
  def render_bookmarks(bookmarks, title, feed_uri, user, except=[])
    # Figure out a current value for the Last-Modified header.
    if bookmarks.empty?
      last_modified = nil
      # Last-Modified is the most recent timestamp in the bookmark list.
      most_recent_bookmark = bookmarks.max do |b1,b2|
        b1.timestamp <=> b2.timestamp
      last_modified = most_recent_bookmark.timestamp
    # If the bookmark list has been modified since it was last requested...
    render_not_modified_or(last_modified) do
      respond_to do |format|
        # If the client requested XML, serialize the ActiveRecord
        # objects to XML. Include references to the tags in the
        # serialization.
        format.xml  { render :xml => 
          bookmarks.to_xml(:except => except + [:id, :user_id],
                           :include => [:tags]) }
        # If the client requested Atom, turn the ActiveRecord objects
        # into an Atom feed.
        format.atom { render :xml => atom_feed_for(bookmarks, title, 
                                                   feed_uri, user) }

That method is also where I start handling conditional HTTP requests. I’ve chosen to use the timestamp of the most recent bookmark as the value of the HTTP header Last-Modified.

The rest of the conditional request handling is in the render_not_modified_or function (see Example 7-9). It’s called just before render_bookmarks is about to write the list of bookmarks, and it applies the rules of conditional HTTP GET. If the list of bookmarks has changed since this client last requested it, this function calls the Ruby keyword yield and the rest of the code in render_bookmarks runs normally. If the list of bookmarks hasn’t changed, this function short-circuits the Rails action, sending a response code of 304 (“Not Modified”) instead of serving the representation.

Example 7-9. application.rb continued: render_not_modified_or

  ## Helper methods

  # A wrapper for actions whose views support conditional HTTP GET.
  # If the given value for Last-Modified is after the incoming value
  # of If-Modified-Since, does nothing. If Last-Modified is before
  # If-Modified-Since, this method takes over the request and renders
  # a response code of 304 ("Not Modified").
  def render_not_modified_or(last_modified)
    response.headers['Last-Modified'] = last_modified.httpdate if last_modified

    if_modified_since = request.env['HTTP_IF_MODIFIED_SINCE']
    if if_modified_since && last_modified &&
        last_modified <= Time.httpdate(if_modified_since)
      # The representation has not changed since it was last requested.
      # Instead of processing the request normally, send a response
      # code of 304 ("Not Modified").
      render :nothing => true, :status => "304 Not Modified"
      # The representation has changed since it was last requested.
      # Proceed with normal request processing.

Example 7-10 shows one more helper function used in multiple actions. The if_found method makes sure the client specified a URI that corresponds to an object in the database. If given a non-null object, nothing happens: if_found uses yield to return control to the action that called it. If given a null object, the function short-circuits the request with a response code of 404 (“Not Found”), and the action never gets a chance to run.

Example 7-10. application.rb continued: if_found.

  # A wrapper for actions which require the client to have named a
  # valid object. Sends a 404 response code if the client named a
  # nonexistent object. See the user_id_from_username filter for an
  # example.
  def if_found(obj)
    if obj
      yield     else
      render :text => "Not found.", :status => "404 Not Found"

I’ve also implemented a number of filters: pieces of code that run before the Rails actions do. Some Rails filters perform common setup tasks (see Example 7-11). This is the job of authenticate, which checks the client’s credentials. Filters may also check for a problem and short-circuit the request if they find one. This is the job of must_authenticate, and also must_specify_user, which depends on the if_found method defined above. Filters let me keep common code out of the individual actions.

Example 7-11. application.rb continued: filters

  ## Filters

  # All actions should try to authenticate a user, even those actions
  # that don't require authorization. This is so we can show an
  # authenticated user their own private bookmarks.
  before_filter :authenticate

  # Sets @authenticated_user if the user provides valid
  # credentials. This may be used to deny access or to customize the
  # view.
  def authenticate    
    @authenticated_user = nil
    authenticate_with_http_basic do |user, pass|      
      @authenticated_user = User.authenticated_user(user, pass)
    return true

  # A filter for actions that _require_ authentication. Unless the
  # client has authenticated as some user, takes over the request and
  # sends a response code of 401 ("Unauthorized").  Also responds with
  # a 401 if the user is trying to operate on some user other than
  # themselves. This prevents users from doing things like deleting
  # each others' accounts.
  def must_authenticate    
    if @authenticated_user && (@user_is_viewing_themselves != false)
      return true
      request_http_basic_authentication("Social bookmarking service")      
      return false

  # A filter for controllers beneath /users/{username}. Transforms 
  # {username} into a user ID. Sends a 404 response code if the user
  # doesn't exist.
  def must_specify_user
    if params[:username]
      @user = User.find_by_name(params[:username])      
      if_found(@user) { params[:user_id] = @user.id }
      return false unless @user
    @user_is_viewing_themselves = (@authenticated_user == @user)
    return true

Finally, the application controller is where I’ll implement my primary view method: atom_feed_for (see Example 7-12). This method turns a list of ActiveRecord Bookmark objects into an Atom document. The controller that wants to serve a list of bookmarks needs to provide a title for the feed (such as “Bookmarks for leonardr”) and a URI to the resource being represented. The resulting document is rich in links. Every bookmark links to the external URI, to other people who bookmarked that URI, and to bookmarks that share tags with this one.

Example 7-12. application.rb concluded: atom_feed_for

  ## Methods for generating a representation

  # This method converts an array of ActiveRecord's Bookmark objects
  # into an Atom feed.
  def atom_feed_for(bookmarks, title, feed_uri, user=nil)
    feed = Atom::Feed.new
    feed.title = title
    most_recent_bookmark = bookmarks.max do |b1,b2|
      b1.timestamp <=> b2.timestamp
    feed.updated = most_recent_bookmark.timestamp

    # Link this feed to itself
    self_link = feed.links.new
    self_link['rel'] = 'self'
    self_link['href'] = feed_uri + ".atom"

    # If this list is a list of bookmarks from a single user, that user is
    # the author of the feed.
    if user
      user_to_atom_author(user, feed)

    # Turn each bookmark in the list into an entry in the feed.
    bookmarks.each do |bookmark|
      entry = feed.entries.new
      entry.title = bookmark.short_description
      entry.content = bookmark.long_description

      # In a real application, a bookmark would have a separate
      # "modification date" field which was not under the control of
      # the user. This would also make the Last-Modified calculations
      # more accurate.
      entry.updated = bookmark.timestamp      

      # First, link this Atom entry to the external URI that the
      # bookmark tracks.
      external_uri = entry.links.new
      external_uri['href'] = bookmark.uri

      # Now we give some connectedness to this service. Link this Atom
      # entry to this service's resource for this bookmark.
      bookmark_resource = entry.links.new
      bookmark_resource['rel'] = "self"
      bookmark_resource['href'] = bookmark_url(bookmark.user.name, 
                                               bookmark.uri_hash) + ".atom"
      bookmark_resource['type'] = "application/xml+atom"

      # Then link this entry to the list of users who've bookmarked
      # this URI.
      other_users = entry.links.new
      other_users['rel'] = "related"
      other_users['href'] = uri_url(bookmark.uri_hash) + ".atom"
      other_users['type'] = "application/xml+atom"

      # Turn this entry's user into the "author" of this entry, unless
      # we already specified a user as the "author" of the entire
      # feed.
      unless user
        user_to_atom_author(bookmark.user, entry) 

      # For each of this bookmark's tags...
      bookmark.tags.each do |tag|
        # ...represent the tag as an Atom category.
        category = entry.categories.new
        category['term'] = tag
        category['scheme'] = user_url(bookmark.user.name) + "/tags"

        # Link to this user's other bookmarks tagged using this tag.
        tag_uri = entry.links.new
        tag_uri['href'] = tag_url(bookmark.user.name, tag.name) + ".atom"
        tag_uri['rel'] = 'related'
        tag_uri['type'] = "application/xml+atom"

        # Also link to all bookmarks tagged with this tag.
        recent_tag_uri = entry.links.new
        recent_tag_uri['href'] = recent_url(tag.name) + ".atom"
        recent_tag_uri['rel'] = 'related'
        recent_tag_uri['type'] = "application/xml+atom"
    return feed.to_xml

  # Appends a representation of the given user to an Atom feed or element
  def user_to_atom_author(user, atom)
    author = atom.authors.new
    author.name = user.full_name
    author.email = user.email
    author.uri = user_url(user.name)

Example 7-13 shows what kind of Atom representation this method might serve.

Example 7-13. An Atom representation of a list of bookmarks

<feed xmlns='http://www.w3.org/2005/Atom'>
 <title>Bookmarks for leonardr</title>
 <link href="http://localhost:3000/v1/users/leonardr/bookmarks.atom" rel="self"/>
  <title>REST and WS-*</title>
  <content>Joe Gregorio's lucid explanation of RESTful principles</content>
  <category term="rest" scheme="http://localhost:3000/v1/users/leonardr/rest"/>
  <link href="http://bitworking.org/news/125/REST-and-WS" rel="alternate"/>
  <link href="http://localhost:3000/v1/users/leonardr/bookmarks/68044f26e373de4a08ff343a7fa5f675.atom" 
   rel="self" type="application/xml+atom"/>
  <link href="http://localhost:3000/v1/recent/rest.atom"
   rel="related" type="application/xml+atom"/>

The UsersController

Now I’m ready to show you some specific actions. I’ll start with the controller that makes user accounts possible. In the code in Example 7-14, note the call to before_filter that sets up the must_authenticate filter. You don’t need to authenticate to create (POST) a user account (as whom would you authenticate?), but you must authenticate to modify (PUT) or destroy (DELETE) an account.

Example 7-14. app/controllers/users_controller.rb

class UsersController < ApplicationController  

  # A client must authenticate to modify or delete a user account.
  before_filter :must_authenticate, :only => [:modify, :destroy]

  # POST /users
  def create   
    user = User.find_by_name(params[:user][:name])
    if user
      # The client tried to create a user that already exists.
      headers['Location'] = user_url(user.name)
      render :nothing => true, :status => "409 Conflict"
      user = User.new(params[:user])
      if user.save
        headers['Location'] = user_path(user.name)
        render :nothing => true, :status => "201 Created"
        # There was a problem saving the user to the database.
        # Send the validation error messages along with a response
        # code of 400.
        render :xml => user.errors.to_xml, :status => "400 Bad Request"

The conventions of RESTful Rails impose a certain structure on UsersController (and, indeed, on the name of the class itself). This controller exposes a resource for the list of users, and one resource for each particular user. The create method corresponds to a POST to the user list. The show, update, and delete methods correspond to a GET, PUT, or DELETE request on a particular user.

The create method follows a pattern I’ll use for POST requests throughout this service. If the client tries to create a user that already exists, the response code is 409 (“Conflict”). If the client sends bad or incomplete data, the ActiveRecord validation rules (defined in the User) model) fail, and the call to User#save returns false. The response code then is 400 (“Bad Request”). If all goes well, the response code is 201 (“Created”) and the Location header contains the URI of the newly created user. All I’ve done in Example 7-15 is put into code the things I said in What’s Supposed to Happen?” and What Might Go Wrong?” earlier in this chapter. I’ll mention this generic control flow again in Chapter 8.

Example 7-15. app/controllers/users_controller.rb continued

  # PUT /users/{username}
  def update
    old_name = params[:id]
    new_name = params[:user][:name]
    user = User.find_by_name(old_name)

    if_found user do
      if old_name != new_name && User.find_by_name(new_name)
        # The client tried to change this user's name to a name
        # that's already taken. Conflict!
        render :nothing => true, :status => "409 Conflict"      
        # Save the user to the database.
        if user.save
          # The user's name changed, which changed its URI.
          # Send the new URI.
          if user.name != old_name
            headers['Location'] = user_path(user.name)
            status = "301 Moved Permanently"
            # The user resource stayed where it was.
            status = "200 OK"
          render :nothing => true, :status => status
          # There was a problem saving the user to the database.
          # Send the validation error messages along with a response
          # code of 400.
          render :xml => user.errors.to_xml, :status => "400 Bad Request"

The update method has a slightly different flow, and it’s a flow I’ll use for PUT requests throughout the service. The general outline is the same as for POST. The twist is that instead of trying to create a user (whose name might already be in use), the client can rename an existing user (and their new name might already be in use).

I send a 409 response code (“Conflict”) if the client proposes a new username that already exists, and a 400 response code (“Bad Request”) if the data validation fails. If the client successfully edits a user, I send not a 201 response code (“Created”) but a simple 200 (“OK”).

The exception is if the client successfully changes a user’s name. Now that resource is available under a different URI: say, /users/leonard instead of /users/leonardr. That means I need to send a response code of 301 (“Moved Permanently”) and put the user’s new URI in the Location header.

The GET and DELETE implementations are more straightforward, as shown in Example 7-16.

Example 7-16. app/controllers/users_controller.rb continued

  # GET /users/{username}
  def show
    # Find the user in the database.
    user = User.find_by_name(params[:id])
    if_found(user) do
      # Serialize the User object to XML with ActiveRecord's to_xml.
      # Don't include the user's ID or password when building the XML
      # document.
      render :xml => user.to_xml(:except => [:id, :password])

  # DELETE /users/{username}
  def destroy
    user = User.find_by_name(params[:id])
    if_found user do
      # Remove the user from the database.
      render :nothing => true, :status => "200 OK"

There is one hidden detail: the if_found method sends a response code of 404 (“Not Found”) if the user tries to GET or DELETE a nonexistent user. Otherwise, the response code is 200 (“OK”). I have not implemented conditional HTTP GET for user resources: I figured the possible bandwidth savings wasn’t big enough to justify the added complexity.

The BookmarksController

This is the other main controller in this application (see Example 7-17). It exposes a user’s list of bookmarks and each individual bookmark. The filters are interesting here. This BookmarksController is for displaying a particular user’s bookmarks, and any attempt to see a nonexistent user’s bookmarks should be rebuffed with a stern 404 (“Not Found”). That’s the job of the must_specify_user filter I defined earlier. The must_authenticate filter works like it did in UsersController: it prevents unauthenticated requests from getting through to Rails actions that require authentication. I’ve also got a one-off filter, fix_params, that enforces consistency in incoming representations of bookmarks.

Example 7-17. app/controllers/bookmarks_controller.rb

class BookmarksController < ApplicationController
  before_filter :must_specify_user
  before_filter :fix_params
  before_filter :must_authenticate, :only => [:create, :update, :destroy]

  # This filter cleans up incoming representations.
  def fix_params    
    if params[:bookmark]
      params[:bookmark][:user_id] = @user.id if @user

The rest of BookmarksController is just like UsersController: fairly involved create (POST) and update (PUT) methods, simple show (GET) and delete (DELETE) methods (see Example 7-18). The only difference is that this controller’s list resource responds to GET, so I start with a simple implementation of index. Like many of the Rails actions I’ll define, index and show simply delegate to the show_bookmarks action.

Example 7-18. app/controllers/bookmarks_controller.rb continued

  # GET /users/{username}/bookmarks
  def index
    # Show this user's bookmarks by passing in an appropriate SQL
    # restriction to show_bookmarks.
    show_bookmarks(["user_id = ?", @user.id],
                   "Bookmarks for #{@user.name}", 
                   bookmark_url(@user.name), @user)

  # POST /users/{username}/bookmarks
  def create
    bookmark = Bookmark.find_by_user_id_and_uri(params[:bookmark][:user_id], 
    if bookmark
      # This user has already bookmarked this URI. They should be
      # using PUT instead.
      headers['Location'] = bookmark_url(@user.name, bookmark.uri)
      render :nothing => true, :status => "409 Conflict"
      # Enforce default values for 'timestamp' and 'public'
      params[:bookmark][:timestamp] ||= Time.now
      params[:bookmark][:public] ||= "1"

      # Create the bookmark in the database.
      bookmark = Bookmark.new(params[:bookmark])
      if bookmark.save
        # Add tags.
        bookmark.tag_with(params[:taglist]) if params[:taglist]

        # Send a 201 response code that points to the location of the
        # new bookmark.
        headers['Location'] = bookmark_url(@user.name, bookmark.uri)
        render :nothing => true, :status => "201 Created"
        render :xml => bookmark.errors.to_xml, :status => "400 Bad Request"

  # PUT /users/{username}/bookmarks/{URI-MD5}
  def update
    bookmark = Bookmark.find_by_user_id_and_uri_hash(@user.id, params[:id])
    if_found bookmark do
      old_uri = bookmark.uri
      if old_uri != params[:bookmark][:uri] && 
          Bookmark.find_by_user_id_and_uri(@user.id, params[:bookmark][:uri])
        # The user is trying to change the URI of this bookmark to a
        # URI that they've already bookmarked. Conflict!
        render :nothing => true, :status => "409 Conflict"
        # Update the bookmark's row in the database.
        if bookmark.update_attributes(params[:bookmark])
          # Change the bookmark's tags.
          bookmark.tag_with(params[:taglist]) if params[:taglist]
          if bookmark.uri != old_uri
            # The bookmark changed URIs. Send the new URI.
            headers['Location'] = bookmark_url(@user.name, bookmark.uri)
            render :nothing => true, :status => "301 Moved Permanently"
            # The bookmark stayed where it was.
            render :nothing => true, :status => "200 OK"
          render :xml => bookmark.errors.to_xml, :status => "400 Bad Request"

  # GET /users/{username}/bookmarks/{uri}
  def show
    # Look up the requested bookmark, and render it as a "list"
    # containing only one item.
    bookmark = Bookmark.find_by_user_id_and_uri_hash(@user.id, params[:id])
    if_found(bookmark) do
                       "#{@user.name} bookmarked #{bookmark.uri}",
                       bookmark_url(@user.name, bookmark.uri_hash),

  # DELETE /users/{username}/bookmarks/{uri}
  def destroy
    bookmark = Bookmark.find_by_user_id_and_uri_hash(@user.id, params[:id])
    if_found bookmark do
      render :nothing => true, :status => "200 OK"

The TagsController

This controller exposes a user’s tag vocabulary, and the list of bookmarks she’s filed under each tag (see Example 7-19). There are two twists here: the tag vocabulary and the “tag rename” operation.

The tag vocabulary is simply a list of a user’s tags, along with a count of how many times this user used the tag. I can get this data fairly easily with ActiveResource, and format it as a representation with to_xml, but what about security? If you tag two public and six private bookmarks with “ruby,” when I look at your tag vocabulary, I should only see “ruby” used twice. If you tag a bunch of private bookmarks with “possible-acquisition,” I shouldn’t see “possible-acquisition” in your vocabulary at all. On the other hand, when you’re viewing your own bookmarks, you should be able to see the complete totals. I use some custom SQL to count only the public tags when appropriate. Incidentally, this is another resource that doesn’t support conditional GET.

Example 7-19. app/controllers/tags_controller.rb

class TagsController < ApplicationController
  before_filter :must_specify_user
  before_filter :must_authenticate, :only => [:update]

  # GET /users/{username}/tags
  def index    
    # A user can see all of their own tags, but only tags used
    # in someone else's public bookmarks.
    if @user_is_viewing_themselves
      tag_restriction = ''
      tag_restriction = " AND bookmarks.public='1'"
    sql = ["SELECT tags.*, COUNT(bookmarks.id) as count" +
           " FROM tags, bookmarks, taggings" +
           " WHERE taggings.taggable_type = 'Bookmark'" +
           " AND tags.id = taggings.tag_id" +
           " AND taggings.taggable_id = bookmarks.id" + 
           " AND bookmarks.user_id = ?" + tag_restriction +
           " GROUP BY tags.name", @user.id]
    # Find a bunch of ActiveRecord Tag objects using custom SQL.
    tags = Tag.find_by_sql(sql)

    # Convert the Tag objects to an XML document.
    render :xml => tags.to_xml(:except => [:id])

I said earlier I’d handle the “tag rename” operation with HTTP PUT. This makes sense since a rename is a change of state for an existing resource. The difference here is that this resource doesn’t correspond to a specific ActiveRecord object. There’s an ActiveRecord Tag object for every tag, but that object represents everyone’s use of a tag. This controller doesn’t expose tags, per se: it exposes a particular user’s tag vocabulary.

Renaming a Tag object would rename it for everybody on the site. But if you rename “good” to “bad,” then that should only affect your bookmarks. Any bookmarks I’ve tagged as “good” should stay “good.” The client is not changing the tag, just one user’s use of the tag.

From a RESTful perspective none of this matters. A resource’s state is changed with PUT, and that’s that. But the implementation is a bit tricky. What I need to do is find all the client’s bookmarks tagged with the given tag, strip off the old tag, and stick the new tag on. Unlike with users or bookmarks, I won’t be sending a 409 (“Conflict”) response code if the user renames an old tag to a tag that already exists. I’ll just merge the old tag into the new one (see Example 7-20).

Example 7-20. app/controllers/tags_controller.rb continued

  # PUT /users/{username}/tags/{tag} 
  # This PUT handler is a little trickier than others, because we
  # can't just rename a tag site-wide. Other users might be using the
  # same tag.  We need to find every bookmark where this user uses the
  # tag, strip the "old" name, and add the "new" name on.
  def update
    old_name = params[:id]
    new_name = params[:tag][:name] if params[:tag]
    if new_name
      # Find all this user's bookmarks tagged with the old name
      to_change = Bookmark.find(["bookmarks.user_id = ?", @user.id], 
      # For each such bookmark...
      to_change.each do |bookmark|
        # Find its tags.
        tags = bookmark.tags.collect { |tag| tag.name }
        # Remove the old name.
        # Add the new name.
        tags << new_name
        # Assign the new set of tags to the bookmark.
        bookmark.tag_with tags.uniq
      headers['Location'] = tag_url(@user.name, new_name)
      status = "301 Moved Permanently"
    render :nothing => true, :status => status || "200 OK"

  # GET /users/{username}/tags/{tag}
  def show
    # Show bookmarks that belong to this user and are tagged
    # with the given tag.
    tag = params[:id]
    show_bookmarks(["bookmarks.user_id = ?", @user.id], 
                   "#{@user.name}'s bookmarks tagged with '#{tag}'",
                   tag_url(@user.name, tag), @user, tag)

The Lesser Controllers

Every other controller in my application is read-only. This means it implements at most index and show. Hopefully by now you get the idea behind the controllers and their action methods, so I’ll cover the rest of the controllers briefly.

The CalendarController

This resource, a user’s posting history, is something like the one exposed by TagsController#show. I’m getting some counts from the database and rendering them as XML. This document doesn’t directly correspond to any ActiveRecord object, or list of such objects; it’s just a summary. As before, I need to be sure not to include other peoples’ private bookmarks in the count.

The main body of code goes into the Bookmark.calendar method, defined in the Bookmark model class (see The Bookmark Model). The controller just renders the data.

ActiveRecord’s to_xml doesn’t do a good job on this particular data structure, so I’ve implemented my own view function: calendar_to_xml (see Example 7-21). It uses Builder::XmlMarkup (a Ruby utility that comes with Rails) to generate an XML document without writing much code.

Example 7-21. app/controllers/calendar_controller.rb

class CalendarController < ApplicationController
  before_filter :must_specify_user

  # GET /users/{username}/calendar
  def index
    calendar = Bookmark.calendar(@user.id, @user_is_viewing_themselves)
    render :xml => calendar_to_xml(calendar)

  # GET /users/{username}/calendar/{tag}
  def show    
    tag = params[:id]
    calendar = Bookmark.calendar(@user.id, @user_is_viewing_themselves,
    render :xml => calendar_to_xml(calendar, tag)


  # Build an XML document out of the data structure returned by the
  # Bookmark.calendar method.
  def calendar_to_xml(days, tag=nil)
    xml = Builder::XmlMarkup.new(:indent => 2)
    # Build a 'calendar' element.
    xml.calendar(:tag => tag) do
      # For every day in the data structure...
      days.each do |day|
        # ...add a "day" element to the document
        xml.day(:date => day.date, :count => day.count)

The RecentController

The controller in Example 7-22 shows recently posted bookmarks. Its actions are just thin wrappers around the show_bookmarks method defined in application.rb.

Example 7-22. app/controllers/recent_controller.rb

# recent_controller.rb
class RecentController < ApplicationController

  # GET /recent
  def index
    # Take bookmarks from the database without any special conditions.
    # They'll be ordered with the most recently-posted first.
    show_bookmarks(nil, "Recent bookmarks", recent_url)

  # GET /recent/{tag}
  def show
    # The same as above, but only fetch bookmarks tagged with a
    # certain tag.
    tag = params[:id]
    show_bookmarks(nil, "Recent bookmarks tagged with '#{tag}'", 
                   recent_url(tag), nil, tag)

The UrisController

The controller in Example 7-23 shows what the site’s users think of a particular URI. It shows a list of bookmarks, all for the same URI but from different people and with different tags and descriptions.

Example 7-23. app/controllers/uris_controller.rb

# uris_controller.rb
class UrisController < ApplicationController
  # GET /uris/{URI-MD5}
  def show
    # Fetch all the visible Bookmark objects that correspond to
    # different people bookmarking this URI.
    uri_hash = params[:id]
    sql = ["SELECT bookmarks.*, users.name as user from bookmarks, users" +
           " WHERE users.id = bookmarks.user_id AND bookmarks.uri_hash = ?",
    Bookmark.only_visible_to!(sql, @authenticated_user)
    bookmarks = Bookmark.find_by_sql(sql)

    if_found(bookmarks) do

      # Render the list of Bookmark objects as XML or a syndication feed,
      # depending on what the client requested.
      uri = bookmarks[0].uri
      render_bookmarks(bookmarks, "Users who've bookmarked #{uri}",
                       uri_url(uri_hash), nil)

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required