سؤال

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.

هل كانت مفيدة؟

المحلول

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))
}

نصائح أخرى

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
    )

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

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top