Play/Scala: 使用orElse 与ActionBuilder 组合?
Play/Scala: use orElse to compose with ActionBuilder?
我们在 play 应用程序中使用 play-pac4j 进行身份验证。
我们希望拥有相同的 route/controller 端点,但具有不同的行为,具体取决于用户角色。
从概念上讲,这会做类似的事情:
val ACTION_ONE: ActionBuilder[Request, AnyContent] = Secure(
JWT_CLIENT, Authorizers.Role1
)(anyContentLarge)
val ACTION_TWO: ActionBuilder[Request, AnyContent] = Secure(
JWT_CLIENT, Authorizers.Role2
)(anyContentLarge)
def index = ACTION_ONE.async{ req => index1(req) } orElse ACTION_TWO.async{ req => index2(req) }
def index1(req: Request[AnyContent]) = //... behavior with role1
def index2(req: Request[AnyContent]) = //... behavior with role2
但是PlayActions
的作文只提供了andThen
,没有orElse
。
有办法实现吗?
我不认为你能够以 orElse
的方式创作 Action
s。
但是您应该能够创建一个“组合”ActionBuilder
,它使用您现有的 2 个 ActionBuilder
并执行 orElse
逻辑。虽然你只能为 运行 提供一具尸体。而这个机构将不得不依靠 AuthenticatedRequest#profiles
之类的东西来确定要做什么。
类似于:
def index = ACTION_COMBINED.async{ req: AuthenticatedRequest =>
// Check something on req.profiles
if (...) index1(req) else index2(req)
}
更准确地说,我对 play-pac4j 不熟悉
所以我终于实现了它:)
它使用了 'fallBackToNext',我们的代码库中已经有了一个方法,它的行为类似于 fallBackTo 但带有一个异步 lambda 参数,因此仅当第一个 future 已经失败时才会执行下一个 future(防止大在不需要时进行计算,但会降低并行度)。
以下是大部分逻辑:
/**
* This combination of action is the implementation class of the "orElse" operator,
* allowing to have one and only one action to be executed within the given actions
*/
class EitherActions[A](actions: Seq[Action[A]]) extends Action[A] {
require(actions.nonEmpty, "The actions to combine should not be empty")
override def parser: BodyParser[A] = actions.head.parser
override def executionContext: ExecutionContext = actions.head.executionContext
/**
* @param request
* @return either the first result to be successful, or the first to be failure
*/
override def apply(
request: Request[A]
): Future[Result] = {
// as we know actions is nonEmpty, we can start with actions.head and directly fold on actions.tail
// this removes the need to manage an awkward "zero" value in the fold
val firstResult = actions.head.apply(request)
// we wrap all apply() calls into changeUnauthorizedIntoFailure to be able to use fallbackToNext on 403
val finalResult = actions.tail.foldLeft( changeUnauthorizedIntoFailure(firstResult) ) {
( previousResult, nextAction ) =>
RichFuture(previousResult).fallbackToNext{ () =>
changeUnauthorizedIntoFailure(nextAction.apply(request))
}(executionContext)
}
// restore the original message
changeUnauthorizedIntoSuccess(finalResult)
}
/**
* to use fallBackToNext, we need to have failed Future, thus we change the Success(403) into a Failure(403)
* we keep the original result to be able to restore it at the end if none of the combined actions did success
*/
private def changeUnauthorizedIntoFailure(
before: Future[Result]
): Future[Result] = {
val after = before.transform {
case Success(originalResult) if originalResult.header.status == Unauthorized =>
Failure(EitherActions.UnauthorizedWrappedException(originalResult = originalResult))
case Success(originalResult) if originalResult.header.status == Forbidden =>
Failure(EitherActions.UnauthorizedWrappedException(originalResult = originalResult))
case keepResult@_ => keepResult
}(executionContext)
after
}
/**
* after the last call, if we still have a UnauthorizedWrappedException, we change it back to a Success(403)
* to restore the original message
*/
private def changeUnauthorizedIntoSuccess(
before: Future[Result]
): Future[Result] = {
val after = before.transform {
case Failure(EitherActions.UnauthorizedWrappedException(_, _, result)) => Success(result)
case keepResult@_ => keepResult
}(executionContext)
after
}
def orElse( other: Action[A]): EitherActions[A] = {
new EitherActions[A]( actions :+ other)
}
}
object EitherActions {
private case class UnauthorizedWrappedException(
private val message: String = "",
private val cause: Throwable = None.orNull,
val originalResult: Result,
) extends Exception(message, cause)
}
我们在 play 应用程序中使用 play-pac4j 进行身份验证。
我们希望拥有相同的 route/controller 端点,但具有不同的行为,具体取决于用户角色。
从概念上讲,这会做类似的事情:
val ACTION_ONE: ActionBuilder[Request, AnyContent] = Secure(
JWT_CLIENT, Authorizers.Role1
)(anyContentLarge)
val ACTION_TWO: ActionBuilder[Request, AnyContent] = Secure(
JWT_CLIENT, Authorizers.Role2
)(anyContentLarge)
def index = ACTION_ONE.async{ req => index1(req) } orElse ACTION_TWO.async{ req => index2(req) }
def index1(req: Request[AnyContent]) = //... behavior with role1
def index2(req: Request[AnyContent]) = //... behavior with role2
但是PlayActions
的作文只提供了andThen
,没有orElse
。
有办法实现吗?
我不认为你能够以 orElse
的方式创作 Action
s。
但是您应该能够创建一个“组合”ActionBuilder
,它使用您现有的 2 个 ActionBuilder
并执行 orElse
逻辑。虽然你只能为 运行 提供一具尸体。而这个机构将不得不依靠 AuthenticatedRequest#profiles
之类的东西来确定要做什么。
类似于:
def index = ACTION_COMBINED.async{ req: AuthenticatedRequest =>
// Check something on req.profiles
if (...) index1(req) else index2(req)
}
更准确地说,我对 play-pac4j 不熟悉
所以我终于实现了它:)
它使用了 'fallBackToNext',我们的代码库中已经有了一个方法,它的行为类似于 fallBackTo 但带有一个异步 lambda 参数,因此仅当第一个 future 已经失败时才会执行下一个 future(防止大在不需要时进行计算,但会降低并行度)。
以下是大部分逻辑:
/**
* This combination of action is the implementation class of the "orElse" operator,
* allowing to have one and only one action to be executed within the given actions
*/
class EitherActions[A](actions: Seq[Action[A]]) extends Action[A] {
require(actions.nonEmpty, "The actions to combine should not be empty")
override def parser: BodyParser[A] = actions.head.parser
override def executionContext: ExecutionContext = actions.head.executionContext
/**
* @param request
* @return either the first result to be successful, or the first to be failure
*/
override def apply(
request: Request[A]
): Future[Result] = {
// as we know actions is nonEmpty, we can start with actions.head and directly fold on actions.tail
// this removes the need to manage an awkward "zero" value in the fold
val firstResult = actions.head.apply(request)
// we wrap all apply() calls into changeUnauthorizedIntoFailure to be able to use fallbackToNext on 403
val finalResult = actions.tail.foldLeft( changeUnauthorizedIntoFailure(firstResult) ) {
( previousResult, nextAction ) =>
RichFuture(previousResult).fallbackToNext{ () =>
changeUnauthorizedIntoFailure(nextAction.apply(request))
}(executionContext)
}
// restore the original message
changeUnauthorizedIntoSuccess(finalResult)
}
/**
* to use fallBackToNext, we need to have failed Future, thus we change the Success(403) into a Failure(403)
* we keep the original result to be able to restore it at the end if none of the combined actions did success
*/
private def changeUnauthorizedIntoFailure(
before: Future[Result]
): Future[Result] = {
val after = before.transform {
case Success(originalResult) if originalResult.header.status == Unauthorized =>
Failure(EitherActions.UnauthorizedWrappedException(originalResult = originalResult))
case Success(originalResult) if originalResult.header.status == Forbidden =>
Failure(EitherActions.UnauthorizedWrappedException(originalResult = originalResult))
case keepResult@_ => keepResult
}(executionContext)
after
}
/**
* after the last call, if we still have a UnauthorizedWrappedException, we change it back to a Success(403)
* to restore the original message
*/
private def changeUnauthorizedIntoSuccess(
before: Future[Result]
): Future[Result] = {
val after = before.transform {
case Failure(EitherActions.UnauthorizedWrappedException(_, _, result)) => Success(result)
case keepResult@_ => keepResult
}(executionContext)
after
}
def orElse( other: Action[A]): EitherActions[A] = {
new EitherActions[A]( actions :+ other)
}
}
object EitherActions {
private case class UnauthorizedWrappedException(
private val message: String = "",
private val cause: Throwable = None.orNull,
val originalResult: Result,
) extends Exception(message, cause)
}