10
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

Pathを構築するためのDSLであるDirectivePathMathcerについて。
ドキュメントは以下。
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判定にうつる。
PathMatcherNpathに与えるとDirectiveNが得られるようになっている。

文字列

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

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

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

正規表現を使いたい場合

pathRegexオブジェクトを渡すだけでよい。

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の場合

abc(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に数値を入れたい場合

用意されているIntNumberHexLongNumberを使えば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")) {
    ...
  }

折衷案

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

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
  • 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)
10
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?