Finch  ライブラリー使ってみた

# はじめに

あるきっかけで  [Finch](https://github.com/finagle/finch) を使う機会があったのですが、
なれない形式なのか色々と四苦八苦。そのまとめになります。

Finch と書いていますが、finchx を使っています。
version 0.25 から finch, finchx の2種類になり、
finchx のほうは Cats Effects を使えるようになっています。

Cats とかよくわからないけど、とりあえず動かしてみたい。
私がそういう人だったのでそういう人向けになれば幸いです。


## コード

localhost:8080/api/sample に GET, POST でリクエストしたときに
文字列を返すような API を作成する場合

アプリケーションコードは以下

```scala
import cats.effect.{ IO, Sync }
import com.twitter.finagle.{ Http, ListeningServer }
import com.twitter.util.Await
import io.finch.{ Bootstrap, Endpoint, Text }

object App extends scala.App with Endpoint.Module[IO] {
  val endpoint = "api" :: "sample" :: Service.make

  val services = Bootstrap
    .serve[Text.Plain](endpoint)
    .toService

  val server: ListeningServer = Http.server.serve(":8080", services)
  Await.ready(server)
}

object Service extends Endpoint.Module[IO] {
  import io.finch.{ BadRequest, Endpoint, Ok }
  import shapeless.{ :+:, CNil }
  def make()(implicit runtime: Sync[IO]): Endpoint[IO, String :+: String :+: CNil] =
    get(query) { query: Option[String] =>
      Ok("get request")
    } :+: post(stringBody :: header("content-type")) { (body: String, header: String) =>
      (body, header) match {
        case (_, header) => Ok(s"ok. header: $header, body: $body")
        case _           => BadRequest(new Exception(s"invalid request. header: $header, body: $body"))
      }
    }

  private val query =
    paramOption[String]("id")
      .mapAsync(query => IO.apply(query))
}

```

build.sbt は以下

```scala
lazy val `finch-sample` = (project in file("./finch-sample"))
  .settings(
    name         := "finch-sample",
    scalaVersion := "2.13.3",
    libraryDependencies ++= Seq(
      "com.github.finagle" %% "finchx-core"
    ).map(_               % "0.32.1")
  )
```

環境は以下
- scala: 2.13.3
- JDK: 14.0.1
- sbt: 1.3.13
- finchx-core: 0.32.1

# 内容

アプリケーションコードでは `Endpoint.Module[F[_]]` をミックスインします。
今回は `cats.effect.IO` を使いたかったので、`F[_]`の部分は`IO`になっています。

## Endpoint 定義

リクエスト を受診したとき、Endpoint の定義にマッチした条件で処理が実行されます。
マッチする条件が無い場合はデフォルトの動作になります（404 エラーレスポンス）。

### 定義、連結
Endpoint を定義するときに考えている基本事項について
#### クエリパラメーターを取得する
`paramOption[A]`, `param[A]` でリクエストにあるクエリパラメーターを取得できます

#### リクエストボディを取得する
文字列で取得したい時は`stringBody`, `stringBodyOption` で取得します

Json で取得したい、そんなときは`jsonBody`,`jsonBodyOption`で取得します
JsonDecoder を定義してあげてJsonオブジェクトにパースされた状態で扱うことができます

他にもバイナリ形式で取得できたりもするようです。

#### ヘッダーの値を取得する
`header(name: String)`、`headerOption(name: String)` を使って指定したヘッダーの値を取得します

#### 連結
Endpoint の条件の連結を行いたい場合は `::` をつかって連結できます。
今回の場合だと、POSTの場合の連結でつかっています。

Endpoint 自体の連結をしたい場合、`shapeless` の `:+:`を使って連結します。
今回の場合だと、GET + POST の Endpoint 連結する場合に使っています。


### GET 用の Endpoint について

```scala
get(query) { query: Option[String] =>
  Ok("get request")
} 

private val query =
    paramOption[String]("id")
      .mapAsync(query => IO.apply(query))
```
リクエストにあるクエリパラメーターを取得する Endpoint の定義
この場合だと、"id" という key のクエリパラメーターの value を `Option[String]` の形式で取得、
`text/plain` の `200 OK`レスポンスを返すという処理になっています。
（query 自体の値は捨ててる状態になっています）

### POST 用の Endpoint について

```scala
post(stringBodyOption :: headerOption("content-type")) { (body: Option[String], header: Option[String]) =>
      (body, header) match {
        case (_, Some(value)) => Ok(s"ok. header: $value, body: ${body.getOrElse("no body")}")
        case _           => BadRequest(new Exception(s"invalid request. header: $header, body: $body"))
      }
    }
```
リクエストボディを文字列形式で取得します。
リクエストに存在する `content-type` ヘッダーの値を取得するようになっており、
`content-type` がある場合に 200 を返し、存在しない場合は 400 エラーを返すような実相になっています。

今回は受け取った値をそのまま返す。という感じですが、
受け取った値を使って処理分岐をしていくことも当然できます。

### 作成した Endpoint の結合

```scala
  def make()(implicit runtime: Sync[IO]): Endpoint[IO, String :+: String :+: CNil] =
    get(query) { query: Option[String] => ???
    } :+: post(stringBodyOption :: headerOption("content-type")) { (body: Option[String], header: Option[String]) => ???
    }
```
長いので Endpoint 内の処理は「???」にしていますが、上述した Endpoint を結合しようとしています。
処理の結合自体は `:+:` で結合します。
このメソッドの戻り値の型は `Endpoint[IO, String :+: String :+: CNil]` になっています。
これは連結した Endpoint の戻り値の型に依存し、今回は戻す形式が 2つとも`String`で2つ連結しているためです。
`Json`や他の形式で返す場合、連結する個数が変わる場合はそれに応じて形式や個数が変更する必要があります。

## API サーバー起動

```scala
object App extends scala.App with Endpoint.Module[IO] {
```
アプリケーション起動させる object に`Endpoint.Module[F[_]]` をミックスインします。

```scala
  val endpoint = "api" :: "sample" :: Service.make

  val services = Bootstrap
    .serve[Text.Plain](endpoint)
    .toService
```
`http://(host)/api/sample` をリクエストパスにするために`"api" :: "sample"`を記述
先程つくった Endpoint を連結して endpoint 変数に代入

BootStrap から API サーバーとしての動作を定義
`text/plain`形式で今回の Endpoint の結果を返すようにします。

```scala
val server: ListeningServer = Http.server.serve(":8080", services)
Await.ready(server)
```
8080 ポートと用いて API サーバーを起動。
`Await.ready` で待ち受けするようにすればサーバーとして稼働を開始。

# 最後に

初めて使うライブラリは最初は罠にはまる感じがあるのは当然として、
finch は Endpoint の定義や、取得できた値の扱いがちょっと慣れない感じでした。

Cats.effect を使えるので、いろいろより便利に出来る方法もあるのですが、
今回は活用できるようになっていないのがちょっと残念になっていました。