パスのマッチング
このレッスンでは、/users/{id} のような URL パターンが実際のパスとどう照合されているのかを掴み、その照合そのものを自分で書きます。
パス変数は、パターンの穴です
REST API では、対象を識別する値を URL の中に埋め込みます。
Java
@RestController
public class UserController {
@GetMapping("/users/{id}")
public User find(@PathVariable Long id) {
return userService.findById(id);
}
@GetMapping("/users/{userId}/posts/{postId}")
public Post findPost(@PathVariable Long userId, @PathVariable Long postId) {
return postService.find(userId, postId);
}
}{id} の部分がパス変数です。これは「ここには何か 1 つの値が入る」という穴で、@PathVariable を付けた引数がその穴の中身を受け取ります。中かっこの中の名前と引数名が一致していれば、対応付けは自動で行われます。名前が違う場合は @PathVariable("id") Long userId のように明示します。
Long と書けば、String から Long への変換も Spring がやってくれます。数字でない値が来た場合は変換に失敗し、コントローラのメソッドは呼ばれずに 400 Bad Request が返ります。
照合は「区切りごとの突き合わせ」です
パターンと実際のパスをどう突き合わせているのか、素朴に考えると次の手順になります。
- どちらもスラッシュで区切って断片の列にする
- 断片の数が違えば、その時点で不一致
- 前から順に 1 つずつ比べる
- パターン側の断片が中かっこで囲まれていれば、実際の側の断片は何でもよい。その値を変数として覚えておく
- 中かっこでなければ、文字列として完全に一致していなければ不一致
- 最後まで比べ終わったら一致
たとえばパターン /users/{id}/posts と実際のパス /users/12/posts を突き合わせると、断片はどちらも 3 つで、1 つめの users は文字列一致、2 つめは {id} なので 12 を覚えて通過、3 つめの posts は文字列一致となり、一致と判定されます。
一方で /users/12 を同じパターンに当てると、断片が 2 つしかないので 2 番目の手順で不一致になります。
| パターン | 実際のパス | 結果 |
|---|---|---|
| /users/{id} | /users/12 | 一致 id は 12 |
| /users/{id} | /users/12/posts | 不一致 (断片の数が違う) |
| /users/{id} | /orders/12 | 不一致 (users と orders) |
| /users/{userId}/posts/{postId} | /users/3/posts/9 | 一致 userId は 3、postId は 9 |
Spring が実際に使っている照合はもっと高機能で、* や ** のようなワイルドカード、拡張子の指定、複数のパターンが同時に当たったときにどちらを優先するかという順位付けまで持っています。しかし骨格はここに書いた手順そのものです。
演習でやること
パターンと実際のパスの 2 つの文字列を受け取り、照合して結果を返す関数を書きます。分割・変数名の抽出・連結は用意済みです。一致した場合は補助メソッドで取り出した変数を id=12 のような形でつなぎ、変数が複数あればカンマで区切ります。一致しない場合は no-match を返します。
Spring では @GetMapping("/users/{id}") と @PathVariable を書くだけで済んでいる処理を、ここでは照合の判断を自分で書きます。書き終えたあとにもう一度アノテーションを見ると、あの 2 行がどれだけの仕事を隠しているのかが分かるはずです。
演習で使うひな形
splitPath("/users/12/") は "users" と "12" の配列を返します。variableName("{id}") は "id"、variableName("users") は null です。断片数と固定部分の一致を確かめた後に formatVariables を呼ぶと、例えば "id=12" が返ります。切り出しや連結は用意済みで、書くのは照合の判断です。
要件
- pattern または path が null なら no-match を返すこと
- patterns と values の長さを比較すること
- variableName が null の断片だけを固定部分として equals で照合すること
- すべて一致したら formatVariables の結果を返すこと