Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Play Framework 2.2 action composition returning a custom object

I am trying to create a custom play.api.mvc.Action which can be used to populate a CustomerAccount based on the request and pass the CustomerAccount into the controller.

Following the documentation for Play 2.2.x I've created an Action and ActionBuilder but I cannot seem to return the CustomerAccount from within the action.

My current code is:

case class AccountWrappedRequest[A](account: CustomerAccount, request: Request[A]) extends WrappedRequest[A](request)

case class Account[A](action: Action[A]) extends Action[A] {
  lazy val parser = action.parser

  def apply(request: Request[A]): Future[SimpleResult] = {
    AccountService.getBySubdomain(request.host).map { account => 
      // Do something to return the account like return a new AccountWrappedRequest?
      action(AccountWrappedRequest(account, request))
    } getOrElse {
      Future.successful(NotFound(views.html.account_not_found()))
    }
  }
}

object AccountAction extends ActionBuilder[AccountWrappedRequest] { 
  def invokeBlock[A](request: Request[A], block: (AccountWrappedRequest[A]) => Future[SimpleResult]) = {
    // Or here to pass it to the next request?
    block(request) // block(AccountWrappedRequest(account??, request))
  }

  override def composeAction[A](action: Action[A]) = Account(action) 
}

Note: This will not compile because the block(request) function is expecting a type of AccountWrappedRequest which I cannot populate. It will compile when using a straight Request

Additionally...

Ultimately I want to be able to combine this Account action with an Authentication action so that the CustomerAccount can be passed into the Authentication action and user authentication can be provided based on that customer's account. I would then want to pass the customer account and user into the controller.

For example:

Account(Authenticated(Action))) { request => request.account; request.user ... } or better yet as individual objects not requiring a custom request object.

like image 692
David Avatar asked Nov 04 '13 17:11

David


2 Answers

I'm not sure if this is the best way to do it but I have managed to come up with a solution that seems to work pretty well.

The key was to match on the request converting it into an AccountWrappedRequest inside invokeBlock before passing it on to the next request. If another Action in the chain is expecting a value from an earlier action in the chain you can then similarly match the request converting it into the type you need to access the request parameters.

Updating the example from the original question:

case class AccountWrappedRequest[A](account: CustomerAccount, request: Request[A]) extends WrappedRequest[A](request)

case class Account[A](action: Action[A]) extends Action[A] {
  lazy val parser = action.parser

  def apply(request: Request[A]): Future[SimpleResult] = {
    AccountService.getBySubdomain(request.host).map { account => 
      action(AccountWrappedRequest(account, request))
    } getOrElse {
      Future.successful(NotFound(views.html.account_not_found()))
    }
  }
}

object AccountAction extends ActionBuilder[AccountWrappedRequest] { 
  def invokeBlock[A](request: Request[A], block: (AccountWrappedRequest[A]) => Future[SimpleResult]) = {
    request match {
      case req: AccountRequest[A] => block(req)
      case _ => Future.successful(BadRequest("400 Invalid Request"))
    }
  }

  override def composeAction[A](action: Action[A]) = Account(action) 
}

Then inside the apply() method of another Action (the Authenticated action in my case) you can similarly do:

def apply(request: Request[A]): Future[SimpleResult] = {
  request match {
    case req: AccountRequest[A] => {
      // Do something that requires req.account
      val user = User(1, "New User")
      action(AuthenticatedWrappedRequest(req.account, user, request))
    }
    case _ => Future.successful(BadRequest("400 Invalid Request"))
  }
}

And you can chain the actions together in the ActionBuilder

override def composeAction[A](action: Action[A]) = Account(Authenticated(action))

If AuthenticatedWrappedRequest is then passed into the controller you would have access to request.account, request.user and all the usual request parameters.

As you can see there are a couple of cases where the response is unknown which would generate a BadRequest. In reality these should never get called as far as I can tell but they are in there just incase.

I would love to have some feedback on this solution as I'm still fairly new to Scala and I'm not sure if there might be a better way to do it with the same result but I hope this is of use to someone too.

like image 106
David Avatar answered Nov 17 '22 18:11

David


I wrote a standalone small (ish) example that does what you're looking for:

https://github.com/aellerton/play-login-example

I gave up trying to use the Security classes that exist in the play framework proper. I'm sure they're good, but I just couldn't understand them.

Brief guide...

In the example code, a controller is declared as using the AuthenticatedRequests trait:

object UserSpecificController extends Controller with AuthenticatedRequests {
  ...
}

Forcing any page to require authentication (or redirect to get it) is done with the RequireAuthentication action:

def authenticatedIndex = RequireAuthentication { implicit request: AuthenticatedRequest[AnyContent] =>
  Ok("This content will be accessible only after logging in)
}

Signing out is done by using the AbandonAuthentication action:

def signOut = AbandonAuthentication { implicit request =>
  Ok("You're logged out.").withNewSession
}

Note that for this to work, you must override methods from the AuthenticatedRequests trait, e.g.:

override def authenticationRequired[A](request: Request[A]): Future[SimpleResult] = {
  Future.successful(
    Redirect(routes.LoginController.showLoginForm).withSession("goto" -> request.path)
  )
}

There's more to it; best to see the code.

HTH Andrew

like image 40
Andrew E Avatar answered Nov 17 '22 19:11

Andrew E