Up: Chapter 5

5.3 Making it easier with RestHelper

The above example shows you how Lift deals with REST calls. However, it’s a tad verbose. Lift’s RestHelper trait contains a lot of very helpful shortcuts that make code more concise, easier to read and easier to maintain. Let’s look at a bunch of examples and then we’ll work through each one:
BasicWithHelper.scala
package code
package lib
​
import model._
​
import net.liftweb._
import common._
import http._
import rest._
import json._
import scala.xml._
​
/**
 * A simple example of a REST style interface
 * using the basic Lift tools
 */
object BasicWithHelper extends RestHelper {
  /*
   * Serve the URL, but have a helpful error message when you
   * return a 404 if the item is not found
   */
  serve {
    case "simple3" :: "item" :: itemId :: Nil JsonGet _ =>
      for {
        // find the item, and if it's not found,
        // return a nice message for the 404
        item <- Item.find(itemId) ?~ "Item Not Found"
      } yield item: JValue
​
    case "simple3" :: "item" :: itemId :: Nil XmlGet _ =>
      for {
        item <- Item.find(itemId) ?~ "Item Not Found"
      } yield item: Node
  }
​
​
  
  serve {
    // Prefix notation
    case JsonGet("simple4" :: "item" :: Item(item) :: Nil, _) =>
      // no need to explicitly create a LiftResponse
      // Just make it JSON and RestHelper does the rest
      item: JValue
​
    // infix notation
    case "simple4" :: "item" :: Item(item) :: Nil XmlGet _ =>
      item: Node
  }
​
  // serve a bunch of items given a single prefix
  serve ( "simple5" / "item" prefix {
    // all the inventory
    case Nil JsonGet _ => Item.inventoryItems: JValue
    case Nil XmlGet _ => Item.inventoryItems: Node
​
    // a particular item
    case Item(item) :: Nil JsonGet _ => item: JValue
    case Item(item) :: Nil XmlGet _ => item: Node
  })
​
  /**
   * Here's how we convert from an Item
   * to JSON or XML depending on the request's
   * Accepts header
   */
  implicit def itemToResponseByAccepts: JxCvtPF[Item] = {
    case (JsonSelect, c, _) => c: JValue
    case (XmlSelect, c, _) => c: Node
  }
​
  /**
   * serve the response by returning an item
   * (or a Box[Item]) and let RestHelper determine
   * the conversion to a LiftResponse using
   * the itemToResponseByAccepts partial function
   */
  serveJx[Item] {
    case "simple6" :: "item" :: Item(item) :: Nil Get _ => item
    case "simple6" :: "item" :: "other" :: item :: Nil Get _ => 
      Item.find(item) ?~ "The item you're looking for isn't here"
  }
​
  /**
   * Same as the serveJx example above, but we've
   * used prefixJx to avoid having to copy the path
   * prefix over and over again
   */
  serveJx[Item] {
    "simple7" / "item" prefixJx {
      case Item(item) :: Nil Get _ => item
      case "other" :: item :: Nil Get _ => 
        Item.find(item) ?~ "The item you're looking for isn't here"
    }
  }
  
}
The first thing is how we declare and register the RestHelper-based service:
/**
 * A simple example of a REST style interface
 * using the basic Lift tools
 */
object BasicWithHelper extends RestHelper {
Our BaseicWithHelper singleton extends the net.liftweb.http.rest.RestHelper trait. We register the dispatch in Boot.scala:
    LiftRules.statelessDispatchTable.append(BasicWithHelper)
This means that the whole BasicWithHelper singleton is a PartialFunction[Req, () => Box[LiftResponse]] that aggregates all the sub-patterns contained inside it. We defined the sub-patterns in a serve block which contains the pattern to match. For example:
  serve {
    case "simple3" :: "item" :: itemId :: Nil JsonGet _ =>
      for {
        // find the item, and if it’s not found,
        // return a nice message for the 404
        item <- Item.find(itemId) ?~ "Item Not Found"
      } yield item: JValue
​
    case "simple3" :: "item" :: itemId :: Nil XmlGet _ =>
      for {
        item <- Item.find(itemId) ?~ "Item Not Found"
      } yield item: Node
  }
Let’s break this down further:
case "simple3" :: "item" :: itemId :: Nil JsonGet _ =>
The above matches /simple3/item/xxx where xxx is extracted to the itemId variable. The request must also have an Accepts header that calls for JSON.
If the pattern matches, execute the following code:
      for {
        // find the item, and if it’s not found,
        // return a nice message for the 404
        item <- Item.find(itemId) ?~ "Item Not Found"
      } yield item: JValue
Some things to notice, we didn’t explicitly create a function that returns a Box[LiftResponse]. Instead, the type is Box[JValue]. RestHelper provides implicit conversions from Box[JValue] to () => Box[LiftResponse]. Specifically, if the Box is a Failure, RestHelper will generate a 404 response with the Failure message as the 404’s body. If the Box is Full, RestHelper will create a JsonResponse with the value in the payload. Let’s take a look at the two cases:
dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl http://localhost:8080/simple3/item/12999
Item Not Found
​
dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl http://localhost:8080/simple3/item/1234
{
  "id":"1234",
  "name":"Cat Food",
  "description":"Yummy, tasty cat food",
  "price":4.25,
  "taxable":true,
  "weightInGrams":1000,
  "qnty":4
}
The XML example is pretty much the same, except we coerse the response to Box[Node] which RestHelper converts into an XmlResponse:
    case "simple3" :: "item" :: itemId :: Nil XmlGet _ =>
      for {
        item <- Item.find(itemId) ?~ "Item Not Found"
      } yield item: Node
Which results in the following:
dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl -i -H "Accept: application/xml" http://localhost:8080/simple3/item/1234
HTTP/1.1 200 OK
Expires: Wed, 9 Mar 2011 01:48:38 UTC
Content-Length: 230
Cache-Control: no-cache; private; no-store
Content-Type: text/xml; charset=utf-8
Pragma: no-cache
Date: Wed, 9 Mar 2011 01:48:38 UTC
X-Lift-Version: Unknown Lift Version
Server: Jetty(6.1.22)
​
<?xml version="1.0" encoding="UTF-8"?>
<item>
  <id>1234</id>
  <name>Cat Food</name>
  <description>Yummy, tasty cat food</description>
  <price>4.25</price>
  <taxable>true</taxable>
  <weightInGrams>1000</weightInGrams>
  <qnty>4</qnty>
</item>
Okay... that’s simpler because we define stuff in the serve block and the conversions from JValue and Node to the right response types is taken care of. Just to be explicit about where the implicit conversions are defined, they’re in the Item singleton:
  /**
   * Convert an item to XML
   */
  implicit def toXml(item: Item): Node = 
    <item>{Xml.toXml(item)}</item>
​
​
  /**
   * Convert the item to JSON format.  This is
   * implicit and in the companion object, so
   * an Item can be returned easily from a JSON call
   */
  implicit def toJson(item: Item): JValue = 
    Extraction.decompose(item)
Okay, so, yippee skippy, we can do simpler REST. Let’s keep looking at examples of how we can make it even simpler. This example uses extractors rather than doing the explicit Item.find:
  serve {
    // Prefix notation
    case JsonGet("simple4" :: "item" :: Item(item) :: Nil, _) =>
      // no need to explicitly create a LiftResponse
      // Just make it JSON and RestHelper does the rest
      item: JValue
​
    // infix notation
    case "simple4" :: "item" :: Item(item) :: Nil XmlGet _ =>
      item: Node
  }
If you like DRY and don’t want to keep repeating the same path prefixes, you can use prefix, for example:
  // serve a bunch of items given a single prefix
  serve ( "simple5" / "item" prefix {
    // all the inventory
    case Nil JsonGet _ => Item.inventoryItems: JValue
    case Nil XmlGet _ => Item.inventoryItems: Node
​
    // a particular item
    case Item(item) :: Nil JsonGet _ => item: JValue
    case Item(item) :: Nil XmlGet _ => item: Node
  })
The above code will list all the items in response to /simple5/item and will serve a specific item in response to /simple5/item/1234, as we see in:
dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl http://localhost:8080/simple5/item
[{
  "id":"1234",
  "name":"Cat Food",
  "description":"Yummy, tasty cat food",
  "price":4.25,
  "taxable":true,
  "weightInGrams":1000,
  "qnty":4
},
...
,{
  "id":"1237",
  "name":"Sloth Food",
  "description":"Slow, slow sloth food",
  "price":18.33,
  "taxable":true,
  "weightInGrams":750,
  "qnty":62
}]
​
dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl http://localhost:8080/simple5/item/1237
{
  "id":"1237",
  "name":"Sloth Food",
  "description":"Slow, slow sloth food",
  "price":18.33,
  "taxable":true,
  "weightInGrams":750,
  "qnty":62
}
In the above examples, we’ve explicitly coersed the results into a JValue or Node depending on the request type. With Lift, it’s possible to define a conversion from a given type to response types (the default response types are JSON and XML) based on the request type and then define the request patterns to match and RestHelper takes care of the rest (so to speak.) Let’s define the conversion from Item to JValue and Node (note the implicit keyword, that says that the conversion is available to serveJx statements:
  implicit def itemToResponseByAccepts: JxCvtPF[Item] = {
    case (JsonSelect, c, _) => c: JValue
    case (XmlSelect, c, _) => c: Node
  }
This is pretty straight forward. If it’s a JsonSelect, return a JValue and if it’s an XmlSelect, convert to a Node.
This is used in the serveJx statement:
  serveJx[Item] {
    case "simple6" :: "item" :: Item(item) :: Nil Get _ => item
    case "simple6" :: "item" :: "other" :: item :: Nil Get _ => 
      Item.find(item) ?~ "The item you’re looking for isn’t here"
  }
So /simple6/item/1234 will match and result in an Item being returned and based on the above implicit conversion, we turn the Item into a JValue or Node depending on the Accepts header and then convert that to a () => Box[LiftResponse]. Let’s see what curl has to say about it:
dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl http://localhost:8080/simple6/item/1237
{
  "id":"1237",
  "name":"Sloth Food",
  "description":"Slow, slow sloth food",
  "price":18.33,
  "taxable":true,
  "weightInGrams":750,
  "qnty":62
}
​
dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl -H "Accept: application/xml" http://localhost:8080/simple6/item/1234
<?xml version="1.0" encoding="UTF-8"?>
<item>
  <id>1234</id>
  <name>Cat Food</name>
  <description>Yummy, tasty cat food</description>
  <price>4.25</price>
  <taxable>true</taxable>
  <weightInGrams>1000</weightInGrams>
  <qnty>4</qnty>
</item>  
Note also that /simple6/item/other/1234 does the right thing. This is because the path is 4 elements long, so it won’t match the first part of the pattern, but does match the second part of the pattern.
Finally, let’s combine serveJx and it’s DRY helper, prefixJx.
  serveJx[Item] {
    "simple7" / "item" prefixJx {
      case Item(item) :: Nil Get _ => item
      case "other" :: item :: Nil Get _ => 
        Item.find(item) ?~ "The item you’re looking for isn’t here"
    }
  }
Up: Chapter 5

(C) 2012 David Pollak