Quantcast

Separating the concerns of routing and rendering

classic Classic list List threaded Threaded
2 messages Options
Reply | Threaded
Open this post in threaded view
|  
Report Content as Inappropriate

Separating the concerns of routing and rendering

laufer
I have been thinking about a good way to separate the concerns of routing and rendering. I was wondering what others in the Unfiltered community thought about this approach, so any reactions are welcome if you have the time.

Here are some pieces of a plan. The renderer needs to look at the various Accept headers in the request.

class UserPlan(..., val renderer: Renderer[User]) ... {

    case req @ GET(Path(Seg("users" :: name :: Nil))) => {
      userRepository findByName name match {
        case Some(user) => Ok ~> renderer(req)(user)
        case _ => NotFound
      }
    }

Here are the general trait and a resource-specific example that looks at more than one Accept header:

trait Renderer[T] {
  def apply[R](req: HttpRequest[R])(resource: T): ResponseFunction[Any]
}

object userRenderer extends Renderer[User] {
  def apply[R](req: HttpRequest[R])(user: User) = {
    req match {
      case Accepts.Json(_) => JsonContent ~> ResponseString("""{"name": "blah"}""")
      case Accepts.Html(_) => HtmlContent ~> {
        val langs = req match { case AcceptLanguage(langs) => langs ; case _ => List empty }
        if (langs find (_ startsWith "de") isDefined)
          ResponseString("""<html>mein name ist hase</html>""")
        else
          ResponseString("""<html>my name is blah</html>""")
      }
      case _ => PlainTextContent ~> ResponseString(user toString)
    }
  }
}

The code is part of this work-in-progress example:

https://github.com/webservices-cs-luc-edu/unfiltered-example-bookmarks

Regards,

Konstantin
Reply | Threaded
Open this post in threaded view
|  
Report Content as Inappropriate

Re: Separating the concerns of routing and rendering

chris_lewis
Konstantin,

I like the idea of separating the routing and rendering concerns, and while your approach does achieve this, I wanted to explore a bit further. I've pondered this same problem on various occasions, and I don't like the idea that the routing code separates itself from rendering by means of a continuation. While it does work, I wanted to aim for even less coupling.

It seems to me that routing is all about matching a request to an appropriate piece of logic. In terms of Unfiltered, I'll call this a ResourceIntent. Rendering is simply converting the result of an application function into a format suitable for the client; I'll call this a ResponseIntent. A ResponseIntent needs the model produced by the ResourceIntent, as well as the request, so that it can determine how to render the model. Let's say these are wrapped in a type ModelPackage:

/* A module modeled after unfiltered.Cycle. We have types for a ModelPackage, a ResourceIntent, and a ResponseIntent. */
object Cycle {
  type ModelPackage[RE, M] = (HttpRequest[RE], M)
  type ResourceIntent[RE, M] = PartialFunction[HttpRequest[RE], ModelPackage[RE, M]]
  type ResponseIntent[RE, M, R] = PartialFunction[ModelPackage[RE, M], ResponseFunction[R]]
}


Like the filter and netty modules, these intents will need target-specific representations:

/* Filter-specific intent types. */
object Plan {
  type ResourceIntent[M] = Cycle.ResourceIntent[HttpServletRequest, M]
  type ResponseIntent[M] = Cycle.ResponseIntent[HttpServletRequest, M, HttpServletResponse]
}


A ResourceIntent is where the usual request matching happens. Instead of a ResponseFunction, the result will be some model type M (here, an Option[User]):

val f: ResourceIntent[Option[User]] = {
  case r @ GET(Path(Seg("users" :: name :: Nil))) => r -> userRepository.findByName(name)
}

This is just like a normal plan, except the rendering logic is gone (only application logic remains). Elsewhere we define a ResponseIntent, whose argument will be type-compatible with a ResourceIntent's result:

val g: ResponseIntent[Option[User]] = {
  case (r, Some(user)) => ResponseString("User: " + user.name)
}

The argument here is a ModelPackage; a Tuple2 of the request and some intent-specific model type. This allows rendering to be tailored based on the request. With a ResourceIntent `f` and a ResponseIntent `g`, we can compose a full Intent while keeping the routing + app logic separate from the rendering:

class App extends unfiltered.filter.Plan {
  import Plan._
 
  val intent = f andThen g
}


I've put together a rough prototype that uses this approach, so I expect the above code *should* compile, provided the missing pieces (userRepository) are filled in. I'd like the community's feedback on this, as well as the separation problem in general. Thanks for Unfiltered, and thank you Konstantin for starting this discussion.

-chris
Loading...