Version 6, last updated by dpp at 20 Jan 04:18 UTC
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(MyRest) // stateful -- associated with a servlet container session LiftRules.statelessDispatchTable.append(MyRest) // stateless -- no session created
Within your MyRest object, you can define which URLs to serve:
serve {
case Req("api" :: "static" :: _, "xml", GetRequest) => <b>Static</b>
case Req("api" :: "static" :: _, "json", GetRequest) => JString("Static")
}
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("api" :: "static" :: _, _) => <b>Static</b>
case JsonGet("api" :: "static" :: _, _) => JString("Static")
}
The above can also be written:
serve {
case "api" :: "static" :: _ XmlGet _=> <b>Static</b>
case "api" :: "static" :: _ JsonGet _ => JString("Static")
}
Note: If you want to navigate your Web Service, you must remember to add a *.xml or *.json (depending in what you have implemented) 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(id)
}
And with extractors, we convert an element to a particular type and only succeed
with the pattern match (and the dispatch) if the parameter can be converted. For example:
serve {
case "api" :: "user" :: AsLong(id) :: _ XmlGet _ => <b>ID: {id}</b>
case "api" :: "user" :: AsLong(id) :: _ JsonGet _ => JInt(id)
}
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(xml).map { u => u.save; u.toXml}
case "api" :: "user" :: _ JsonPut json -> _ =>
// json is a net.liftweb.json.JsonAST.JValue
User.createFromJson(json).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 (JsonSelect, c, _) => c.toJson
case (XmlSelect, c, _) => 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(info) :: _, _) => Full(info)
}
Or:
// extract the parameters, create a user
// return the appropriate response
def addUser(): Box[UserInfo] =
for {
firstname <- S.param("firstname") ?~ "firstname parameter missing" ~> 400
lastname <- S.param("lastname") ?~ "lastname parameter missing"
email <- S.param("email") ?~ "email parameter missing"
} yield {
val u = User.create.firstName(firstname).
lastName(lastname).email(email)
S.param("password") foreach u.password.set
u.saveMe
}
serveJx {
case Post("api" :: "add_user" :: _, _) => 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(id) :: _ XmlGet _ => <b>ID: {id}</b>
}
serve {
case "api" :: "user" :: AsLong(id) :: _ JsonGet _ => JInt(id)
}
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(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 {
f => {
// schedule a "Null" return if there's no other answer
// after 110 seconds
Schedule.schedule(() => f(JNull), 110 seconds)
// register for an "onChange" event. When it
// fires, return the changed item as a response
Item.onChange(item => f(item: JValue))
}
}
})
}