Skip to content

Repository files navigation

Play2 Stackable Action Composition

![Gitter](https://badges.gitter.im/Join Chat.svg)

This module offers Action Composition Utilities to Play2.1, 2.2, 2.3, 2.4, 2.5 applications

Target

This module targets the Scala version of Play2.x

This module has been tested on Play2.5.0

Motivation

Action Composition is somewhat limited in terms of composability.

For example, imagine that we want automatic DB transaction management, auth, and pjax functionality.

defTxAction(f: DBSession=>Request[AnyContent] =>Future[Result]):Action[AnyContent] = {
Action.async { request =>importTxBoundary.Future._DB localTx { session =>
f(session)(request)
}
}
}
defAuthAction(authority: Authority)(f: User=>DBSession=>Request[AnyContent] =>Future[Result]):Action[AnyContent] = {
TxAction { session => request =>valuser:Either[Result, User] = authorized(authority)(request)
user.right.map(u => f(u)(session)(request)).merge
}
}
typeTemplate=Html=>HtmldefPjaxAction(authority: Authority)(f: Template=>User=>DBSession=>Request[AnyContent] =>Future[Result]):Action[AnyContent] = {
AuthAction(authority) { user => session => request =>valtemplate=if (req.headers.keys("X-Pjax")) views.html.pjaxTemplate.apply else views.html.fullTemplate.apply
f(template)(user)(session)(request)
}
}
defindex=PjaxAction(NormalUser) { template => user => session => request =>valmessages=Message.findAll(session)
Ok(views.hrml.index(messages)(template))
}

So far so good, but what if we need a new action that does both DB transaction management and pjax?

We have to create another PjaxAction.

defPjaxAction(f: Template=>DBSession=>Request[AnyContent] =>Future[Result]):Action[AnyContent] = {
TxAction { session => request =>valtemplate=if (req.headers.keys("X-Pjax")) html.pjaxTemplate.apply else views.html.fullTemplate.apply
f(template)(session)(request)
}
}

What a mess!

As an alternative, this module offers Composable Action composition using the power of traits.

Example

  1. First step, Create a sub trait of StackableController for every function.

    traitAuthElementextendsStackableControllerwithAuthConfigImpl {
    self: ControllerwithAuth=>caseobjectAuthKeyextendsRequestAttributeKey[User]
    caseobjectAuthorityKeyextendsRequestAttributeKey[Authority]
    overridedefproceed[A](req: RequestWithAttributes[A])(f: RequestWithAttributes[A] =>Future[Result]):Future[Result] = {
    (for {
    authority <- req.get(AuthorityKey).toRight(authorizationFailed(req)).right
    user <- authorized(authority)(req).right
    } yieldsuper.proceed(req.set(AuthKey, user))(f)).merge
    }
    implicitdefloggedIn(implicitreq: RequestWithAttributes[_]):User= req.get(AuthKey).get
    }
    traitDBSessionElementextendsStackableController {
    self: Controller=>caseobjectDBSessionKeyextendsRequestAttributeKey[DBSession]
    overridedefproceed[A](req: RequestWithAttributes[A])(f: RequestWithAttributes[A] =>Future[Result]):Future[Result] = {
    importTxBoundary.Future._DB.localTx { session =>super.proceed(req.set(DBSessionKey, session))(f)
    }
    }
    implicitdefdbSession(implicitreq: RequestWithAttributes[_]):DBSession= req.get(DBSessionKey).get
    }
    traitPjaxElementextendsStackableControllerwithAuthConfigImpl {
    self: ControllerwithAuth=>typeTemplate=Html=>HtmlcaseobjectTemplateKeyextendsRequestAttributeKey[Template]
    overridedefproceed[A](req: RequestWithAttributes[A])(f: RequestWithAttributes[A] =>Future[Result]):Future[Result] = {
    valtemplate=if (req.headers.keys("X-Pjax")) views.html.pjaxTemplate else views.html.fullTemplate
    super.proceed(req.set(TemplateKey, template))(f)
    }
    implicitdeftemplate(implicitreq: RequestWithAttributes[_]):Template= req.get(TemplateKey).get
    }
  2. mix your traits into your Controller

    objectApplicationextendsControllerwithPjaxElementwithAuthElementwithDBSessionElementwithAuthwithAuthConfigImpl {
    defmessages=StackAction(AuthorityKey->NormalUser) { implicit req =>valmessages=Message.findAll
    Ok(html.messages(messages)(loggedIn)(template))
    }
    defeditMessage(id: MessageId) =StackAction(AuthorityKey->Administrator) { implicit req =>valmessages=Message.findAll
    Ok(html.messages(messages)(loggedIn)(template))
    }
    }
  3. Mixin different combinations of traits, depending on the functionality that you need.

    objectNoAuthControllerextendsControllerwithPjaxElementwithDBSessionElement {
    defmessages=StackAction { implicit req =>valmessages=Message.findAll
    Ok(html.messages(messages)(GuestUser)(template))
    }
    defeditMessage(id: MessageId) =StackAction { implicit req =>valmessages=Message.findAll
    Ok(html.messages(messages)(GuestUser)(template))
    }
    }

How to use

Add a dependency declaration into your Build.scala or build.sbt file:

libraryDependencies +="jp.t2v"%%"stackable-controller"%"0.6.0"
  • for Play2.2.x, use 0.3.0
  • for Play2.3.x, use 0.4.1
  • for Play2.4.x, use 0.5.1

ExecutionContext

When you want to use scala.concurrent.ExecutionContext, you can use StackActionExecutionContext method.

StackActionExecutionContext returns an ExecutionContext that is given when StackAction method calling. So, users of your StackElement can customize ExecutionContext.

traitAsyncElementextendsStackableControllerwithAuthConfigImpl {
self: ControllerwithAuth=>privatecaseobjectFooKeyextendsRequestAttributeKey[Foo]
overridedefproceed[A](req: RequestWithAttributes[A])(f: RequestWithAttributes[A] =>Future[Result]):Future[Result] = {
valctx:ExecutionContext=StackActionExecutionContext(req)
valfuture:Future[Foo] = getFooAsynchronously(ctx)
future flatMap { foo =>super.proceed(req.set(FooKey, foo))(f)
} recoverWith {
_ =>super.proceed(req)(f)
}
}
implicitdeffoo(implicitreq: RequestWithAttributes[_]):Foo= req.get(FooKey).get
}

License

This library is released under the Apache Software License, version 2, which should be included with the source in a file named LICENSE.

About

Composable Action composition framework for Play2.x

Resources

Stars

101 stars

Watchers

9 watching

Forks

Releases

Packages

Contributors

Languages