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)