[Akka-HTTP]ルーティングDSLの基本となるPathMatcherのまとめ


Pathを構築するためのDSLである`Directive`と`PathMathcer`について。
ドキュメントは以下。
[The PathMatcher DSL — Akka Documentation](http://doc.akka.io/docs/akka/2.4.4/scala/http/routing-dsl/path-matchers.html)

# tl;dr

大体ここに書いてある。
[The PathMatcher DSL — Akka Documentation](http://doc.akka.io/docs/akka/2.4.4/scala/http/routing-dsl/path-matchers.html)

sprayも参考になる。
[Path Filters · spray/spray Wiki](https://github.com/spray/spray/wiki/Path-Filters)

# PathMatcher

Pathを作るための型といえるもの。
ドキュメントのOverviewにあるように`PathMatcher`の型は以下のようになっている。

```scala
trait PathMatcher[L: Tuple]
type PathMatcher0 = PathMatcher[Unit]
type PathMatcher1[T] = PathMatcher[Tuple1[T]]
```

リクエストがあったPathに対してマッチするかどうかの判定を行い、マッチする場合はpassして内側の処理に入り、マッチしなかった場合はrejectして次のPath判定にうつる。
`PathMatcherN`は`path`に与えると`DirectiveN`が得られるようになっている。

# 文字列

Pathを定義する基本となるはずのやつ。
文字列でルーティングを定義する。

```scala
val route = path("ping") {
  get {
    complete("pong")
  }
}
```

これに`localhost:8080/ping`にGETリクエストを送ると`pong`と返ってくる。
`get`を使用してGETであることを明示しているが、HTTPメソッドは指定しなければGETとなる

# 正規表現を使いたい場合

`path`に`Regex`オブジェクトを渡すだけでよい。

```scala
val route = path("[abc]+".r) { rgx: String =>
  get {
    complete(s"matched: $rgx")
  }
}
```

この`rgx`には正規表現のグループ数によってバインドされる文字列が変化し、
グループ数が0の場合は正規表現として与えた文字列が、1の場合はそのグループとしてマッチした文字列が得られる。
注意点として、正規表現のグループ数が1より大きい場合は使用できず、実行時例外がthrowされてしまう。
なおグループ数はこのように求められる。([akka/EnhancedRegex.scala](https://github.com/akka/akka/blob/master/akka-http-core/src/main/scala/akka/http/impl/util/EnhancedRegex.scala))

```scala
"regex".r.pattern.matcher("").groupCount()
```

### グループ数が0の場合

```scala
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)`か、というちょっと変わった例。

```scala
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として与えた数字がオブジェクトとして得られる。

```scala
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]`を与えることが出来る。

```scala
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を使う

さっそく、例として上げたルーティングを実装するとこのようになる。

```scala
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`のみを使って実現できる。

```scala
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`でも表現できる。

```scala
path(separateOnSlashes("nice/user")) {
} ~
  path(separateOnSlashes("nice/user") / ("greet" | "hello")) {
    ...
  }
```

## 折衷案

`pathPrefix`と`path`を組み合わせた一般的な(?)実装が以下。

```scala
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`を複数組み合わせることが可能。

```scala
path("hi" / IntNumber / ".+".r) { (n: Int, str: String) =>
  complete(s"n: $n, str: $str")
}
```

`PathMatcher1`を2つ組み合わせると`Directive[Tuple2[A, B]]`となって`Tuple2`を受け取ることが出来るようになる。
それぞれ型を書くと以下のようになる。(なおIntelliJだとうまく型解決出来ないっぽくてエラーが出てしまう)

```scala
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`
- `Rest`
  - マッチしていないPathの残りを表す
  - 型は`PathMatcher1[String]`で、残りのPathを文字列として得られる
- `RestPath`
  - `Rest`と同じで、型が`Pathmatcher1[Path]`となっている
  - 残りのPathに何かしら操作したい場合はこちらを使うべき   

使用例としてはこんなところ

```scala
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)
```