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 的方式创作 Actions。

但是您应该能够创建一个“组合”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)
}