4

In regard to this question I am curious how one can do post-request REST processing a la (crude):

def postProcessor[T](content: T) = {
  request match {
    case Accepts.Json() => asJson(content)
    case Accepts.Xml()  => asXml(content)
    case _ => content
  }
}

overriding onRouteRequest in Global config does not appear to provide access to body of the response, so it would seem that Action composition is the way to go to intercept the response and do post-processing task(s).

Question: is this a good idea, or is it better to do content-type casting directly within a controller (or other class) method where the type to cast is known?

Currently I'm doing this kind of thing everywhere:

toJson( i18n("account not found") )
toJson( Map('orderNum-> orderNum) )

while I'd like the toJson/toXml conversion to happen based on accepts header post-request.

Community
  • 1
  • 1
virtualeyes
  • 11,147
  • 6
  • 56
  • 91

3 Answers3

4

You want to be able to compute a Result containing a representation of an object of a type A according to the request’s Accept header value. You can encode this capability with the following type trait:

trait Repr[-A] {
  def render(a: A, request: RequestHeader): Result
}

You could then render any resource from your controller using the following helper trait:

trait ReprSupport {
  def repr[A](a: A)(implicit request: RequestHeader, repr: Repr[A]) =
    repr.render(a, request)
}

object MyApp extends Controller with ReprSupport {
  def index = Action { implicit request =>
    repr(Foo("bar"))
  }
}

Where Foo is a simple case class defined as follows:

case class Foo(bar: String)

In order to be able to compile the above code, you need to have a value of type Repr[Foo] in your implicit scope. A first implementation could be written as follows:

object Foo extends AcceptExtractors {
  implicit val fooRepr = new Repr[Foo] {
    def render(foo: Foo, request: RequestHeader): Result = request match {
      case Accepts.Html() => Ok(views.html.foo(foo)) // Assumes there is a foo.scala.html template taking just one parameter of type Foo
      case Accepts.Json() => Ok(Json.obj("bar" -> foo.bar))
      case _ => NotAcceptable
    }
  }
}

But for each data type for which you’ll want to write a Repr instance, the render method will follow the same pattern:

implicit val somethingRepr = new Repr[Something] {
  def render(value: Something, request: RequestHeader): Result = request match {
    // <Some interesting code> (e.g. case Accepts.Html() => Ok(views.html.something(value)))
    case _ => NotAcceptable
  }
}

You probably want to reduce the boilerplate and to avoid users to forget the last “case” statement by abstracting over this pattern. You can for example write the following helper method to build a Repr[Something]:

object Repr {
  def apply[A](f: PartialFunction[RequestHeader, A => Result]): Repr[A] = new Repr[A] {
    def render(a: A, request: RequestHeader): Result =
      if (f.isDefinedAt(request)) f(request)(a)
      else NotAcceptable
  }
}

Thus you just need to write the following to get a Repr[Foo]:

implicit val fooRepr = Repr[Foo] {
  case Accepts.Html() => foo => Ok(views.html.foo(foo))
  case Accepts.Json() => foo => Ok(Json.obj("bar" -> foo.bar))
}
Julien Richard-Foy
  • 9,634
  • 2
  • 36
  • 42
  • great stuff. The Repr structure is a good addition to the "wrapper" I talked about – Andy Petrella Jun 17 '12 at 21:38
  • Note that the above code does not take Accept values’s [quality factor](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1) into account. – Julien Richard-Foy Jun 17 '12 at 21:44
  • hopefully, it's rarely used, especially within a browser. However, this is true for content negotiation between WS f.i. – Andy Petrella Jun 17 '12 at 21:57
  • @JulienRichard-Foy this is impressive (thus, you get the checkmark); however, a bit too much boilerplate for my liking. As it stands, complex conversions are already handled in-scope; I'm looking to handle the simple cases, for example Strings and Maps that have not yet been converted to json, xml, etc. I'll need to experiment, probably passing in a status code (so can return something other than "Ok"), session Map argument (to Status(content).withSession(session)), and who knows what else, have yet to explore a solution ;-) Thanks for pointing the way... – virtualeyes Jun 21 '12 at 07:13
1

One option could be to create a kind of wrapper for that.

It should be something like the following:

  //THE WRAPPER that takes the action computation and the formatter
  def acceptEnabledAction[A]: (RequestHeader => A, (RequestHeader, A) => Result) => Action[AnyContent] =
     (a, f) => 
        Action { request => 
            f(request, a(request))
        }

  //a sample formatter
  val encoder = (r, r) => r.accept /*or smthg*/ match {
        case x if x == "text/json" || x == "application/json" => Ok(toJson(r)) /*dummy to json*/
        case x if x == "text/xml" || x == "application/xml"   => Ok(toXml(r)) /*dummy to xml*/
        case _ => BadRequest("not accepted")
      }


  //an action using it    
  def index = acceptEnabledAction[Map[String, Boolean]](
      rh => /*the real action content is here*/Map("a" -> true), 
      encoder
    )
Andy Petrella
  • 4,345
  • 26
  • 29
  • +1, @andy petralla, my thoughts exactly. I have implemented action composition for authentication, and intended to do something similar for REST post-processing. In regard to your approach, I'm on Play 2.1 so can match request on Accepts.Json, Accepts.Xml, etc. Also, there may be cases, for example, form binding error, where you convert to Json (e.errorsAsJson) in place, so we would want to check in your encoder method if content is already of a given type. Finally, the composed action should accept generic T content, so as to have a clean "def index = Rest {...} implementation" – virtualeyes Jun 17 '12 at 08:03
  • For composition IOT do some auth, I've written two posts: http://ska-la.blogspot.be/2012/06/attempt-of-play20-action-interceptor.html and http://ska-la.blogspot.be/2012/06/type-safed-composable-interceptors-in.html. It might interest you. Indeed, I saw the extractors in play2.1, but still on 2.0. For the `T`, isn't it sufficient that `acceptEnabledAction` as a generic result. For the encoder, you could have `acceptEnabledAction` returning an Either (or Validation) with left part as the errored form and the right side as the value: `Either[Form[T], T]`. So the encoder will have it as param. – Andy Petrella Jun 17 '12 at 09:55
  • just got back to this, checked out your posts, thanks, but not (yet) my cup of tea. Scalaz, Shapeless, and their symbol ridden friends are a bit difficult to grok for me at this time. Either enabled for comprehensions tend to achieve something similar to what you are doing, but with a much nicer syntax (IMO), and far easier (for me at any rate) to understand ;-) I'll get to Scalaz et al, but only a few months into Scala, need a foundation to build a house... – virtualeyes Jun 21 '12 at 07:23
  • Hey no problem, I've done some work to enable f° stuffs. I'm in the scala work for 8 months only, and indeed I had to dig a while for gettin' some of my f° skills back ^^. However, if you just check out the github project (which I probably have to move to a simple library) and you can reuse the Intercept class, without having to understand what's goin' on under the sea. – Andy Petrella Jun 21 '12 at 10:09
  • I'm saying it's not required ;-) Action composition provides your "steal" approach to some degree. I mean, you can return a User instance, right? Is it a requirement to compose userid + username using Sabin's Shapeless library? I would think not, dao.user.findById(id) does the trick, and action composition abstracts away the success/fail response (i.e. "def update = Authenticated { implicit request => // look, request.user } "). Don't get me wrong, what you're doing is cool (for comprehension monad is nicely done, for example), but I'm not yet sold on pure F in Scala – virtualeyes Jun 21 '12 at 10:41
  • You're right. It's not required, actually I'm trying to avoid amap the boilerplate. So such steals action would return the User instance or a Failure, for all actions that are needing such instance. But I am, definitively, not trying to have you using it ^^, especially if you don't need it ;-). – Andy Petrella Jun 21 '12 at 10:52
  • The for comprehension monad, that is quite interesting and immediately useful. Play action composition provides similar functionality to Steal (although not as functional, which I see as a benefit in this case), so I'm happily going with the former for now. I'll check out your blog, clearly you like experimenting with the bleeding edge ;-) – virtualeyes Jun 21 '12 at 11:19
0

Another option is using the mimerender module (disclosure: I wrote it). You define the mapping once:

val m = mapping(
  "text/html" -> { s: String => views.html.index(s) },
  "application/xml" -> { s: String => <message>{s}</message> },
  "application/json" -> { s: String => toJson(Map("message" -> toJson(s))) },
  "text/plain" -> identity[String]_
)

and just reuse it throughout all your controllers:

object Application extends Controller {
  def index = Action { implicit request =>
    m.status(200)("Hello, world!")
  }
}

Note: it's a very early release and has only been tested on Play 2.0.4

Martin Blech
  • 13,135
  • 6
  • 31
  • 35