Thursday, November 4, 2010

ruby on rails style routes with lift

In my latest project i am using lift/scala for restful services. spending the last couple of years using ruby on rails for such, the first thing i was looking for was a convenient way to define my generic restful routes. here is what i cam up with:

object Routes extends RestHelper with Logger
{      
 // response builder
 implicit def cvt:JxCvtPF[ResponseItem] = 
 { 
  case (XmlSelect, response, request) => response.toXml 
  case (JsonSelect, response, request) => response.toJson
 }
 
 private val PREFIX:String = "api"
 
 private var _services:mutable.ListMap[String, BaseService] = null
    
 def registerService(service:BaseService, alias:String=null)
 {
  if (null == _services) _services = new mutable.ListMap[String, BaseService]()
  val key:String = if (null != alias) alias else service.getClass.getName.split("\\.").toList.last.replace("Service$", "")
  _services += key -> service
 }  
   
 /* 
 *** standard restful routes
 */  
   
 serveJx
 {
  // GET /api/service/index.{xml|json}
  case Get(PREFIX :: StringValue(service) :: "index" :: Nil, _) => invokeApi(service, "index")   
 }
 serveJx
 { 
  // GET /api/service/1.{xml|json}
  case Get(PREFIX :: StringValue(service) :: LongValue(id) :: Nil, _) => invokeApi(service, "get", id)
 }
 serveJx
 { 
  // GET /api/service/api.{xml|json}
  case Get(PREFIX :: StringValue(service) :: StringValue(api) :: Nil, _) => invokeApi(service, api)
 }
 serveJx
 { 
  // GET /api/service/api/1.{xml|json}
  case Get(PREFIX :: StringValue(service) :: StringValue(api) :: LongValue(id) :: Nil, _) => invokeApi(service, api, id)
 }  
 serveJx
 { 
  // POST /api/service.{xml|json}
  case Post(PREFIX :: StringValue(service) :: Nil, _) => invokeApi(service, "create")
 }
 serveJx
 { 
  // POST /api/service/api.{xml|json}
  case Post(PREFIX :: StringValue(service) :: StringValue(api) :: Nil, _) => invokeApi(service, api)
 }
 serveJx
 { 
  // POST /api/service/api/1.{xml|json}
  case Post(PREFIX :: StringValue(service) :: StringValue(api) :: LongValue(id) :: Nil, _) => invokeApi(service, api, id)
 }  
 serveJx
 { 
  // PUT /api/service/1.{xml|json}
  case Put(PREFIX :: StringValue(service) :: LongValue(id) :: Nil, _) => invokeApi(service, "update", id)
 }
 serveJx
 { 
  // DELETE /api/service/1.{xml|json}
  case Delete(PREFIX :: StringValue(service) :: LongValue(id) :: Nil, _) => invokeApi(service, "delete", id)
 }
 
 private def invokeApi(service:String, api:String, id:Long=0):Box[ResponseItem] =
 {
  val serviceName:String = StringHelpers.camelify(service)
  val methodName:String = StringHelpers.camelifyMethod(api)
  
  info("processing api " + service + ":" + api + " as " + serviceName + "[Service]:" + methodName)
     
  if (!_services.contains(serviceName)) return Full(Failed("unknown service " + service))
  
  _services(serviceName) match
  {
   case serviceHandler:BaseService =>
   {     
    serviceHandler.getClass.getMethods.foreach((method:Method) =>
    {      
     if (methodName == method.getName)
     {      
      val result:Object = if (0 == id) method.invoke(serviceHandler) else method.invoke(serviceHandler, id.asInstanceOf[AnyRef])       
      result match
      {
       case response:ResponseItem => return Full(response) 
       case _ => return Empty
      }
     }
    })
    return Full(Failed("unknown API " + service + ":" + api)) 
   }
   case _ => Full(Failed("invalid API configuration, service is not a BaseService"))
  }
 }
}


Once the routes handler (above) is define, you need to register the service implementation(s) to the routes handler and register the routes handler to Lift's dispatch table, this is done in lift's boot sequence, typically defined in Boot.scala. for example:

Routes.registerService(SessionService)
Routes.registerService(UserService)
.    
.
.

LiftRules.dispatch.append(Routes)


Naturally, you can extend this rest handler to handle custom restful routes, using the serveJx or serve directives, this is explained in more details here

Please note that a bug in scala is preventing from bundling all those serveJx case statements together, thus, the DRYlessness.

1 comment:

  1. please note a breaking change introduced by liftweb 2.3, details here http://groups.google.com/group/liftweb/browse_thread/thread/a1687794b26a63b8

    ReplyDelete