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

Model Code

Those are the controllers. I’ve also got three “model” classes, corresponding to my three main database tables: User, Bookmark, and Tag. The Tag class is defined entirely through the acts_as_taggable Rails plugin, so I’ve only got to define User and Bookmark.

The model classes define validation rules for the database fields. If a client sends bad data (such as trying to create a user without specifying a name), the appropriate validation rule is triggered and the controller method sends the client a response code of 400 (“Bad Request”). The same model classes could be used in a conventional web application, or a GUI application. The validation errors would be displayed differently, but the same rules would always apply.

The model classes also define a few methods which work against the database. These methods are used by the controllers.

The User Model

This is the simpler of the two models (see Example 7-24). It has some validation rules, a one-to-many relationship with Bookmark objects, and a few methods (called by the controllers) for validating passwords.

Example 7-24. app/models/user.rb

class User < ActiveRecord::Base
  # A user has many bookmarks. When the user is destroyed,
  # all their bookmarks should also be destroyed.
  has_many :bookmarks, :dependent => :destroy

  # A user must have a unique username.
  validates_uniqueness_of :name

  # A user must have a username, full name, and email.
  validates_presence_of :name, :full_name, :email

  # Make sure passwords are never stored in plaintext, by running them
  # through a one-way hash as soon as possible.
  def password=(password)
    super(User.hashed(password))
  end

  # Given a username and password, returns a User object if the
  # password matches the hashed one on file. Otherwise, returns nil.
  def self.authenticated_user(username, pass)
    user = find_by_name(username)
    if user
      user = nil unless hashed(pass) == user.password
    end
    return user
  end

  # Performs a one-way hash of some data.
  def self.hashed(password)
    Digest::SHA1.new(password).to_s
  end
end

The Bookmark Model

This is a more complicated model (see Example 7-25). First, let’s define the relationships between Bookmark and the other model classes, along with some validation rules and a rule for generating the MD5 hash of a URI. We have to keep this information because the MD5 calculation only works in one direction. If a client requests /v1/uris/55020a5384313579a5f11e75c1818b89, we can’t reverse the MD5 calculation. We need to be able to look up a URI by its MD5 hash.

Example 7-25. app/models/bookmark.rb

class Bookmark < ActiveRecord::Base
  # Every bookmark belongs to some user.
  belongs_to :user

  # A bookmark can have tags. The relationships between bookmarks and
  # tags are managed by the acts_as_taggable plugin.
  acts_as_taggable

  # A bookmark must have an associated user ID, a URI, a short
  # description, and a timestamp.
  validates_presence_of :user_id, :uri, :short_description, :timestamp

  # The URI hash should never be changed directly: only when the URI
  # changes.
  attr_protected :uri_hash

  # And.. here's the code to update the URI hash when the URI changes.
  def uri=(new_uri)
    super
    self.uri_hash = Digest::MD5.hexdigest(new_uri)
  end

  # This method is triggered by Bookmark.new and by
  # Bookmark#update_attributes. It replaces a bookmark's current set
  # of tags with a new set.
  def tag_with(tags)
    Tag.transaction do
      taggings.destroy_all
      tags.each { |name| Tag.find_or_create_by_name(name).on(self) }
    end
  end

That last method makes it possible to associate tags with bookmarks. The acts_as_taggable plugin allows me to do basic queries like “what bookmarks are tagged with ‘ruby’?” Unfortunately, I usually need slightly more complex queries, like “what bookmarks belonging to leonardr are tagged with ‘ruby’?”, so I can’t use the plugin’s find_tagged_with method. I need to define my own method that attaches a tag restriction to some preexisting restriction like “bookmarks belonging to leonardr.”

This custom_find method is the workhorse of the whole service, since it’s called by the ApplicationController#show_bookmarks method, which is called by many of the RESTful Rails actions (see Example 7-26).

Example 7-26. app/models/bookmark.rb continued

  # This method finds bookmarks, possibly ones tagged with a
  # particular tag.
  def self.custom_find(conditions, tag=nil, limit=nil)
    if tag       
      # When a tag restriction is specified, we have to find bookmarks
      # the hard way: by constructing a SQL query that matches only
      # bookmarks tagged with the right tag.
      sql = ["SELECT bookmarks.* FROM bookmarks, tags, taggings" +
             " WHERE taggings.taggable_type = 'Bookmark'" +
             " AND bookmarks.id = taggings.taggable_id" +
             " AND taggings.tag_id = tags.id AND tags.name = ?",
             tag]
      if conditions
        sql[0] << " AND " << conditions[0]
        sql += conditions[1..conditions.size]
      end
      sql[0] << " ORDER BY bookmarks.timestamp DESC"
      sql[0] << " LIMIT " << limit.to_i.to_s if limit
      bookmarks = find_by_sql(sql)
    else
      # Without a tag restriction, we can find bookmarks the easy way:
      # with the superclass find() implementation.
      bookmarks = find(:all, {:conditions => conditions, :limit => limit,
                              :order => 'timestamp DESC'})
    end    
    return bookmarks
  end

There are two more database-related methods (see Example 7-27). The Bookmark.only_visible_to! method manipulates a set of ActiveRecord conditions so that they only apply to bookmarks the given user can see. The Bookmark.calendar method groups a user’s bookmarks by the date they were posted. This implementation may not work for you, since it uses a SQL function (DATE) that’s not available for all databases.

Example 7-27. app/models/bookmark.rb concluded

  # Restricts a bookmark query so that it only finds bookmarks visible
  # to the given user. This means public bookmarks, and the given
  # user's private bookmarks.
  def self.only_visible_to!(conditions, user)
    # The first element in the "conditions" array is a SQL WHERE
    # clause with variable substitutions. The subsequent elements are
    # the variables whose values will be substituted. For instance,
    # if "conditions" starts out empty: [""]...

    conditions[0] << " AND " unless conditions[0].empty?
    conditions[0] << "(public='1'"
    if user
      conditions[0] << " OR user_id=?"
      conditions << user.id
    end
    conditions[0] << ")"

    # ...its value might now be ["(public='1' or user_id=?)", 55].
    # ActiveRecord knows how to turn this into the SQL WHERE clause
    # "(public='1' or user_id=55)".
  end 

  # This method retrieves data for the CalendarController. It uses the
  # SQL DATE() function to group together entries made on a particular
  # day.
  def self.calendar(user_id, viewed_by_owner, tag=nil)    
    if tag
      tag_from = ", tags, taggings"
      tag_where = "AND taggings.taggable_type = 'Bookmark'" +
        " AND bookmarks.id = taggings.taggable_id" +
        " AND taggings.tag_id = tags.id AND tags.name = ?"
    end

    # Unless a user is viewing their own calendar, only count public
    # bookmarks.
    public_where = viewed_by_owner ? "" : "AND public='1'"

    sql = ["SELECT date(timestamp) AS date, count(bookmarks.id) AS count" +
           " FROM bookmarks#{tag_from} " +
           " WHERE user_id=? #{tag_where} #{public_where} " +
           " GROUP BY date(timestamp)", user_id]
    sql << tag if tag

    # This will return a list of rather bizarre ActiveRecord objects,
    # which CalendarController knows how to turn into an XML document.
    find_by_sql(sql)
  end
end

Now you should be ready to start your Rails server in a console window, and start using the web service.

$ script/server

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