Search code examples
scalaplayframeworkcomposition

Play Framework: Composing multiple ActionTransformers - can I add fields in multiple transformers and access them all in the invokeBlock?


Using Play action composition, I'm wondering if there's a way to add fields to a request in multiple ActionTransformers, such that I can access both fields in the request.

Simple example that doesn't work:

import scala.concurrent.{ExecutionContext, Future}
import play.api.mvc.{Action, ActionTransformer, Request, Results, WrappedRequest}

class RequestWithName[A](request: Request[A], val name: String) extends WrappedRequest[A](request)
def addName(implicit ec: ExecutionContext) = new ActionTransformer[Request, RequestWithName] {
  override def executionContext: ExecutionContext = ec
  override def transform[A](request: Request[A]): Future[RequestWithName[A]] = ???
}

class RequestWithUserId[A](request: Request[A], val userId: String) extends WrappedRequest[A](request)
def addUserId(implicit ec: ExecutionContext) = new ActionTransformer[Request, RequestWithUserId] {
  override def executionContext: ExecutionContext = ec
  override def transform[A](request: Request[A]): Future[RequestWithUserId[A]] = ???
}

Action.andThen(addName).andThen(addUserId) { req =>
  Results.Ok(req.name + req.userId) // compile error: name not available
}

Action.andThen(addUserId).andThen(addName) { req =>
  Results.Ok(req.name + req.userId) // compile error: userId not available
}

It makes sense why these compile errors happen - the last andThen returns an ActionTransformer that has only one of the two fields. But is there a way to accomplish the same thing, without making them aware of each other? Eg. I could add a RequestWithUserIdAndName - but then I can't compose that with other transforms that add even more fields.


Solution

  • How about writing something like EnrichedRequest which has a collection of enrichments (implemented as some sealed hierarchy), so that you would lift it first, and then add enrichments as you see fit?

    sealed trait Enrichment
    case class UserName(name: String) extends Enrichment
    case class UserId(name: String) extends Enrichment
    
    class EnrichedRequest[A](
      request: Request[A],
      val enrichments: List[Enrichment]
    ) extends WrappedRequest[A](request)
    def asEnriched(implicit ec: ExecutionContext) = new ActionTransformer[Request, EnrichedRequest] {
      ...
    }
    
    def addName(implicit ec: ExecutionContext) = new ActionTransformer[EnrichedRequest, EnrichedRequest] {
      ...
    }
    
    def addUserId(implicit ec: ExecutionContext) = new ActionTransformer[EnrichedRequest, EnrichedRequest] {
      ...
    }
    
    Action.andThen(asEnriched).andThen(addName).andThen(addUserId) { ... }
    Action.andThen(asEnriched).andThen(addUserId).andThen(addName) { ... }
    

    That would be easy to implement and freely extensible. The only downside is that to extract data you would have to do something like:

    enrichments.collect {
      case UserName(name) => name
    }.head
    

    to extract data back.