15 RESTful Web Services
Many web applications today offer an API that allows others to extend the functionality of the application. An API is a set of exposed functions that is meant to allow third parties to reuse elements of the application. There is a number of sites that catalog the available APIs, such as ProgrammableWeb (see
http://www.programmableweb.com/). An example of a site that has combined the GoogleMaps and Flickr APIs is FlickrVision.com. FlickrVision allows users to visualize where in the world recent photos have been taken by combining the geolocation information embedded in the photos and the mapping system of GoogleMaps. This is just one example of an API mashup, and there are countless other examples.
15.1 Some Background on REST
Before we dive into the details of building a RESTful API with Lift, let’s start by discussing a little about REST and the protocol that it sits atop: HTTP. If you’re already familiar with REST and HTTP, feel free to skip to the implementation in Section
15.2↓.
15.1.1 A Little Bit about HTTP
As we build our web service, it will to be helpful to know a few things about HTTP requests and responses. If you’re comfortable with the Request-Response cycle then feel free to jump to Section
15.1.2↓ to get down to business.
A simplification of how the web works is that clients, typically web browsers, send HTTP Requests to servers, which respond with HTTP Responses. Let’s take a look at an exchange between a client and a server.
We’re going to send a GET request to the URI
http://demo.liftweb.net/ using the
cURL utility. We’ll enable dumping the HTTP protocol header information so that you can see all of the information associated with the request and response. The cURL utility sends the output shown in Listing
15.1.1↓:
$ curl -v http://demo.liftweb.net/
* About to connect() to demo.liftweb.net port 80 (#0)
* Trying 64.27.11.183... connected
* Connected to demo.liftweb.net (64.27.11.183) port 80 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.19.0 (i386-apple-darwin9.5.0) libcurl/7.19.0 zlib/1.2.3
> Host: demo.liftweb.net
> Accept: */*
And gets the corresponding response, shown in Listing
15.1.1↓, from the server:
< HTTP/1.1 200 OK
< Server: nginx/0.6.32
< Date: Tue, 24 Mar 2009 20:52:55 GMT
< Content-Type: text/html
< Connection: keep-alive
< Expires: Mon, 26 Jul 1997 05:00:00 GMT
< Set-Cookie: JSESSIONID=5zrn24obipm5;Path=/
< Content-Length: 8431
< Cache-Control: no-cache; private; no-store;
must-revalidate; max-stale=0; post-check=0; pre-check=0; max-age=0
< Pragma: no-cache
< X-Lift-Version: 0.11-SNAPSHOT
<
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns:lift="http://liftweb.net" xmlns="http://www.w3.org/1999/xhtml">
<head>....
This seems pretty straightforward: we ask for a resource, and the server returns it to us. Take a look at the HTTP request. We’d like to point out the method called, in this case a “GET”, and the URI, which is “http://demo.liftweb.net/”. Method calls and addresses are what make the web work. You can think of the web as a series of method calls on varying resources, where the URI (Uniform Resource Identifier) identifies the resource upon which the method will be called.
Methods are defined as part of the HTTP standard, and we’ll use them in our API. In addition to GET, the other HTTP methods are POST, DELETE, PUT, HEAD, and OPTIONS. You may also see methods referred to as actions or verbs. In this chapter, we will focus on using GET and PUT for our API.
As do Requests, Responses come with a few important pieces of information. Of note are the Response Code and the Entity Body. In the above example, the Response Code is “200 OK” and the Entity Body is the HTML content of the webpage, which is shown as the last two lines starting with “<!DOCTYPE.” We’ve truncated the HTML content here to save space.
This was a quick overview of HTTP, but if you’d like to learn more, take a look at the protocol definition found at. We wanted to point out a few of the interesting parts of the cycle before we got into building a REST API.
15.1.2 Defining REST
Roy Fielding defined REST in his dissertation and defined the main tenet of the architecture to be a uniform interface to resources. “Resources” refers to pieces of information that are named and have representations. Examples include an image, a Twitter status, or a timely item such as a stock quote or the current temperature. The uniform interface is supported by a set of constraints that include the following:
-
Statelessness of communication: This is built on top of HTTP, which is also stateless.
-
Client-server–style interaction: Again, just as the Web consists of browsers talking to servers, REST discusses machines or applications talking to servers in the same way.
-
Support for caching: REST uses the caching headers of HTTP to support the caching of resources.
These features are shared by both the web and by RESTful services. REST adds additional constraints regarding interacting with resources:
-
Naming: As we mentioned, a resource must be identified, and this is done using URLs.
-
Descriptive actions: Using the HTTP actions, GET, PUT, and DELETE makes it obvious what action is being performed on the resource.
-
URL addressability: URLs should allow for the addressing of representation of a resource.
Fielding’s goal was to define a method that allowed machine-to-machine communication to mimic that of browser-to-server communication and to take advantage of HTTP as the underlying protocol.
15.1.3 Comparing XML-RPC to REST Architectures
What, then, is the difference between a RESTful architecture and a traditional RPC architecture?
An RPC application follows a more traditional software development pattern. It ignores most of the features offered by HTTP, such as the HTTP methods. Instead, the scoping and data to be used by the call are contained in the body of a POST request. XML-RPC works similarly to the web for getting resources, but breaks from the HTTP model for everything else by overloading the POST request. You will often see the term SOAP when referring to an XML-RPC setup, because SOAP permits the developer to define the action and the resource in the body of the request and ignore the HTTP methods.
RESTful architectures embrace HTTP. We’re using the web; we may as well take advantage of it.
15.2 A Simple API for PocketChange
We’re going to start with a simple example, so we’ll only touch on some of the more complex steps of building a web service, such as authentication
↓↓ and authorization. If you would like to see the code involved in performing authentication and authorization for our REST API, see Section
9.9↑. For the purposes of this example, we’re going to model two calls to the server: a GET request that responds with the details of an expense, and a PUT to add a new expense.The URLs will be:
Note that a URL (Uniform Resource Locator) is a type of URI in which the URI also serves to locate the resource on the web. A URN (Uniform Resource Name) is another type of URI that provides a unique name to a resource without specifying an actual location, though it may look a lot like a URL. For more information on the distinctions among URIs, see
http://en.wikipedia.org/wiki/Uniform_Resource_Name.
We would like the REST API to support both XML and JSON for this data. Additionally, we would like to support an Atom feed on an account so that people can track expenses as they’re added. The URL for the Atom feed will be a GET of the form:
http://www.pocketchangeapp.com/api/account/<account id>
In the next few sections we’ll show how you can easily add support for these methods and formats using Lift.
15.3 Adding REST Helper Methods to our Entities
In order to simplify our REST handler code, we would like to add some helper methods for our
Expense entity to support generation of both XML and JSON for our consumers. We’ll add these to a new
RestFormatters object inside the
src/main/scala/com/pocketchangeapp/RestFormatters.scala source file. First, we add some common functionality in Listing
15.3↓ by adding several helper methods for computing REST header values.
Common Expense REST Helpers
/* The REST timestamp format. Not threadsafe, so we create
* a new one each time. */
def timestamp = new SimpleDateFormat("yyyy-MM-dd’T’HH:mm:ss’Z’")
// A simple helper to generate the REST ID of an Expense
def restId (e : Expense) =
"http://www.pocketchangeapp.com/api/expense/" + e.id
// A simple helper to generate the REST timestamp of an Expense
def restTimestamp (e : Expense) : String =
timestamp.format(e.dateOf.is)
Listing
15.3↓ shows a helper method for generating a proper JSON representation of a given
Expense using the Lift JSON DSL. Although
Expense is a
Mapper entity, we don’t use the
Expense.asJs↓ method inherited from
Mapper because we want to better control the format.
Expense Entity JSON Formatters
/**
* Generates the JSON REST representation of an Expense
*/
def toJSON (e : Expense) : JValue = {
import net.liftweb.json.JsonDSL._
import net.liftweb.json.JsonAST._
("expense" ->
("id" -> restId(e)) ~
("date" -> restTimestamp(e)) ~
("description" -> e.description.is) ~
("accountname" -> e.accountName) ~
("accountid" -> e.account.obj.open_!.id.is) ~
("amount" -> e.amount.is.toString) ~
("tags" -> e.tags.map(_.name.is).mkString(",")))
}
Finally, Listing
15.3↓ shows the
toXML method, which will generate properly formatted XML for a given
Expense. Like
toJSON, we don’t use the
Expense.toXml↓ method because we want more control over the generated format. Instead, we simply convert the result of
toJSON into XML using the
net.liftweb.json.Xml helper object.
Expense Entity XML REST Formatter
import net.liftweb.json.Xml
/**
* Generates the XML REST representation of an Expense
*/
def toXML (e : Expense) : Node = Xml.toXml(toJSON(e)).first
15.4 Multiple Approaches to REST Handling
As Lift has evolved, two main approaches have emerged that allow you to perform RESTful operations. In Lift 1.0 and up, you can add custom dispatch (Section
3.8 on page 1↑) on your API URLs to call custom handlers for your REST data. In Lift 2.0, the new
net.liftweb.http.rest.RestHelper was introduced that vastly simplifies not only the dispatch for given operations, but also assists with conversion of requests and responses to both XML and JSON. Because custom dispatch is still very much a first-class feature of Lift we will cover both approaches here.
Before we get into the details of each method, there are two last helpers we’d like to define. Listing
15.4↓ shows an
unapply method that we add to our
Expense MetaMapper so that we can use
Expense as an extractor in pattern matching. In this code we not only attempt to match by using a provided
String as the
Expense’s primary key, but we also compute whether the
Expense is in a public account. This assists us in determining authorization for viewing a given
Expense.
Adding an Extractor for Expense
import net.liftweb.util.ControlHelpers.tryo
/**
* Define an extractor that can be used to locate an Expense based
* on its ID. Returns a tuple of the Expense and whether the
* Expense’s account is public.
*/
def unapply (id : String) : Option[(Expense,Boolean)] = tryo {
find(By(Expense.id, id.toLong)).map { expense =>
(expense,
expense.account.obj.open_!.is_public.is)
}.toOption
} openOr None
Similarly, Listing
15.4↓ shows an extractor on the
Account MetaMapper that matches an
Account based on its primary key.
Adding an Extractor for Account
import net.liftweb.util.Helpers.tryo
/**
* Define an extractor that can be used to locate an Account based
* on its ID.
*/
def unapply (id : String) : Option[Account] = tryo {
find(By(Account.id, id.toLong)).toOption
} openOr None
15.4.1 Using Custom Dispatch
Now that we’ve discussed our design, let’s see the code that will handle the routing. In the package
com.pocketchangeapp.api, we have an object named
DispatchRestAPI, which we’ve defined in
src/main/scala/com/pocketchangeapp/api/RestAPI.scala. In
DispatchRestAPI, we define a custom dispatch function to pattern match on the request and delegate to a handler method. The custom dispatch function is shown in Listing
15.4.1↓. You can see that we use our extractors in the matching for both
Expenses and
Accounts. We’ll cover the processing of PUTs in Section
15.5↓, and the Atom processing in Section
15.7↓.
// Import our methods for converting things around
import RestFormatters._
def dispatch: LiftRules.DispatchPF = {
// Define our getters first
case Req(List("api", "expense", Expense(expense,_)), _, GetRequest) =>
() => nodeSeqToResponse(toXML(expense)) // default to XML
case Req(List("api", "expense", Expense(expense,_), "xml"), _, GetRequest) =>
() => nodeSeqToResponse(toXML(expense))
case Req(List("api", "expense", Expense(expense,_), "json"), _, GetRequest) =>
() => JsonResponse(toJSON(expense))
case Req(List("api", "account", Account(account)), _, GetRequest) =>
() => AtomResponse(toAtom(account))
// Define the PUT handler for both XML and JSON MIME types
case request @ Req(List("api", "account", Account(account)), _, PutRequest)
if request.xml_? =>
() => addExpense(fromXML(request.xml,account),
account,
result => CreatedResponse(toXML(result), "text/xml"))
case request @ Req(List("api", "account", Account(account)), _, PutRequest)
if request.json_? =>
() => addExpense(fromJSON(request.body,account),
account,
result => JsonResponse(toJSONExp(result), Nil, Nil, 201))
// Invalid API request - route to our error handler
case Req("api" :: x :: Nil, "", _) =>
() => BadResponse() // Everything else fails
}
Our
DispatchRestAPI object mixes in the
net.liftweb.http.rest.XMLApiHelper trait, which includes several implicit conversions to simplify writing our REST API. Remember that
LiftRules.DispatchPF must return a function
() ⇒ Box[LiftResponse] (Section
3.8 on page 1↑), so we’re using the implicit
putResponseInBox as well as explicitly calling
nodeSeqToResponse to convert our API return values into the proper format.
The server will now service GET requests with the appropriate formatter function and will handle PUT requests with the addExpense method (which we’ll define later in this chapter).
We hook our new dispatch function into LiftRules by adding the code shown in Listing
15.4.1↓ to our
Boot.boot method.
LiftRules.dispatch.prepend(DispatchRestAPI.dispatch)
15.4.2 Using the RestHelper Trait
New in Lift 2.0 is the net.liftweb.http.rest.RestHelper trait. This trait simplifies the creation of REST APIs that support both XML and JSON. For our example, we’ll define the RestHelperAPI object in our RestAPI.scala source file.
Before we get into the details of actual processing with RestHelper, we want to point out some useful parts of its API. First, RestHelper provides a number of built-in extractors for matching not only what HTTP verb a given request uses, but also the format of the request (JSON or XML). These extractors are:
-
Get, JsonGet, XmlGet - matches a raw GET, or a GET of the specified format
-
Post, JsonPost, XmlPost - matches a raw POST, or a POST of the specified format
-
Put, JsonPut, XmlPut - matches a raw PUT, or a PUT of the specified format
-
Delete - matches a DELETE request
-
JsonReq - matches a request with the Accept header containing “application/json”, or whose Accept header contains “*/*” and whose path suffix is “json”
-
XmlReq - matches a request with the Accept header containing “text/xml”, or whose Accept header contains “*/*” and whose path suffix is “xml”
We’ll demonstrate in the following sections how to use these extractors. Note that you can add additional rules for the
JsonReq and
XmlReq extractors by overriding the
RestHelper.suplimentalJsonResponse_? and
suplimentalXmlResponse_? (yes, those are spelled incorrectly) methods to perform additional tests on the request. For example, Listing
15.4.2↓ shows how we can use the existence of a given header to determine whether a request is XML or JSON.
Using a Cookie to Determine the Request Type
override def suplimentalJsonResponse_? (in : Req) =
in.header("This-Is-A-JSON-Request").isDefined
override def suplimentalXmlResponse_? (in : Req) =
in.header("This-Is-A-XML-Request").isDefined
One important difference between RestHelper and our DispatchRestAPI examples that we want to point out is that RestHelper determines whether a request is XML or JSON based on the Accept header and/or the suffix of the path (e.g. /api/expense/1.xml), whereas our DispatchRestAPI used the last component of the path (/api/expense/1/xml). Either approach is valid, just be aware if you’re copying this example code.
Next, like the XMLApiHelper trait, RestHelper provides a number of implicit conversions to LiftResponse from a variety of inputs. We’re not going to cover these directly here, but we’ll point out where we use them in this section.
Similar to our DispatchRestAPI handler, we need to define a set of patterns that we can match against. Unlike DispatchRestAPI, however, RestHelper defines four PartialFunction methods where we can add our patterns: serve, serveJx, serveJxa and serveType. These functions provide increasing automation (and control) over what gets served when the request matches a pattern. We won’t be covering serveType here, since it’s essentially the generalized version that serve, serveJx and serveJxa use behind the scenes.
15.4.2.1 The serve Method
Let’s start with the
serve method. This method essentially corresponds one-to-one with our
DispatchRestAPI.dispatch method. Listing
15.4.2.1↓ shows how we could handle Atom requests, as well as requests that don’t specify a format, using
RestHelper. Note our use of the
RestHelper extractors to match the HTTP Verb being used. Also note that we’re using an implicit conversion from a Box[T] to a Box[LiftResponse] when an implicit function is in scope that can convert T into a LiftResponse. In our example,
Full(toXML(expense)) is equivalent to
boxToResp(Full(toXML(expense)))(nodeToResp). Finally, the serve method can be invoked multiple times and the
PartialFunctions will be chained together.
// Service Atom and requests that don’t request a specific format
serve {
// Default to XML
case Get(List("api", "expense", Expense(expense,_)), _) =>
() => Full(toXML(expense))
case Get(List("api", "account", Account(account)), _) =>
() => Full(AtomResponse(toAtom(account)))
}
We use similar calls to hook our PUT handlers, shown in Listing
15.4.2.1↓.
Using serve to handle PUTs
// Hook our PUT handlers
import DispatchRestAPI.addExpense
serve {
case XmlPut(List("api", "account", Account(account)), (body, request)) =>
() => Full(addExpense(fromXML(Full(body),account),
account,
result => CreatedResponse(toXML(result), "text/xml")))
case JsonPut(List("api", "account", Account(account)), (_, request)) =>
() => Full(addExpense(fromJSON(request.body,account),
account,
result => JsonResponse(toJSON(result), Nil, Nil, 201)))
}
15.4.2.2 The serveJx Method
Like the serve method,
serveJx performs pattern matching on the request. However,
serveJx allows you to specify a conversion function that matches against the requested format
(
net.liftweb.http.rest.JsonSelect or
net.liftweb.http.rest.XmlSelect) and perform your conversion there. Then, all you need to do is match once against a given path and
serveJx will utilize your conversion function to return the proper result. Listing
15.4.2.2↓ shows how we can use a new implicit conversion to handle our format-specific GETs. The single match in our
serveJx call replaces two lines in our
DispatchRestAPI.dispatch method.
// Define an implicit conversion from an Expense to XML or JSON
import net.liftweb.http.rest.{JsonSelect,XmlSelect}
implicit def expenseToRestResponse : JxCvtPF[Expense] = {
case (JsonSelect, e, _) => toJSON(e)
case (XmlSelect, e, _) => toXML(e)
}
serveJx {
case Get(List("api", "expense", Expense(expense,_)), _) => Full(expense)
}
In addition to providing your own conversion function,
serveJx can utilize the
RestHelper autoconversion functionality. To use this, simply use the
auto method to wrap whatever you want to return. Listing
15.4.2.2↓ shows an example of returning a contrived data object with
auto.
Using auto to Convert Return Values
// Just an example of autoconversion
serveJx {
case Get(List("api", "greet", name),_) =>
auto(Map("greeting" ->
Map("who" -> name,
"what" -> ("Hello at " + new java.util.Date))))
}
The conversion is actually performed with the
net.liftweb.json.Extraction object
↓↓↓, so you can autoconvert anything that
Extraction can handle. This includes:
-
Primitives
-
Maps
-
Arrays
-
Collections
-
Options
-
Case classes
15.4.2.3 The serveJxa Method
The serveJxa method is basically the same as the serve and serveJx methods, except that anything that is returned will be automatically converted to JSON via the
net.liftweb.json.Extraction.decompose method.
15.5 Processing Expense PUTs
Now that we’re handling the API calls, we’ll need to write the code to process and respond to requests. The first thing we need to do is deserialize the Expense from the either an XML or JSON request.
In PocketChange our use of
BigDecimal values to represent currency amounts means that we can’t simply use the lift-json deserialization support (Section
C.10 on page 1↓). While lift-json is very good and would make this much simpler, it parses decimal values as doubles which can lead to rounding and precision issues when working with decimal values. Instead, we will need to write our own conversion functions.
To simplify error handling, we break this processing up into two format-specific methods that convert to a
Map representation of the data, and another method that converts the intermediate
Map/
List into an
Expense. Listing
15.5↓ shows the
fromXML method in the
RestFormatters object. This method performs some basic validation to make sure we have the required parameters, but otherwise doesn’t validate the values of those parameters. Note that we provide the
Account to
fromXML so that we can resolve tag names in the
fromMap method (which we’ll cover momentarily).
Deserializing XML to an Expense
def fromXML (rootNode : Box[Elem], account : Account) : Box[Expense] =
rootNode match {
case Full(<expense>{parameters @ _*}</expense>) => {
var data = Map[String,String]()
for(parameter <- parameters) {
parameter match {
case <date>{date}</date> => data += "date" -> date.text
case <description>{description}</description> =>
data += "description" -> description.text
case <amount>{amount}</amount> => data += "amount" -> amount.text
case <tags>{ tags }</tags> => data += "tags" -> tags.text
case _ => // Ignore (could be whitespace)
}
}
fromMap(data, account)
}
case other => Failure("Missing root expense element")
}
Similarly, Listing
15.5↓ shows our
fromJSON method.
Deserializing JSON to an Expense
def fromJSON (obj : Box[Array[Byte]], account : Account) : Box[Expense] =
obj match {
case Full(rawBytes) => {
// We use the Scala util JSON parser here because we want to avoid parsing
// numeric values into doubles. We’ll just leave them as Strings
import scala.util.parsing.json.JSON
JSON.perThreadNumberParser = { in : String => in }
val contents = new String(rawBytes, "UTF-8")
JSON.parseFull(contents) match {
case Some(data : Map[String,Any]) => {
fromMap(data.mapElements(_.toString), account)
}
case other => Failure("Invalid JSON submitted: \"%s\"".format(contents))
}
}
case _ => Failure("Empty body submitted")
}
Finally, Listing
15.5↓ shows our
fromMap method, which takes the data parsed by
fromJSON and
fromXML and converts it into an actual expense.
Converting the Intermediate Data to an Expense
def fromMap (data : scala.collection.Map[String,String],
account : Account) : Box[Expense] = {
val expense = Expense.create
try {
val fieldParsers : List[(String, String => Expense)] =
("date", (date : String) => expense.dateOf(timestamp.parse(date))) ::
("description", (desc : String) => expense.description(desc)) ::
("amount", (amount : String) => expense.amount(BigDecimal(amount))) :: Nil
val missing = fieldParsers.flatMap {
field => // We invert the flatMap here to only give us missing values
if (data.get(field._1).map(field._2).isDefined) None else Some(field._1)
}
if (missing.isEmpty) {
expense.account(account)
data.get("tags").foreach {
tags => expense.tags(tags.split(",").map(Tag.byName(account.id.is,_)).toList)
}
Full(expense)
} else {
Failure(missing.mkString("Invalid expense. Missing: ", ",", ""))
}
} catch {
case pe : java.text.ParseException => Failure("Failed to parse date")
case nfe : java.lang.NumberFormatException =>
Failure("Failed to parse amount")
}
}
Now that we’ve converted the PUT data into an
Expense, we need to actually perform our logic and persist the submitted
Expense. Listing
15.5↓ shows our
addExpense method, which matches against the parsed
Expense and either runs validation if the parse succeeded, or returns an error response to the user if something failed. If validation fails, the user is similarly notified. The
success parameter is a function that can be used to generate the appropriate response based on the newly created Expense. This allows us to return the new Expense in the same format (JSON, XML) in which it was submitted (see the dispatch function, Listing
15.4.1↑).
Saving the submitted Expense
def addExpense(parsedExpense : Box[Expense],
account : Account,
success : Expense => LiftResponse): LiftResponse =
parsedExpense match {
case Full(expense) => {
val (entrySerial,entryBalance) =
Expense.getLastExpenseData(account, expense.dateOf)
expense.account(account).serialNumber(entrySerial + 1).
currentBalance(entryBalance + expense.amount)
expense.validate match {
case Nil => {
Expense.updateEntries(entrySerial + 1, expense.amount.is)
expense.save
account.balance(account.balance.is + expense.amount.is).save
success(expense)
}
case errors => {
val message = errors.mkString("Validation failed:", ",","")
logger.error(message)
ResponseWithReason(BadResponse(), message)
}
}
}
case Failure(msg, _, _) => {
logger.error(msg)
ResponseWithReason(BadResponse(), msg)
}
case error => {
logger.error("Parsed expense as : " + error)
BadResponse()
}
}
15.6 The Request and Response Cycles for Our API
At the beginning of this chapter, we showed you a request and response conversation for
http://demo.liftweb.net/. Let’s see what that looks like for a request to our API. Listing
15.6↓ shows an XML GET request for a given expense. Note that we’re not showing the HTTP Basic authentication setup, required by our authentication configuration (Section
9.9 on page 1↑).
Request and Response for XML GET
Request:
http://www.pocketchangeapp.com/api/expense/3 GET
Response:
<?xml version="1.0" encoding="UTF-8"?>
<expense>
<id>http://www.pocketchangeapp.com/api/expense/3</id>
<accountname>Test</accountname>
<accountid>1</accountid>
<date>2010-10-06T00:00:00Z</date>
<description>Receipt test</description>
<amount>12.00</amount>
<tags>test,receipt</tags>
</expense>
Listing
15.6↓ shows the same request in JSON format.
Request and Response for JSON GET
Request:
http://www.pocketchangeapp.com/api/expense/3/json GET
Response:
{"id":"http://www.pocketchangeapp.com/api/expense/3",
"date":"2010-10-06T00:00:00Z",
"description":"Receipt test",
"accountname":"Test",
"accountid":1,
"amount":"12.00",
"tags":"test,receipt"}
Listing
15.6↓ shows the output for a PUT conversation:
Request and Response for an XML PUT
Request:
http://www.pocketchangeapp.com/api/account/1 - PUT - addEntry(request) + XML Body
Request Body:
<expense>
<date>2010-07-05T14:22:00Z</date>
<description>Test</description>
<amount>12.41</amount>
<tags>test,api</tags>
</expense>
Response:
<?xml version="1.0" encoding="UTF-8"?>
<expense>
<id>http://www.pocketchangeapp.com/api/expense/10</id>
<accountname>Test</accountname>
<accountid>1</accountid>
<date>2010-07-05T14:22:00Z</date>
<description>Test</description>
<amount>12.41</amount>
<tags>api,test</tags>
</expense>
15.7 Extending the API to Return Atom Feeds
In addition to being able to fetch specific expenses using our API, it would be nice to be able to provide a feed of expenses for an account as they’re added. For this example, we’ll add support for Atom, a simple publishing standard for content syndication. The first thing we need to do is write a method to generate an Atom feed for a given Account. Although Atom is XML-based, it’s sufficiently different enough from our REST API XML format that we’ll just write new methods for it. Listing
15.7↓ shows the
toAtom methods (one for
Account, one for
Expense) in our
RestFormatters object that will handle the formatting.
def toAtom (a : Account) : Elem = {
val entries = Expense.getByAcct(a,Empty,Empty,Empty,MaxRows(10))
<feed xmlns="http://www.w3.org/2005/Atom">
<title>{a.name}</title>
<id>urn:uuid:{a.id.is}</id>
<updated>{entries.headOption.map(restTimestamp) getOrElse
timestamp.format(new java.util.Date)}</updated>
{ entries.flatMap(toAtom) }
</feed>
}
def toAtom (e : Expense) : Elem =
<entry>
<id>urn:uuid:{restId(e)}</id>
<title>{e.description.is}</title>
<updated>{restTimestamp(e)}</updated>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml">
<table>
<tr><th>Amount</th><th>Tags</th><th>Receipt</th></tr>
<tr><td>{e.amount.is.toString}</td>
<td>{e.tags.map(_.name.is).mkString(", ")}</td>
<td>{
if (e.receipt.is ne null) {
<img src={"/image/" + e.id} />
} else Text("None")
}</td></tr>
</table>
</div>
</content>
</entry>
Now that we have the format, we simply hook into our dispatch method to match a GET request on a URL like:
http://www.pocketchangeapp.com/api/account/<accound ID>
Refer to Listing
15.4.1↑ again to see this match.
15.7.1 An Example Atom Request
An example Atom reqeust/response cycle for a test account is shown in Listing
15.7.1↓. We’ve cut off the entries here for brevity.
An Example Atom Request and Response
Request:
http://www.pocketchangeapp.com/api/account/1
Response:
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Test</title>
<id>urn:uuid:1</id>
<updated>2010-10-06T00:00:00Z</updated>
<entry>
<id>urn:uuid:http://www.pocketchangeapp.com/api/expense/3</id>
<title>Receipt test</title>
<updated>2010-10-06T00:00:00Z</updated>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml">
<table>
<tr><th>Amount</th><th>Tags</th><th>Receipt</th></tr>
<tr><td>12.00</td>
<td>test, receipt</td>
<td><img src="/image/3" /></td></tr>
</table>
</div>
</content>
</entry>
...
15.7.2 Add a feed tag for the account page
As an extra nicety, we want to add an appropriate Atom
<link/> tag to our Account view page so that people can easily subscribe to the feed from their browser. We do this by making two modifications to our template and snippet code. Listing
15.7.2↓ shows how we insert a new binding point in our
viewAcct.html template to place the new link in the page head section.
Adding a binding to viewAcct.html
...
<lift:Accounts.detail eager_eval="true">
<head><acct:atomLink /></head>
...
Listing
15.7.2↓ shows how we generate a new Atom link based on the current
Account’s id that points to the proper URL for our API.
bind("acct", xhtml,
"atomLink" -> <link href={"/api/account/" + acct.id}
type="application/atom+xml"
rel="alternate" title={acct.name + " feed"} />,
"name" -> acct.name.asHtml,
...
15.8 Conclusion
In this chapter, we outlined a RESTful API for a web application and showed how to implement one using Lift. We then extended that API to return Atom in addition to XML and JSON.
(C) 2012 Lift 2.0 EditionWritten by Derek Chen-Becker, Marius Danciu and Tyler Weir