KtorのLocationsの使い方

最近Ktorを触りだしたのですが、あまりにもドキュメントがなかったので`Locations`で試したものを書いてみました。
Versionは`0.9.0`です。

# Ktorとは？
Kotlin製のWeb frameworkです。  
`Easy to use, fun and asynchronous.`を謳っており、現状まだExperimentalなKotlinの言語機能`coroutine`を利用した実装がされています。  
Getting startedや詳細は、[ktor.io](http://ktor.io/)や[GitHubのリポジトリ](https://github.com/ktorio/ktor)を参照してください。

# Locationsとは？
Ktorは`Feature`と呼ばれるもの必要に応じて`install`することでWebアプリケーションに必要な機能を拡張するようになっています。[(公式ページ)](http://ktor.io/features/index.html)  
`Feature`は[ここ](https://github.com/ktorio/ktor/tree/master/ktor-features)などで実装が進められており、`Auth`, `WebSocket`, `Jackson`, `Gson`, `Guice`, `HtmlBuilder`などがあります。

`Locations`は`Feature`のうちのひとつで、`install`することでType safe routingを実現できます。  
でも[公式](http://ktor.io/features/locations.html)にあるのは空のページだけ…  
なので、[`ktor-locations`](https://github.com/ktorio/ktor/tree/master/ktor-features/ktor-locations)の実装やテストコードを読みながら試しました。

## installとは？

前述の通り、Webアプリケーションに機能を追加するための仕組みです。  
コードでは以下のようになります。

```kotlin
// アプリケーションのエントリポイント
fun Application.main() {
    install(Locations) // ← Locations featureをinstall

    install(WebSockets) { // ← WebSockets featureをinstall
        pingPeriod = Duration.ofMinutes(1) // ← WebSocketsの設定
    }

    install(Routing) { // ← Routing featureをinstall
        get("/") { // ← GET `/` の時の処理を定義
            call.respond("Hello world from Ktor!")
        }
    }
}
```
このように必要な機能を`install`することで、その機能が使えるようになります。  
Webアプリケーションに必須な`Routing`でさえも`Feature`として提供されています。  
自前で`Feature`の実装もできるようで、[簡単なサンプル](https://github.com/ktorio/ktor/tree/master/ktor-samples/ktor-samples-custom-feature)もあります。  

# Locations featureの使い方

## 何ができるのか？
公式のドキュメントがまだないので自分の理解できている範囲での話になりますが、

- パスを`class`や`object`を使って構造化でき、一箇所にまとめることができるようにする
- 型安全にリクエストパラメータやクエリパラメータを扱えるようにする
- 各パスでの処理の指定を`String literal`ではなく、構造化された`class`や`object`で行えるようにする

などかなと思っています。

以下のエンドポイント(GET)でのパス、リクエストパラメータ、クエリパラメータの扱いをSpringと比較してみます。  

- /users
- /users/search
- /users/{id}

`User`クラスや`UserService`クラス、そのメソッドなどはある前提で🙏

###  Spring

```kotlin
@RestController
@RequestMapping("/users")
class UserController(private val userService: UserService) {
    @GetMapping
    fun index(): List<User> {
        val users = userService.findAll()
        return users
    }

    @RequestMapping(value = "/search", method = RequestMethod.GET)
    fun search(@RequestParam(name = "id", required = false) id: Int?, @RequestParam(name = "name", required = false) name: String?): List<User> {
        val users = userService.search(id, name)
        return users
    }

    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    fun get(@PathVariable int id): User {
        val user = userService.find(id)
        return user
    }
}
```

### Locations

```kotlin
// エントリポイント
fun Route.main() {
    install(Locations)
    install(Routing) {
        users()
    }
}

// /users のパスを組み立てる
@location("/users") class Users {
    @location("/search") data class Search(val id: Int? = null, val name: String? = null)
    @location("/{id}") data class Id(val id: Int)
}

// /users 以下の実装
fun Route.users() {
    val userService = UserService()

    get<Users> {
        val users = userService.findAll()
        call.respond(users)
    }

    get<Users.Search> {
        val users = userService.search(it.id, it.name)
        call.respond(users)
    }

    get<Users.Id> {
        val user userService.find(it.id)
        call.respond(user)
    }
}
```

ひとつずつ見ていきましょう。

## パスの構造化とリクエスト・クエリパラメータの扱い

Springでは`@RequestMapping`などでパスを指定し、`UserController`クラス全体を使って構造化しています。

```kotlin
@RequestMapping("/users") // ← ここ
class UserController(private val userService: UserService) {
    @GetMapping // ← ここ
    fun index(): List<User> {...}

    @RequestMapping(value = "/search", method = RequestMethod.GET) // ← ここ
    fun search(@RequestParam(name = "id", required = false) id: Int?, @RequestParam(name = "name", required = false) name: String?): List<User> {...}

    @RequestMapping(value = "/{id}", method = RequestMethod.GET) // ← ここ
    fun get(@PathVariable int id): User {...}
}
```

それに対し、KtorのLocations featureでは`@location`をつけたクラスで構造化します。

```kotlin
// /users の定義
@location("/users") class Users {
    // /users/search?id={id}&name={name} の定義
    @location("/search") data class Search(val id: Int? = null, val name: String? = null)
    // /users/{id} の定義
    @location("/{id}") data class Id(val id: Int)
}
```

`@location`に渡す`String`でパスを表し、`@location`をつけたclassのフィールドでリクエストパラメータやクエリパラメータの値を扱うことができるようになります。  
上記の通り、内部クラスに`@location`をつけることでネストしたパスも表現ができます。  
また、以下のように定義することもできます。

```kotlin
@location("/users/search") data class UserSearch(val id: Int? = null, val name: String? = null)
@location("/users/{id}") data class UserId(val id: Int)
```

ちなみに、`object`でも定義できます。

```kotlin
@location("/single") objet Signle
```

`@RequestMapping`と違い、ここでHttp Methodを指定することはできません。  

### クエリパラメータの扱い

Nullableは普段通り`?`の有り無しで表現します。
Requiredはデフォルト引数を設定するかどうかで表現するようです。

```kotlin
// Non nullかつ`name`が必須
@location("/non-null-required") data class NonNullRequired(val name: String)
// Non nullかつ`name`が必須ではない
@location("/non-null-option") data class NonNullOption(val name: String = "hoge")
// Nullableかつ`name`が必須
@location("/nullable-required") data class NullableRequired(val name: String?)
// Nullableかつ`name`が必須ではない
@location("/nullable-required") data class NullableOption(val name: String? = null)
```

なので[上記の実装](#locations)から、`Search`classのデフォルト引数を無くして、

```kotlin
@location("/search") data class Search(val id: Int? = null, val name: String? = null)
// ↓こうする
@location("/search") data class Search(val id: Int?, val name: String?)
```
`/users/search`にクエリパラメータなしでリクエストを送ると`/users/{id}`にマッチしてしまい、`search`は数字じゃないと言われます。

同一名のクエリパラメータを複数扱いたい場合は、`List`で受け取ることができます。

```kotlin
@location("/multiple-id") data class MultipleId(val id: List<Int>)
```

### リクエストパラメータの扱い

リクエストパラメータは`@location`に渡す文字列のうち、`{}`で囲ったものをフィールドにマッピングすることができます。  
リクエストパラメータは1つ以上指定でき、

```kotlin
@location("/users/{userId}/friends/{friendName}") data class UsersFriend(val userId: Int, val friendName: String)
```  

のように定義することも可能です。  

また、同一のリクエストパラメータ名を指定して`List`として受け取ることもできます。  

```kotlin
@location("/users/{id}/{id}") data class UserIds(val id: List<Int>)
```

ちなみに、

```kotlin
get<UserIds> {
    call.respond(it.id)
}
```
に対して`/users/1/2?id=3&id=4`のようにアクセすると`[3, 4, 1, 2]`が返ってきます。
クエリパラメータに対して処理をしてからリクエストパラメータの処理をしているようです。  

`{}`で囲う文字とフィールド名は同じである必要があり、一致していない場合はアプリケーション起動時に`io.ktor.locations.RoutingException`が発生しますが、ビルドは問題なく通ってしまいます。(もちろんIntelliJ IDEAも教えてくれない)
また、リクエストパラメータがフィールドの型と合わない場合は`java.lang.NumberFormatException`などが発生します。

### パラメータを持つクラスのネスト

classを分けて構造化できるので、パラメータを持つクラスの中にさらにクラスを作ることができます。
その時は、1つ上の階層のクラスをフィールドとして持っていないといけないようです。

```kotlin
@location("/users") class Users {
    @location("/{id}") data class Id(val id: Int) {
        @location("/friends") data class Friends(val id: Id) {
            @location("/{friendId}") data class FriendId(val friendId: Int, val friend: Friend)
        }
    }
}
```
最後の`FriendId`classを`Id`classにすることはできません。

また、このような定義はビルド、起動までできますが、ランタイムでエラーになります。

```kotlin
@location("/users") class Users {
    // このリクエストパラメータ名と
    @location("/{id}") data class Id(val id: Int) {
        @location("/friends") data class Friends(val id: Id) {
            // このリクエストパラメータ名が一緒
            // 🔥ここにアクセスするとエラーになる🔥
            @location("/{id}") data class FriendId(val id: Int, val friend: Friend)
        }
    }
}
```

## 各パスでの処理の指定

Springの場合、`@RequestMapping`などが同時に処理の指定となります。

```kotlin
    @GetMapping // ← ここ
    fun index(): List<User> {...}

    @RequestMapping(value = "/search", method = RequestMethod.GET) // ← ここ
    fun search(@RequestParam(name = "id", required = false) id: Int?, @RequestParam(name = "name", required = false) name: String?): List<User> {...}

    @RequestMapping(value = "/{id}", method = RequestMethod.GET) // ← ここ
    fun get(@PathVariable int id): User {...}
```

Location featureの場合、`@location`を付与したclassと`io.ktor.locations`packageにあるメソッドを使用して指定します。

```kotlin
    // /users GETの処理
    get<Users> {}

    // /users/search GETの処理
    get<Users.Search> {}

    // /users/{id} GETの処理
    get<Users.Id>
```

`io.ktor.locations`packageにはHttp Methodに対応する以下のメソッドが定義してあります。  

- get
- post
- put
- patch
- delete
- head
- options

メソッド名が違うだけですべて定義は以下のようになっており、

```kotlin
inline fun <reified T : Any> Route.get(noinline body: suspend PipelineContext<Unit, ApplicationCall>.(T) -> Unit): Route
```
イメージ的には

```kotlin
Users.Search().let {
    hoge(it.name)
    piyo(it.id)
}
```
のように使うことができます。  

もちろん名前をつけることができるので、

```kotlin
get<Users.Search> { param ->
    val users = userList.filter { it.id == param.id || it.name == param.name }
    call.respond(users)
}
```
のように書くこともできます。

- - -

疲れたのでここまで。Springの例とか記憶を頼りに書いたので間違っているかもしれない。
Ktorのドキュメントがほんとにないので、調べたらちょくちょく書いていきたい。

```kotlin
install(Route) {
    route("/users") {
        get {...}
        post {...}
        route("/{id}") {
            get {...}
            patch {...}
            route("/friends") {
                get {...}
                post {...}
            }
        }
    }
}
```
みたいなことを`Locations`でできないのかなぁと思ってるけど、わからないや。