REST Web Services

Lift makes providing REST-style web services very simple.

First, create an object that extends RestHelper:

import net.liftweb.http._
import net.liftweb.http.rest._

object MyRest extends RestHelper {
  
}

And hook your changes up to Lift in Boot.scala:

LiftRules.dispatch.append // stateful -- associated with a servlet container session
LiftRules.statelessDispatchTable.append // stateless -- no session created

Within your MyRest object, you can define which URLs to serve:

  serve {
    case Req => <b>Static</b>
    case Req => JString
  }

The above code uses the suffix of the request to determine the response type. Lift supports testing the Accept header for a response type:

  serve {
    case XmlGet => <b>Static</b>
    case JsonGet => JString
  }

The above can also be written:

  serve {
    case "api" :: "static" :: _ XmlGet _=> <b>Static</b>
    case "api" :: "static" :: _ JsonGet _ => JString
  }

Note: If you want to navigate your Web Service, you must remember to add a *.xml or *.json at the end of the URL:

http://localhost:8080/XXX/api/static/call.json
http://localhost:8080/XXX/api/static/call.xml

Because the REST dispatch code is based on Scala’s pattern matching, we can extract elements from the request (in this case the third element will be extracted into the id variable which is a String:

  serve {
    case "api" :: "user" :: id :: _ XmlGet _ => <b>ID: {id}</b>
    case "api" :: "user" :: id :: _ JsonGet _ => JString
  }

And with extractors, we convert an element to a particular type and only succeed
with the pattern match if the parameter can be converted. For example:

  serve {
    case "api" :: "user" :: AsLong :: _ XmlGet _ => <b>ID: {id}</b>
    case "api" :: "user" :: AsLong :: _ JsonGet _ => JInt
  }

In the above example, id is extracted if it can be converted to a Long.

Lift’s REST helper can also extract XML or JSON from a POST or PUT request and
only dispatch the request if the XML or JSON is valid:

  serve {
    case "api" :: "user" :: _ XmlPut xml -> _ =>
      // xml is a scala.xml.Node
      User.createFromXml.map { u => u.save; u.toXml}

    case "api" :: "user" :: _ JsonPut json -> _ =>
      // json is a net.liftweb.json.JsonAST.JValue
      User.createFromJson.map { u => u.save; u.toJson}
  }

There may be cases when you want to have a single piece of business logic to calculate a value, but then convert the value to a result based on the request type. That’s where serveJx comes in … it’ll serve a response for JSON and XML requests. If you define a trait called Convertable:

trait Convertable {
  def toXml: Elem
  def toJson: JValue
}

Then define a pattern that will convert from a Convertable to a JSON or XML:

  implicit def cvt: JxCvtPF[Convertable] = {
    case  => c.toJson
    case  => c.toXml
  }

And anywhere you use serveJx and your pattern results in a Box[Convertable], the cvt pattern is used to generate the appropriate response:

  serveJx {
    case Get("api" :: "info" :: Info :: _, _) => Full
  }

Or:

  // extract the parameters, create a user
  // return the appropriate response
  def addUser(): Box[UserInfo] =
    for {
      firstname <- S.param ?~ "firstname parameter missing" ~> 400
      lastname <- S.param ?~ "lastname parameter missing"
      email <- S.param ?~ "email parameter missing"
    } yield {
      val u = User.create.firstName.
      lastName.email

      S.param foreach u.password.set

      u.saveMe
    }

  serveJx {
    case Post => addUser()
  }

In the above example, if the firstname parameter is missing, the response will be a 400 with the response body “firstname parameter missing”. If the lastname parameter is missing, the response will be a 404 with the response body “lastname parameter missing”.

Issues

When using chained extractors such as this, it’s easy to hit a Scala bug in which the Scala compiler generates a method that’s too large for Java. The error looks like:

java.lang.Error: ch.epfl.lamp.fjbg.JCode$OffsetTooBigException: offset too big to fit in 16 bits: 38084

To work around the problem, you need to break your selectors into multiple serve statements.

  serve {
    case "api" :: "user" :: AsLong :: _ XmlGet _ => <b>ID: {id}</b>
  }

  serve {
    case "api" :: "user" :: AsLong :: _ JsonGet _ => JInt
  }

Or you can use the 2.3 “prefix” feature where you can specify the URL prefix before the partial function/pattern match:

/**
 * 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
​
    // /api/item/item_id gets the specified item 
    case Item :: Nil JsonGet _ => item: JValue
​
    // /api/item/search/foo or /api/item/search?q=foo
    case "search" :: q JsonGet _ =>
      (for {
        searchString <- q ::: S.params
        item <- Item.search
      } yield item).distinct: JValue
​
    // DELETE the item in question
    case Item :: Nil JsonDelete _ => 
      Item.delete.map
​
    // PUT adds the item if the JSON is parsable
    case Nil JsonPut Item -> _ => Item.add: JValue
     
    // POST if we find the item, merge the fields from the 
    // the POST body and update the item
    case Item :: Nil JsonPost json -> _ => 
      Item(mergeJson).map(Item.add: JValue)
​
    // Wait for a change to the Items
    // But do it asynchronously
    case "change" :: Nil JsonGet _ =>
      RestContinuation.async {
        f => {
          // schedule a "Null" return if there's no other answer
          // after 110 seconds
          Schedule.schedule(() => f, 110 seconds)
​
          // register for an "onChange" event.  When it
          // fires, return the changed item as a response
          Item.onChange(item => f)
        }
      }
  })
}