Up: Chapter 5

5.2 REST the hard way

Let’s take a look at the raw level of doing REST with Lift: taking an incoming HTTP request and transforming it into a function that returns a Box[LiftResponse] (and don’t worry, it gets easier, but we’re starting with the ugly verbose stuff so you get an idea of what’s happening under the covers):
BasicExample.scala
package code
package lib
​
import model._
​
import net.liftweb._
import common._
import http._
​
/**
 * A simple example of a REST style interface
 * using the basic Lift tools
 */
object BasicExample {
  /*
   * Given a suffix and an item, make a LiftResponse
   */
  private def toResponse(suffix: String, item: Item) =
    suffix match {
      case "xml" => XmlResponse(item)
      case _ => JsonResponse(item)
    }
​
  /**
   * Find /simple/item/1234.json
   * Find /simple/item/1234.xml
   */
  lazy val findItem: LiftRules.DispatchPF = {
    case Req("simple" :: "item" :: itemId :: Nil, //  path
             suffix, // suffix
             GetRequest) => 
               () => Item.find(itemId).map(toResponse(suffix, _))
  }
​
  /**
   * Find /simple2/item/1234.json
   */
  lazy val extractFindItem: LiftRules.DispatchPF = {
    // path with extractor
    case Req("simple2" :: "item" :: Item(item) :: Nil, 
             suffix, GetRequest) =>
               // a function that returns the response
               () => Full(toResponse(suffix, item))
  }
}
One additional piece of the puzzle is hooking up the handlers to Lift. This is done in Boot.scala with the following lines:
    // the stateless REST handlers
    LiftRules.statelessDispatchTable.append(BasicExample.findItem)
    LiftRules.statelessDispatchTable.append(BasicExample.extractFindItem)
​
    // stateful versions of the same
    // LiftRules.dispatch.append(BasicExample.findItem)
    // LiftRules.dispatch.append(BasicExample.extractFindItem)
Let’s break down the code. First, each handler is a PartialFunction[Req, () => Box[LiftResponse]], but we can use a shorthand of LiftRules.dispatchPF which is a Scala type that aliases the partial function.
lazy val findItem: LiftRules.DispatchPF =
defines findItem which has the type signature of a request dispatch handler.
    case Req("simple" :: "item" :: itemId :: Nil, //  path
             suffix, // suffix
             GetRequest) => 
Defines a pattern to match. In this case, any 3 part path that has the first two parts /simple/item will be matched. The third part of the path will be extracted to the variable itemId. The suffix of the last path item will be extracted to the variable suffix and the request must be a GET.
If the above criteria is met, then the partial function is defined and Lift will apply the partial function to get the resulting () => Box[LiftResponse].
               () => Item.find(itemId).map(toResponse(suffix, _))
This is a function that finds the itemId and converts the resulting Item to a response based on the request suffix. The toResponse method looks like:
  /*
   * Given a suffix and an item, make a LiftResponse
   */
  private def toResponse(suffix: String, item: Item) =
    suffix match {
      case "xml" => XmlResponse(item)
      case _ => JsonResponse(item)
    }
That’s all pretty straight forward, if a little verbose. Let’s look at the other example in this file. It uses an extractor to convert the String of the third element of the request path to an Item:
    // path with extractor
    case Req("simple2" :: "item" :: Item(item) :: Nil, 
             suffix, GetRequest) =>
In this case, the pattern will not be matched unless that third element of the path is a valid Item. If it is, the variable item will contain the Item for processing. Converting this to a valid response looks like:
               // a function that returns the response
               () => Full(toResponse(suffix, item))
Let’s look at the object Item’s unapply method to see how the extraction works:
  /**
   * Extract a String (id) to an Item
   */
  def unapply(id: String): Option[Item] = Item.find(id)
In fact, let’s look at the entire Item code listing. As promised, Simply Lift, does not explicitly cover persistence. This class is an in-memory mock persistence class, but it behaves like any other persistence mechanism in Lift.
Item.scala
package code
package model
​
import net.liftweb._
import util._
import Helpers._
import common._
import json._
​
import scala.xml.Node
​
/**
 * An item in inventory
 */
case class Item(id: String, name: String, 
                description: String,
                price: BigDecimal, taxable: Boolean,
                weightInGrams: Int, qnty: Int)
​
/**
 * The Item companion object
 */
object Item {
  private implicit val formats =
    net.liftweb.json.DefaultFormats + BigDecimalSerializer
​
  private var items: List[Item] = parse(data).extract[List[Item]]
​
  private var listeners: List[Item => Unit] = Nil
​
  /**
   * Convert a JValue to an Item if possible
   */
  def apply(in: JValue): Box[Item] = Helpers.tryo{in.extract[Item]}
​
  /**
   * Extract a String (id) to an Item
   */
  def unapply(id: String): Option[Item] = Item.find(id)
​
  /**
   * Extract a JValue to an Item
   */
  def unapply(in: JValue): Option[Item] = apply(in)
​
  /**
   * The default unapply method for the case class.
   * We needed to replicate it here because we
   * have overloaded unapply methods
   */
  def unapply(in: Any): Option[(String, String, 
                                String,
                                BigDecimal, Boolean,
                                Int, Int)] = {
    in match {
      case i: Item => Some((i.id, i.name, i.description,
                            i.price, i.taxable,
                            i.weightInGrams, i.qnty))
      case _ => None
    }
  }
​
  /**
   * Convert an item to XML
   */
  implicit def toXml(item: Item): Node = 
    {Xml.toXml(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)
​
  /**
   * Convert a Seq[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(items: Seq[Item]): JValue = 
    Extraction.decompose(items)
​
  /**
   * Convert a Seq[Item] to XML format.  This is
   * implicit and in the companion object, so
   * an Item can be returned easily from an XML REST call
   */
  implicit def toXml(items: Seq[Item]): Node = 
    {
      items.map(toXml)
    }
​
  /**
   * Get all the items in inventory
   */
  def inventoryItems: Seq[Item] = items
​
  // The raw data
  private def data = 
"""[
  {"id": "1234", "name": "Cat Food",
  "description": "Yummy, tasty cat food",
  "price": 4.25,
  "taxable": true,
  "weightInGrams": 1000,
  "qnty": 4
  },
  {"id": "1235", "name": "Dog Food",
  "description": "Yummy, tasty dog food",
  "price": 7.25,
  "taxable": true,
  "weightInGrams": 5000,
  "qnty": 72
  },
  {"id": "1236", "name": "Fish Food",
  "description": "Yummy, tasty fish food",
  "price": 2,
  "taxable": false,
  "weightInGrams": 200,
  "qnty": 45
  },
  {"id": "1237", "name": "Sloth Food",
  "description": "Slow, slow sloth food",
  "price": 18.33,
  "taxable": true,
  "weightInGrams": 750,
  "qnty": 62
  },
]
"""
​
  /**
   * Select a random Item
   */
  def randomItem: Item = synchronized {
    items(Helpers.randomInt(items.length))
  }
​
  /**
   * Find an item by id
   */
  def find(id: String): Box[Item] = synchronized {
    items.find(_.id == id)
  }
​
  /**
   * Add an item to inventory
   */
  def add(item: Item): Item = {
    synchronized {
      items = item :: items.filterNot(_.id == item.id)
      updateListeners(item)
    }
  }
​
  /**
   * Find all the items with the string in their name or
   * description
   */
  def search(str: String): List[Item] = {
    val strLC = str.toLowerCase()
​
    items.filter(i =>
      i.name.toLowerCase.indexOf(strLC) >= 0 ||
                 i.description.toLowerCase.indexOf(strLC) >= 0)
  }
​
  /**
   * Deletes the item with id and returns the
   * deleted item or Empty if there's no match
   */
  def delete(id: String): Box[Item] = synchronized {
    var ret: Box[Item] = Empty
​
    val Id = id // an upper case stable ID for pattern matching
​
    items = items.filter {
      case i@Item(Id, _, _, _, _, _, _) => 
        ret = Full(i) // side effect
        false
      case _ => true
    }
​
    ret.map(updateListeners)
  }
​
  /**
   * Update listeners when the data changes
   */
  private def updateListeners(item: Item): Item = {
    synchronized {
      listeners.foreach(f => 
        Schedule.schedule(() => f(item), 0 seconds))
​
      listeners = Nil
    }
    item
  }
​
  /**
   * Add an onChange listener
   */
  def onChange(f: Item => Unit) {
    synchronized {
      // prepend the function to the list of listeners
      listeners ::= f
    }
  }
    
}
​
/**
 * A helper that will JSON serialize BigDecimal
 */
object BigDecimalSerializer extends Serializer[BigDecimal] {
  private val Class = classOf[BigDecimal]
​
  def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), BigDecimal] = {
    case (TypeInfo(Class, _), json) => json match {
      case JInt(iv) => BigDecimal(iv)
      case JDouble(dv) => BigDecimal(dv)
      case value => throw new MappingException("Can't convert " + value + " to " + Class)
    }
  }
​
  def serialize(implicit format: Formats): PartialFunction[Any, JValue] = {
    case d: BigDecimal => JDouble(d.doubleValue)
  }
}
Let’s take a look at what the resulting output is:
dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl http://localhost:8080/simple/item/1234
{
  "id":"1234",
  "name":"Cat Food",
  "description":"Yummy, tasty cat food",
  "price":4.25,
  "taxable":true,
  "weightInGrams":1000,
  "qnty":4
}
​
dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl http://localhost:8080/simple/item/1234.xml
<?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>  
dpp@raptor:~/proj/simply_lift/samples/http_rest$ 
​
Up: Chapter 5

(C) 2012 David Pollak