Pathを構築するための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)