Akka-HTTP - ルーティングDSLの基本となるPathMatcherのまとめ
2016-05-17
QiitaScalaAkkaAkka-HTTPPath を構築するための DSL であるDirective
とPathMathcer
について。
ドキュメントは以下。
The PathMatcher DSL — Akka Documentation
tl;dr
大体ここに書いてある。 The PathMatcher DSL — Akka Documentation
spray も参考になる。 Path Filters · spray/spray Wiki
PathMatcher
Path を作るための型といえるもの。
ドキュメントの Overview にあるようにPathMatcher
の型は以下のようになっている。
trait PathMatcher[L: Tuple]
type PathMatcher0 = PathMatcher[Unit]
type PathMatcher1[T] = PathMatcher[Tuple1[T]]
リクエストがあった Path に対してマッチするかどうかの判定を行い、マッチする場合は pass して内側の処理に入り、マッチしなかった場合は reject して次の Path 判定にうつる。
PathMatcherN
はpath
に与えるとDirectiveN
が得られるようになっている。
文字列
Path を定義する基本となるはずのやつ。 文字列でルーティングを定義する。
val route = path("ping") {
get {
complete("pong")
}
}
これにlocalhost:8080/ping
に GET リクエストを送るとpong
と返ってくる。
get
を使用して GET であることを明示しているが、HTTP メソッドは指定しなければ GET となる
正規表現を使いたい場合
path
にRegex
オブジェクトを渡すだけでよい。
val route = path("[abc]+".r) { rgx: String =>
get {
complete(s"matched: $rgx")
}
}
このrgx
には正規表現のグループ数によってバインドされる文字列が変化し、
グループ数が 0 の場合は正規表現として与えた文字列が、1 の場合はそのグループとしてマッチした文字列が得られる。
注意点として、正規表現のグループ数が 1 より大きい場合は使用できず、実行時例外が throw されてしまう。
なおグループ数はこのように求められる。(akka/EnhancedRegex.scala)
"regex".r.pattern.matcher("").groupCount()
グループ数が 0 の場合
val route =
path("hello_.+".r) { rgx: String =>
get {
complete(s"matched: $rgx")
}
}
正規表現にマッチした文字列全体が得られる。
$ curl 'http://localhost:8080/alice'
The requested resource could not be found.
$ curl 'http://localhost:8080/hello_alice'
matched: hello_alice%
グループ数が 1 の場合
a
かb
かc(hoge|foo|bar)
か、というちょっと変わった例。
val route =
path("a|b|c(hoge|foo|bar)+".r) { rgx: String =>
get {
complete(s"matched: $rgx")
}
}
この場合、rgx
にはグループにマッチした文字列が入る。
つまりcfoo
にリクエストを送るとfoo
が得られることとなる。
$ curl 'http://localhost:8080/cfoo'
matched: foo
グループにマッチする文字列が無い場合はnull
が渡ってきてしまう点に注意
$ curl 'http://localhost:8080/a'
matched: null
Path に数値を入れたい場合
用意されているIntNumber
やHexLongNumber
を使えば Path として与えた数字がオブジェクトとして得られる。
val routes = pathPrefix("ping") {
path(IntNumber) { i =>
get {
complete(s"int: $i")
}
} ~
path(LongNumber) { l =>
get {
complete(s"long: $l")
}
} ~
path(DoubleNumber) { d =>
get {
complete(s"double: $d")
}
} ~
path(HexIntNumber) { hi =>
get {
complete(s"hexInt: $hi")
}
} ~
path(HexLongNumber) { hl =>
get {
complete(s"hexLong: $hl")
}
}
}
$ curl http://localhost:8080/ping/1
int: 1
$ curl http://localhost:8080/ping/10000000000000000
long: 10000000000000000
$ curl http://localhost:8080/ping/1.0
double: 1.0
$ curl http://localhost:8080/ping/fff
hexInt: 4095
$ curl http://localhost:8080/ping/ffffffffffff
hexLong: 281474976710655%
なお、path(HexXxxNumber) { ??? }
をpath(XxxNumber) { ??? }
より前に持ってくると
HexXxxNumber
でキャッチされてしまい、XxxNumber
の path には入ってこないため、併用する場合は注意が必要。
Map で Path を作る
path
にはMap[String, T]
を与えることが出来る。
val map: Map[String, Long] = Map(
"one" -> 1L,
"two" -> 2L,
"three" -> 3L
)
val route =
path(map) { l: Long =>
complete(s"long: $l")
}
深い Path を実装する
深い階層になった Path を表現するにはいくつか方法がある。 例としてこのような Path を考える。
GET /nice/user
GET /nice/user/greet
GET /nice/user/hello # .../greetと同じ挙動とする
GET /nice/user/<name>
pathEnd, pathPrefix, pathSuffix を使う
さっそく、例として上げたルーティングを実装するとこのようになる。
val route =
pathPrefix("nice") { // /nice/...
pathPrefix("user") { // /nice/user/...
pathEnd { // nice/user
complete("nice user!")
} ~
pathSuffix("greet" | "hello") { // nice/user/.../greet
complete(s"hello!")
} ~
pathPrefix(".+".r) { name => // /nice/user/<name>/...
pathEndOrSingleSlash { // /nice/user/<name> or /nice/user/<name>/
complete(s"nice name => $name")
}
}
}
}
.../greet
と.../hello
が同じ挙動で、それを表現するには|
を使用すれば良い。
この route に対するリクエストとレスポンスは以下のようになる。
$ curl 'localhost:8080/nice/user'
nice user!
$ curl 'localhost:8080/nice/user/'
The requested resource could not be found.
$ curl 'localhost:8080/nice/user/greet'
hello!
$ curl 'localhost:8080/nice/user/hello'
hello!
$ curl 'localhost:8080/nice/user/greet/'
nice name => greet
$ curl 'localhost:8080/nice/user/alice'
nice name => alice
$ curl 'localhost:8080/nice/user/alice/'
nice name => alice
それぞれの違いを簡単に書くと以下。(ほぼ英単語そのままの意味)
pathEnd
,pathEndOrSingleSlash
- Path の終わり
- 後ろに
/
がつくかどうかの違い
pathPrefix
- Path の先頭に来るもの
PathPrefix
を組み合わせると深い Path を作ることが出来る
pathSuffix
- Path の終端に来るもの
とくにpathSuffix
は終端にあればマッチして処理されてしまうため、思ってない動きをすることもある。
例えば以下のように、適当な Path へのリクエストを正常に処理してしまう。
(もちろん、 ~ path("greet")
とすれば問題ないが、あくまでpathSuffix
を使った例として。)
$ curl 'localhost:8080/nice/user/alice/greet'
hello!
$ curl 'localhost:8080/nice/user/foo/bar/baz/greet'
hello!
path のみで実装する
例のルーティングを Path を結合する~
と Slash 区切りにする/
を組み合わせるとpath
のみを使って実現できる。
val route =
path("nice" / "user") {
complete("nice user!")
} ~
path("nice" / "user" / ("greet" | "hello")) {
complete("hello!")
} ~
path("nice" / "user" / ".+".r ~ Slash.?) { name =>
complete(s"nice name => $name")
}
path
のみでの実装は Play Flamework の routes のようにルーティングが明示的なので人間にはわかりやすいが、コードとして重複が多くメンテナンス性がやや下がってしまうのが難点。
/nice/user/greet
を明示しているので、先述のPathSuffix
を使用した実装とは/nice/user/.../greet
の挙動は変わっている。
$ curl 'localhost:8080/nice/user/alice/greet'
The requested resource could not be found.%
また、path("nice" / "user")
のようなものであればseparateOnSlashes
でも表現できる。
path(separateOnSlashes("nice/user")) {
} ~
path(separateOnSlashes("nice/user") / ("greet" | "hello")) {
...
}
折衷案
pathPrefix
とpath
を組み合わせた一般的な(?)実装が以下。
val route =
pathPrefix("nice" / "user") {
pathEnd {
complete("nice user!")
} ~
path("greet" | "hello") {
complete("hello!")
} ~
path(".+".r ~ Slash.?) { name =>
complete(s"nice name => $name")
}
}
末尾に/
がつくかどうかは~ Slash.?
かpathEndOrSingleSlash
のどちらでも表現できそうだが、
pathEndOrSingleSlash
はその名の通りpathEnd
であるため、その後に Path がまだ続く場合は~ Slash.?
を使う必要が生じる。
複数の PathMatcher1 を組み合わせる
IntNumber
や正規表現などのPathMatcher1
を複数組み合わせることが可能。
path("hi" / IntNumber / ".+".r) { (n: Int, str: String) =>
complete(s"n: $n, str: $str")
}
PathMatcher1
を 2 つ組み合わせるとDirective[Tuple2[A, B]]
となってTuple2
を受け取ることが出来るようになる。
それぞれ型を書くと以下のようになる。(なお IntelliJ だとうまく型解決出来ないっぽくてエラーが出てしまう)
val pathMatcher: PathMatcher[Tuple2[Int, String]] = "hi" / IntNumber / ".+".r
val directive: Directive[Tuple2[Int, String]] = path(pathMatcher)
Segment
PathMatchers.Segment
は他でマッチしなかった Path を拾うためのもの、というイメージ。
関連するのはSegment
, Segment.repeat
, Segments
, Rest
, RestPath
あたり。
Segment
".+".r
と同じように使える- 型は
PathMatcher1[String]
Segment.repeat
Segment
を回数指定(min と max)で繰り返す- 型は
PathMatcher1[List[String]]
Segments
Segment.repeat
を最大 128 回繰り返す- min=0, max=128 な
Segment.repeat
- min=0, max=128 な
Rest
- マッチしていない Path の残りを表す
- 型は
PathMatcher1[String]
で、残りの Path を文字列として得られる
RestPath
Rest
と同じで、型がPathmatcher1[Path]
となっている- 残りの Path に何かしら操作したい場合はこちらを使うべき
使用例としてはこんなところ
val route =
path("foo" / Segment / "yes") { (x: String) =>
complete(s"foo segment: $x")
} ~
path("bar" / Segment / RestPath) { (x: String, y: Path) =>
complete(s"bar segment: $x, $y")
} ~
path("baz" / Segments) { (x: List[String]) =>
complete(s"baz segment: $x")
}
この route に対するリクエストとレスポンスはこうなる。
$ curl 'localhost:8080/foo/hello/yes'
foo segment: hello
$ curl 'localhost:8080/bar/a/b/c'
bar segment: a, b/c
$ curl 'localhost:8080/baz/up/down/left/right'
baz segment: List(up, down, left, right)
from: https://qiita.com/petitviolet/items/b74b89820917df7834ba