Last active
November 8, 2017 16:09
-
-
Save vdebergue/b03615efd59688700a5a10dfcf065feb to your computer and use it in GitHub Desktop.
NAction: composable action builders for play framework
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package core.controllers.utils | |
import akka.stream.Materializer | |
import akka.util.ByteString | |
import play.api.libs.streams.Accumulator | |
import play.api.mvc._ | |
import scala.concurrent.Future | |
/** | |
* NAction are a replacement to play.api.mvc.Action. | |
* NAction allow to have a check directly on the request headers before applying the body parser | |
* | |
* Using the NActionBuilder you also have a better way to compose action with `or` and `and` operators | |
*/ | |
abstract class NAction[A, Tag]( | |
protected val controllerComponents: ControllerComponents, | |
materializer: Materializer | |
) extends EssentialAction { | |
self => | |
def check(rh: RequestHeader): Future[Either[Result, Tag]] | |
def parser: BodyParser[A] | |
def apply(request: RequestWithTag[A, Tag]): Future[Result] | |
def apply(rh: RequestHeader): Accumulator[ByteString, Result] = { | |
val futAcc: Future[Accumulator[ByteString, Result]] = check(rh).map { | |
case Left(r) => Accumulator.done[Result](r) | |
case Right(tag) => | |
val action = new Action[A] { | |
override val executionContext = controllerComponents.executionContext | |
override val parser = self.parser | |
def apply(request: Request[A]) = | |
try { | |
self.apply(RequestWithTag(request, tag)) | |
} catch { | |
case e: NotImplementedError => throw new RuntimeException(e) | |
} | |
} | |
action.apply(rh) | |
}(controllerComponents.executionContext) | |
Accumulator.flatten(futAcc)(materializer) | |
} | |
} | |
sealed trait CheckResult[+Tag] | |
object CheckResult { | |
case object NotApplicable extends CheckResult[Nothing] | |
case class Ok[+Tag](tag: Tag) extends CheckResult[Tag] | |
case class Invalid(result: Result) extends CheckResult[Nothing] | |
} | |
/** | |
* This class wraps a Request and allow to set some tag that is extracted from the request headers | |
*/ | |
case class RequestWithTag[A, Tag](request: Request[A], tag: Tag) extends WrappedRequest(request) | |
object NAction { | |
def async[A, Tag](checkFunction: RequestHeader => Future[Either[Result, Tag]])(bodyParser: BodyParser[A])( | |
block: RequestWithTag[A, Tag] => Future[Result] | |
)(implicit cc: ControllerComponents, mat: Materializer): NAction[A, Tag] = | |
new NAction[A, Tag](cc, mat) { | |
def parser = bodyParser | |
def check(rh: RequestHeader) = checkFunction(rh) | |
def apply(request: RequestWithTag[A, Tag]): Future[Result] = block(request) | |
} | |
def apply[A, Tag](check: RequestHeader => Future[Either[Result, Tag]])(bodyParser: BodyParser[A])( | |
block: RequestWithTag[A, Tag] => Result | |
)(implicit cc: ControllerComponents, mat: Materializer): NAction[A, Tag] = | |
async(check)(bodyParser) { req => | |
Future.successful(block(req)) | |
} | |
} | |
abstract class NActionBuilder[+Tag]( | |
implicit val controllerComponents: ControllerComponents, | |
materializer: Materializer | |
) { self => | |
implicit val ec = controllerComponents.executionContext | |
/** Check to execute before parsing the body of the request */ | |
def partial(rh: RequestHeader): Future[CheckResult[Tag]] | |
// Functions to build an NAction from this builder: | |
/** Function to instantiate a NAction from the NActionBuilder */ | |
def async[A, T1 >: Tag](bodyParser: BodyParser[A])(block: RequestWithTag[A, T1] => Future[Result]): NAction[A, T1] = | |
new NAction[A, T1](controllerComponents, materializer) { | |
def parser = bodyParser | |
def check(rh: RequestHeader) = self.partial(rh).map { | |
case CheckResult.Ok(tag) => Right(tag) | |
case CheckResult.Invalid(result) => Left(result) | |
case CheckResult.NotApplicable => sys.error("Match Error") | |
} | |
def apply(request: RequestWithTag[A, T1]): Future[Result] = block(request) | |
} | |
/** Contruct a NAction with a body parser and a block */ | |
def apply[A, T1 >: Tag](bodyParser: BodyParser[A])(block: RequestWithTag[A, T1] => Result): NAction[A, T1] = | |
async[A, T1](bodyParser) { req: RequestWithTag[A, T1] => | |
Future.successful(block(req)) | |
} | |
// Functions to compose the builder: | |
/** Execute the other check after the first one succeded */ | |
def andThenCheck[S](other: (Tag, RequestHeader) => Future[CheckResult[S]]): NActionBuilder[S] = | |
new NActionBuilder[S] { | |
def partial(rh: RequestHeader) = self.partial(rh).flatMap { | |
case CheckResult.Ok(tag) => other(tag, rh) | |
case invalid: CheckResult.Invalid => Future.successful(invalid) | |
case CheckResult.NotApplicable => Future.successful(CheckResult.NotApplicable) | |
} | |
} | |
/** Execute the other check if first one was not defined */ | |
def orElseCheck[T1 >: Tag](other: RequestHeader => Future[CheckResult[T1]]): NActionBuilder[T1] = | |
new NActionBuilder[T1] { | |
def partial(rh: RequestHeader) = self.partial(rh).flatMap { | |
case ok: CheckResult.Ok[_] => Future.successful(ok) | |
case invalid: CheckResult.Invalid => Future.successful(invalid) | |
case CheckResult.NotApplicable => other(rh) | |
} | |
} | |
/** Execute the other builder if first one was not defined */ | |
def orElse[T1 >: Tag](other: NActionBuilder[T1]): NActionBuilder[T1] = new NActionBuilder[T1] { | |
def partial(rh: RequestHeader) = self.partial(rh).flatMap { | |
case ok: CheckResult.Ok[_] => Future.successful(ok) | |
case invalid: CheckResult.Invalid => Future.successful(invalid) | |
case CheckResult.NotApplicable => other.partial(rh) | |
} | |
} | |
} | |
object NActionBuilder { | |
def fromPartial[Tag]( | |
check: PartialFunction[RequestHeader, Future[Either[Result, Tag]]] | |
)(implicit controllerComponents: ControllerComponents, mat: Materializer): NActionBuilder[Tag] = | |
new NActionBuilder[Tag] { | |
def partial(rh: RequestHeader) = | |
if (check.isDefinedAt(rh)) check.apply(rh).map { | |
case Right(tag) => CheckResult.Ok(tag) | |
case Left(result) => CheckResult.Invalid(result) | |
} else Future.successful(CheckResult.NotApplicable) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** Method to extract the api key from a request and to test its validity */ | |
val apiKeyChecker = NActionBuilder.fromPartial[ApiKey] { | |
case request if request.headers.get(settings.apiKeyHeader).isDefined => | |
val apiKey = new ApiKey(request.headers.get(settings.apiKeyHeader).get) | |
apiKeyService.isValid(apiKey).map { | |
case true => Right(apiKey) | |
case false => | |
implicit val messages = messagesApi.preferred(request) | |
Left(ApiErrors.invalidAuth.toResult) | |
} | |
} | |
val ForbiddenRecover = NActionBuilder.fromPartial[Nothing] { | |
case request => | |
implicit val messages = messagesApi.preferred(request) | |
Future.successful(Left(ApiErrors.noRights.toResult)) | |
} | |
/** Action that will get the api key from the request or return a forbidden result */ | |
val withApiKey = apiKeyChecker orElse ForbiddenRecover |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment