5.4 A complete REST example
The above code gives us the bits and pieces that we can combine into a full fledged REST service. Let’s do that combination and see what such a service looks like:
FullRest.scala
package code
package lib
import model._
import net.liftweb._
import common._
import http._
import rest._
import util._
import Helpers._
import json._
import scala.xml._
/**
* A full REST example
*/
object FullRest extends RestHelper {
// Serve /api/item and friends
serve( "api" / "item" prefix {
// /api/item returns all the items
case Nil JsonGet _ => Item.inventoryItems: JValue
// /api/item/count gets the item count
case "count" :: Nil JsonGet _ => JInt(Item.inventoryItems.length)
// /api/item/item_id gets the specified item (or a 404)
case Item(item) :: Nil JsonGet _ => item: JValue
// /api/item/search/foo or /api/item/search?q=foo
case "search" :: q JsonGet _ =>
(for {
searchString <- q ::: S.params("q")
item <- Item.search(searchString)
} yield item).distinct: JValue
// DELETE the item in question
case Item(item) :: Nil JsonDelete _ =>
Item.delete(item.id).map(a => a: JValue)
// PUT adds the item if the JSON is parsable
case Nil JsonPut Item(item) -> _ => Item.add(item): JValue
// POST if we find the item, merge the fields from the
// the POST body and update the item
case Item(item) :: Nil JsonPost json -> _ =>
Item(mergeJson(item, json)).map(Item.add(_): JValue)
// Wait for a change to the Items
// But do it asynchronously
case "change" :: Nil JsonGet _ =>
RestContinuation.async {
satisfyRequest => {
// schedule a "Null" return if there's no other answer
// after 110 seconds
Schedule.schedule(() => satisfyRequest(JNull), 110 seconds)
// register for an "onChange" event. When it
// fires, return the changed item as a response
Item.onChange(item => satisfyRequest(item: JValue))
}
}
})
}
The whole service is JSON only and contained in a single serve block and uses the prefix helper to define all the requests under /api/item as part of the service.
The first couple of patterns are a re-hash of what we’ve already covered:
// /api/item returns all the items
case Nil JsonGet _ => Item.inventoryItems: JValue
// /api/item/count gets the item count
case "count" :: Nil JsonGet _ => JInt(Item.inventoryItems.length)
// /api/item/item_id gets the specified item (or a 404)
case Item(item) :: Nil JsonGet _ => item: JValue
The next is a search feature at /api/item/search. Using a little Scala library fun, we create a list of the request path elements that come after the search element and all the query parameters named q. Based on these, we search for all the Items that match the search term. We wind up with a List[Item] and we remove duplicates with distinct and finally coerse the List[Item] to a JValue:
// /api/item/search/foo or /api/item/search?q=foo
case "search" :: q JsonGet _ =>
(for {
searchString <- q ::: S.params("q")
item <- Item.search(searchString)
} yield item).distinct: JValue
Next, let’s see how to delete an Item:
// DELETE the item in question
case Item(item) :: Nil JsonDelete _ =>
Item.delete(item.id).map(a => a: JValue)
The only real difference is we’re looking for a JsonDelete HTTP request.
Let’s see how we add an Item with a PUT:
// PUT adds the item if the JSON is parsable
case Nil JsonPut Item(item) -> _ => Item.add(item): JValue
Note the Item(item) -> _ after JsonPut. The extraction signature for JsonPut is (List[String], (JValue, Req)). The List[String] part is simple... it’s a List that contains the request path. The second part of the Pair is a Pair itself that contains the JValue and the underlying Req (in case you need to do something with the request itself). Because there’s a def unapply(in: JValue): Option[Item] method in the Item singleton, we can extract (pattern match) the JValue that is built from the PUT request body. This means if the user PUTs a JSON blob that can be turned into an Item the pattern will match and we’ll evaluate the right hand side of the case statement which adds the Item to inventory. That’s a big ole dense pile of information. So, we’ll try it again with POST.
case Item(item) :: Nil JsonPost json -> _ =>
Item(mergeJson(item, json)).map(Item.add(_): JValue)
In this case, we’re match a POST on /api/item/1234 that has some parsable JSON in the POST body. The mergeJson method takes all the fields in the found Item and replaces them with any of the fields in the JSON in the POST body. So a POST body of {"qnty": 123} would replace the qnty field in the Item. The Item is then added back into the backing store.
Cool. So, we’ve got a variety of GET support in our REST service, a DELETE, PUT and POST. All using the patterns that RestHelper gives us.
Now we have some fun.
One of the features of Lift’s HTML side is support for Comet (server push via long-polling.) If the web container supports it, Lift will automatically use asynchronous support. That means that during a long poll, while no computations are being performed related to the servicing of the request, no threads will be consumed. This allows lots and lots of open long polling clients. Lift’s REST support includes asynchronous support. In this case, we’ll demonstrate opening an HTTP request to /api/item/change and wait for a change to the backing store. The request will be satisfied with a change to the backing store or a JSON JNull after 110 seconds:
case "change" :: Nil JsonGet _ =>
RestContinuation.async {
satisfyRequest => {
// schedule a "Null" return if there’s no other answer
// after 110 seconds
Schedule.schedule(() => satisfyRequest(JNull), 110 seconds)
// register for an "onChange" event. When it
// fires, return the changed item as a response
Item.onChange(item => satisfyRequest(item: JValue))
}
}
If we receive a GET request to /api/item/change, invoke RestContinuation.async. We pass a closure that sets up the call. We set up the call by scheduling a JNull to be sent after 110 seconds. We also register a function which is invoked when the backing store is changed. When either event (110 seconds elapses or the backing store changes), the functions will be invoked and they will apply the satifyRequest function which will invoke the continuation and send the response back to the client. Using this mechanism, you can create long polling services that do not consume threads on the server. Note too that the satisfyRequest function is fire-once so you can call it lots of times, but only the first time counts.
(C) 2012 David Pollak