Akka HTTP:自定義指令(Directive)

自定義指令

有3種建立自定義指令的基本方法:html

  1. 將已有指令經過命名配置(好比經過組合的方式)的方式來定義新的指令
  2. 轉換已存在的指令
  3. 從頭開始實現一個指令

命名配置

建立自定義指令最簡便的方法就是將一個或多個已有指令經過配置的方式分配一個新的名字來定義。事實上Akka HTTP預約義的大多數指令都由以較低級別指令命名配置的方式來定義的。如:git

val getPut = get & put

def postEntity[T](um: FromRequestUnmarshaller[T]): Directive1[T] = post & entity(um)

def completeOk: Route = complete(HttpEntity.Empty)

def completeNotImplemented: Route = complete(StatusCodes.NotImplemented)

轉換已存在的指令

第二種方式是經過「轉換方法」來轉換現有指令,這是在Directive類上定義的方法:github

  • map/tmap
  • flatMap/tflatMap
  • require/trequire
  • recover/recoverPF

map、tmap

map、tmap就和Scala集合庫上的map轉換相似,它能夠將值映射轉換成另外一個值。map用於Directive1類型的指令(單值指令),而tmap用於值爲其它元組的狀況,它的簽名以下:web

def tmap[R](f: L => R): Directive[Out]

tmap能夠用來將提取的元組轉換成另外一個元組,提取的數量和類型均可以改變,而map只用改變變換後的類型。以下是一個虛構的例子:api

val twoIntParameters: Directive[(Int, Int)] =
  parameters(("a".as[Int], "b".as[Int]))

val myDirective: Directive1[String] =
  twoIntParameters.tmap {
    case (a, b) => (a + b).toString
  }

// tests:
Get("/?a=2&b=5") ~> myDirective(x => complete(x)) ~> check {
  responseAs[String] mustBe "7"
}

flatMap、tflatMap

經過map、tmap能夠將指令抽取的值轉換成其它值,但不能改變其「抽取」的性質。當須要抽取一個對它作一些轉換操做,並將結果交給一個嵌套的指令使用時,map、tmap就無能爲力了。同map、tmap相似,flatMap也是用於單值指令,而tflatMap用於其它元組值。tflatMap的函數簽名以下:app

def tflatMap[R: Tuple](f: L => Directive[R]): Directive[R]

能夠看一個例子,預約義的method指令,它的定義以下:函數

def method(httpMethod: HttpMethod): Directive0 =
  extractMethod.flatMap[Unit] {
    case `httpMethod` => pass
    case _            => reject(MethodRejection(httpMethod))
  } & cancelRejections(classOf[MethodRejection])

val get: Directive0 = method(HttpMethods.GET)
val post: Directive0 = method(HttpMethods.POST)
  1. 經過調用extractMethod指令獲取請求的HTTP方法,再經過flatMap[Unit]轉換方法對它進行處理。由於extractMethod是一個單值指令且轉換後值爲Unit(也是個單值),這裏調用flatMap方法。
  2. 當請求的實際HTTP方法與傳入參數httpMethod匹配時,調用pass指令使其經過,不然調用reject(MethodRejection(httpMethod))拒絕。

require、trequire

require方法將單個指令轉換爲沒有抽取值的指令,該指令根據謂詞函數過濾請求,全部謂詞函數調用後爲false的請求都被拒絕,其它請求保持不變。它的定義以下:post

def require(predicate: T => Boolean, rejections: Rejection*): Directive0 =
  underlying.filter(predicate, rejections: _*).tflatMap(_ => Empty)

從定義能夠看出,它其實是先經過謂詞函數調用filter方法對請求進行過濾,而後再調用tflatMap函數將指令抽取的值去掉。ui

recover、recoverPF

recover方法容許「捕獲」由底層指令向上冒泡產生的rejections,並生成且有相同抽取類型的替代指令。這樣就能夠恢復指令來經過而不是拒絕它。它們的定義分別以下:scala

def recover[R >: L: Tuple](recovery: immutable.Seq[Rejection] => Directive[R]): Directive[R] =
  Directive[R] { inner => ctx =>
    import ctx.executionContext
    @volatile var rejectedFromInnerRoute = false
    tapply({ list => c => rejectedFromInnerRoute = true; inner(list)(c) })(ctx).fast.flatMap {
      case RouteResult.Rejected(rejections) if !rejectedFromInnerRoute => recovery(rejections).tapply(inner)(ctx)
      case x => FastFuture.successful(x)
    }
  }

def recoverPF[R >: L: Tuple](recovery: PartialFunction[immutable.Seq[Rejection], Directive[R]]): Directive[R] =
  recover { rejections => recovery.applyOrElse(rejections, (rejs: Seq[Rejection]) => RouteDirectives.reject(rejs: _*)) }

從頭開始實現一個指令

能夠經過調用Directive.apply或它的子類型來從頭開始定義一個指令,Directive的簡化定義看起來像下面這樣:

abstract class Directive[L](implicit val ev: Tuple[L]) {
  def tapply(f: L => Route): Route
}

object Directive {

  /**
   * Constructs a directive from a function literal.
   */
  def apply[T: Tuple](f: (T => Route) => Route): Directive[T] =
    new Directive[T] { def tapply(inner: T => Route) = f(inner) }

}

Directive類型有一個抽象方法tapply,參數f是一個函數類型,將類型L傳入並返回RouteDirective的伴身對象提供了apply來實現自定義指令。它的參數是一個高階函數(T => Route) => Route,就像小括號那樣,咱們應把(T => Route)當作一個總體,它是函數參數,返回類型爲Route

f爲咱們自定義指令用於從RequestContext裏抽取值(值的類型爲Tuple[L]),而inner就是f抽取值後調用的嵌套路由,在調用inner時將抽取出的值做爲參數傳入。

對於一個抽取訪問host和port的指令,能夠這樣實現:

def hostnameAndPort: Directive[(String, Int)] = Directive[(String, Int)] { inner => ctx =>
  // inner: (String, Int) => Route
  // ctx: RequestContext

  val authority: Uri.Authority = ctx.request.uri.authority
  val tupleValue: (String, Int) = (authority.host.address(), authority.port)
  val route: Route = inner(tupleValue)
  route(ctx) // Future[RouteResult]
}

Full source at GitHub

讓咱們來分析下這個例子:

  1. 首先是hostnameAndPort指令的類型Directive[(String, Int)],它從請求上下文(RequestContext)中抽取出的值是Tuple2[String, Int]
  2. apply方法執行的代碼參數是:inner => ctx => ....其實能夠當作:inner => ((ctx: RequestContext) => Future[RouteResult])inner就是f函數參數(T => Route)部分。
  3. inner(tupleValue)執行後結果route的類型是Route,這時這段代碼爲的類型就爲inner => ctx => Route,而實際上Directive.apply須要的參數類型爲inner => Route。以前咱們知道,Route是一個類型別名RequestContext => Future[RouteResult],因此咱們須要將ctx => Route轉換爲Route。而將tupleValue做爲參數調用route後將獲取結果類型Future[RouteResult],這段代碼的類型就是inner => ctx => Future[RouteResult] -> inner => Route

本文節選自《Scala Web開發》,更多內容請訪問:https://www.yangbajing.me/scala-web-development/server-api/routing-dsl/custom-directive.html

相關文章
相關標籤/搜索