ディレクティブ

ディレクティブ

"Directive" は、任意の複雑な route structures を作成するのに使われる小さなブロックです。Akka HTTPは予め数多くのディレクティブを定義しています。あなた自身で構築することも簡単に出来ます。

基本

ディレクティブは ルート を作成します。"プリミティブ"なルートの作成方法と比較することで、ディレクティブがどの様に動作するのかを理解する助けになります。

Route が単純な関数型 Route の型エイリアスであるので、インスタンスは様々な方法で関数のインスタンスとして記述する事が出来ます。関数リテラルの例:

val route: Route = { ctx => ctx.complete("yeah") }

より簡潔な例:

val route: Route = _.complete("yeah")

complete ディレクティブを使うことによって、より簡潔になります:

val route = complete("yeah")

三つの方法で記述された Route は全て等価です。作成された route は全てのケースにおいて同じ振る舞いをします。

ここで特に重要なポイントに焦点を当てるために、もっと複雑な例を見てみましょう。これらの二つのルートを考えてみます:

val a: Route = {
  println("MARK")
  ctx => ctx.complete("yeah")
}

val b: Route = { ctx =>
  println("MARK")
  ctx.complete("yeah")
}

ab との違いは、いつ println 文が実行されるかです。 a の場合はルートが構築された時に*一回だけ*実行され、 b の場合はルートが実行される度に実行されます。

complete ディレクティブを使うと、この様に同じ効果が得られます:

val a = {
  println("MARK")
  complete("yeah")
}

val b = complete {
  println("MARK")
  "yeah"
}

これが動作するのは、 complete ディレクティブの引数が by-name によって評価される、つまりルートが実行される度に再実行されるからです。

もう一歩先に進みましょう:

val route: Route = { ctx =>
  if (ctx.request.method == HttpMethods.GET)
    ctx.complete("Received GET")
  else
    ctx.complete("Received something else")
}

getcomplete ディレクティブを使うことによって、ルートをこの様に記述する事ができます:

val route =
  get {
    complete("Received GET")
  } ~
  complete("Received something else")

この場合も、生成されたルートは全てのケースにおいて同じように振舞います。

必要に応じて、二通りのやり方を混在させる事も出来ます:

val route =
  get { ctx =>
    ctx.complete("Received GET")
  } ~
  complete("Received something else")

ここでは、 get ディレクティブ内部のルートは明示的な関数リテラルとして記述されています。

しかしながら、これの例を見ても、"手動"で書いたコードよりもディレクティブを使ってルートを構築する方が、はるかに簡潔で読みやすく、保守しやすくなります。また、より良い記述性を提供します(次の節で説明します)。そして、Akka HTTPのルーティングDSLを使うと、 リクエストコンテキスト を直接操作する Route 関数リテラルを経由したルートの構築に戻る必要はほとんどありません。

構造

ディレクティブの一般的な分析は次の通りです:

name(arguments) { extractions =>
  ... // inner route
}

それは名前を持ち、ゼロかそれ以上の引数と内部ルートを持ちます( RouteDirectives は常に末端で使用され、内部ルートを持つことのできない特別なルートです)。加えて、ディレクティブは値を"抽出"し、内部ルートで関数の引数として利用させる事ができます。"外側から"見えるとき、その内側のルートを持つディレクティブは、 Route 型の式を形成します。

ディレクティブの意味

ディレクティブは以下の一つ以上の事が出来ます:

  • 内部ルートを通過する前に、受診した RequestContext を変換する(即ち、リクエストを変更します)

  • 幾つかのロジックにしたがって、 RequestContext をフィルタする、即ち、特定のリクエストを通過させ、他のリクエストを拒否する

  • RequestContext から値を抽出し、内部ルートで "extractions" として利用できるようにする

  • 幾つかのロジックを ルートリザルト Futureの変換チェーンに連結する(即ち、レスポンスもしくはリジェクションを変更する)

  • リクエストの完了

これは Directive が内部ルートの機能を完全にラップし、リクエスト側とレスポンス側の両方(もしくは一方)に任意の複雑な変換を適用する事が出来ることを意味します。

ディレクティブの作成

注釈

ディレクティブの間の ~ (チルダ)を忘れても、完全に正しいScalaコードとしてコンパイルする事が出来ますが、期待したようには動作しません。単一の式として意図したものは、実際には複数の式であり、最後の式のみが結果として使用されます。あるいは、 concat コンビネータを使用することもできます。 concat(a, b, c)a ~ b ~ c と同じです。

これまでの例の通り、ディレクティブがネストするのは「普通」のやり方です。具体的な例を見てみましょう:

val route: Route =
  path("order" / IntNumber) { id =>
    get {
      complete {
        "Received GET request for order " + id
      }
    } ~
    put {
      complete {
        "Received PUT request for order " + id
      }
    }
  }

この getput ディレクティブは ~ 演算子で連結し、 path ディレクティブの内部ルートとして機能するルートを形成します。この構造をより明確にするために、以下のように書く事が出来ます:

def innerRoute(id: Int): Route =
  get {
    complete {
      "Received GET request for order " + id
    }
  } ~
  put {
    complete {
      "Received PUT request for order " + id
    }
  }

val route: Route = path("order" / IntNumber) { id => innerRoute(id) }

ディレクティブがただのメソッドとして実装されているのではなく、独立した Directive 型のオブジェクトとして実装されていることは、このスニペットからはわかりません。しかし、これによってディレクティブをより柔軟に作成する事が出来ます。例えば、ディテクティブとして | を使うようなこともできます。例を書くもう一つの方法は次の通りです:

val route =
  path("order" / IntNumber) { id =>
    (get | put) { ctx =>
      ctx.complete(s"Received ${ctx.request.method.name} request for order $id")
    }
  }

より良い例( Route 関数を手作業で明示的に書くことを避けた):

val route =
  path("order" / IntNumber) { id =>
    (get | put) {
      extractMethod { m =>
        complete(s"Received ${m.name} request for order $id")
      }
    }
  }

If you have a larger route structure where the (get | put) snippet appears several times you could also factor it out like this:

val getOrPut = get | put
val route =
  path("order" / IntNumber) { id =>
    getOrPut {
      extractMethod { m =>
        complete(s"Received ${m.name} request for order $id")
      }
    }
  }

getOrPut は引数を取らないので、 val として書けることに注意して下さい。

ネストする代わりに & 演算子を使うこともできます。

val getOrPut = get | put
val route =
  (path("order" / IntNumber) & getOrPut & extractMethod) { (id, m) =>
    complete(s"Received ${m.name} request for order $id")
  }

ここでは、抽出を生成するディレクティブが & と連結される時、結果として得られる "super-directive" は単に連結された部分抽出を抽出する事がわかります。

そしてもう一度、因子を分解する事が出来ます。それによって、ディレクティブの「因数分解 」を極端に押し進める事が出来ます。

val orderGetOrPutWithMethod =
  path("order" / IntNumber) & (get | put) & extractMethod
val route =
  orderGetOrPutWithMethod { (id, m) =>
    complete(s"Received ${m.name} request for order $id")
  }

|& 演算子でディレクティブを連結したり、複雑なディレクティブの設定を val として保存することで、全てのディレクティブを内部ルートを取る事ができます。

複雑なディレクティブを一つのディレクティブに「圧縮」する事は、読みやすく、保守可能なルーティングコードを得る最適な方法とは限りません。実際、この節の最初の例が最も読みやすいものであるかもしれません。

それでもなお、ここにあるエクササイズの目的は、どのように柔軟なディレクティブの力を、あなたのアプリケーションに適した抽象レベルで、Webサービスの振る舞いを定義するのに使う事ができるのかを示す事です。

ディレクティブの型安全

|& 演算子を使ってディレクティブを連結すると、ルーティングDSLは全ての抽出を期待した通りに動作させ、論理的な制約をコンパイル時に施します。

例えば、 | ディレクティブで抽出を生成することは出来ません:

val route = path("order" / IntNumber) | get // doesn't compile

また、抽出する数とその種類は一致している必要があります:

val route = path("order" / IntNumber) | path("order" / DoubleNumber)   // doesn't compile
val route = path("order" / IntNumber) | parameter('order.as[Int])      // ok

& 演算子で抽出を生成するディレクティブを連結するとき、全ての抽出は集約されます:

val order = path("order" / IntNumber) & parameters('oem, 'expired ?)
val route =
  order { (orderId, oem, expired) =>
    ...
  }

ディレクティブは、WebサービスロジックをDRYや型安全性を保ちながら、小さなブロックをプラグアンドプレイで構築するための優れた方法を提供します。 豊富な Predefined Directives (alphabetically) でもあなたのニーズを満たせない場合は、 Custom Directives を簡単に作る事ができます。

Contents