http4s × tapir 〜 エンドポイントにおける記述と実行の分離

HTTP エンドポイントをタイプフルに記述してビジネスロジックを分離するライブラリ tapir の http4s への適用。

## はじめに
関数型な Scala の界隈では記述と実行の分離がよく奨励されるが[^1]、この考え方をHTTPのエンドポイントにも当てはめたのが [tapir](https://github.com/softwaremill/tapir) （**T**yped **API** desc<b>R</b>iptions） で、エンドポイントの記述とビジネスロジックをいい感じに分離してくれる。

現時点で Akka Http と [http4s](https://github.com/http4s/http4s) に対応しているが、この記事では http4s との組み合わせを試してみる。


[^1]: 例えば Cats Effect の IO, Monix の Task, あるいは ZIO におけるプログラムの記述と実行（のエフェクト）の分離。Free Monad や Tagless Final ではさらに一段高い抽象レベルで分離されていた。逆に Scala 標準の Future は、記述と実行が分離していないことが[嫌われていたりする](https://medium.com/@sderosiaux/are-scala-futures-the-past-69bd62b9c001)。

## 概要
こんな特徴がある

1. **エンドポイントの記述とビジネスロジックの分離**: エンドポイントをロジックから独立させることで、サーバーの受け口としても、クライアントからの呼び先としても、OpenAPI ドキュメント作成の入力としても、統一的に扱えて、再利用もしやすくなる。

2. **読みやすい EDSL**: tapir 自体について知らなくても、どういうエンドポイントなのか読めばわかる。OpenApi（Swagger）にも最初から準拠していて、関連するフィールドの指定も含めて、一連の [fluent interface](http://bliki-ja.github.io/FluentInterface/) の流れの中で表現できる。

3. **型と値と関数だけの Scala コード**: ルート定義ファイルなどを書くこともないし、アノテーションもマクロも使わない。エディターや IDEとの相性もよいので補完や型推論がスムーズ。

4. **シンプルな型**: [Finch](https://finagle.github.io/finch/) のように [Shapeless](https://github.com/milessabin/shapeless) などを使い倒してると（これはこれで面白いけど）、開発が進むうちに型が巨大化してコンパイルに異常に時間がかかったり、エラーメッセージが複雑過ぎて読めなくなったりすることがままあるが、tapir はただの Generic なケースクラスしか使ってないので、人間にもコンパイラにも優しい。

5. **3 と 4 の結果、合成・分解が簡単**: ただの値と関数でしかないから、普通の *extract 〜* 系のリファクタリングテクニックが使いやすく、重複解消 → 再利用がはかどる。

エンドポイント型は下のようなケースクラスと型エイリアスとして提供される。

```scala
case class Endpoint[A, I, E, O, -R](
  securityInput: EndpointInput[A],
  input        : EndpointInput[I],
  errorOutput  : EndpointOutput[E],
  output       : EndpointOutput[O],
  info         : EndpointInfo)

type PublicEndpoint[I, E, O, -R] = Endpoint[A, I, E, O, -R]
```
`securityInput` は認証トークン等のセキュリティ関連パラメータを扱うが、必要ない場合は `PublicEndpoint` が使える。型パラメータ`R`は、エンドポイントが WebSocket やストリームであるような場合に指定する。必要なければ `Any` で良い。→ [公式doc](https://tapir.softwaremill.com/en/latest/endpoint/basics.html#basics)


## サンプル実装
上述の特徴を踏まえてサンプルコードを書いてみる。

簡単にするためにエラー型は `Unit` にしたが、本当は例えば `(StatusCode, ErrorInfo)` などのような型が使われる。また *Effect* としては、`Task` でも `F[_]`でもよかったが[^about-effect]、簡単のため `IO` 決め打ちにした。

各種バージョンは
- Scala: 3.1.3
- Cats: 2.8.0
- http4s: 0.23.14
- Tapir: 1.0.3
- [その他](https://github.com/yasuabe/qiita-cats3-sample/blob/main/build.sbt)

### Hello, World
まず、エンドポイントが一つだけの最小のサンプルで試してみる。

[http4s のドキュメント](https://http4s.org/v0.20/service/) に、下のような Hello World が紹介されているが、、、

```scala
  val helloWorldService = HttpRoutes.of[IO] {
    case GET -> Root / "hello" / name =>
      Ok(s"Hello, $name.")
  }.orNotFound
```
tapir を使って分解してみると以下のようになる。

```scala
// エンドポイントの記述
val helloWorldEP: PublicEndpoint[String, Unit, String, Any] =
  endpoint.get.in("hello" / path[String]("name")).out(stringBody)

// ビジネスロジック
def helloLogic(name: String): IO[Either[Unit, String]] =
  s"Hello, $name.".asRight[Unit].pure[IO]

// エンドポイントとロジックを組み合わせて http4s の HttpRoutes に
val helloWorldRoute: HttpRoutes[IO] =
  Http4sServerInterpreter[IO]().toRoutes(helloWorldEP serverLogic helloLogic)

// HttpRoutes に 404 を加味して Ember で実行可能な HttpApp に
val helloWorldService: HttpApp[IO] =
  helloWorldRoute.orNotFound
```

少しコード増えたが型安全に分解できた。きれいに分解できると合成も簡単になり、共通要素をまとめたり抽象度をそろえたりしやすくなる。次にエンドポイントを複数にして、その効用をみてみる。

[ここまでのソース](https://github.com/yasuabe/qiita-cats3-sample/blob/main/src/main/scala/tapir_sample/Main0.scala)

### 複数エンドポイント
以下のような３つのエンドポイントを例に考えてみる。

```scala
val helloEP: PublicEndpoint[String, Unit, String, Any] =
  endpoint.get
    .in("hello" / path[String]("name"))
    .out(stringBody)

val hiEP: PublicEndpoint[String, Unit, String, Any] =
  endpoint.get
    .in("hi" / path[String]("name"))
    .out(stringBody)

val byeEP: PublicEndpoint[String, Unit, String, Any] =
  endpoint.get
    .in("bye" / path[String]("name"))
    .out(stringBody)
```

#### 重複コードの抽出
上の3つのエンドポイントには何点か重複部分があるが、どの部分も普通の Scala コードなので、共通コードを抽出するリファクタリングが自然にできる。例えば次のような共通要素をくくりだすと、、、

- `String` を返す `GET` リクエストである
- `name` というパス変数がある

下のコードのように整理できる。

```scala
val nameParam = path[String]("name")

val greetEP: PublicEndpoint[Unit, Unit, String, Any] =
  endpoint.get.out(stringBody)

val helloEP: PublicEndpoint[String, Unit, String, Any] =
  greetEP.in("hello" / nameParam)

val hiEP: PublicEndpoint[String, Unit, String, Any] =
  greetEP.in("hi" / nameParam)

val byeEP: PublicEndpoint[String, Unit, String, Any] =
  greetEP.in("bye" / nameParam)
```
ちなみに OpenAPI の `description`や `example` のようなドキュメント用のフィールドも、`nameParam` の定義部分で一緒に指定できる。

```scala
val nameParam: PathCapture[String] =
  path[String]("name").description("名前").example("World")
```
URLパラメータやヘッダなども同様で、共通的に使われるパラメータを **DRY** に一括定義できる。アノテーションベースで Swagger 用のコーディングをしていると、こうした共通パラメータは得てしてコピペが多くなりがちだったが、tapir 方式の場合そうした不都合もなくなる。

#### ルートの合成
上で見たように、エンドポイントとビジネスロジックを `toRoutes` で合成すると `HttpRoutes` になる。tapir そのものの話題ではないが、この `HttpRoutes` の合成も一応見ておく。

上の 3つのエンドポイントにそれぞれ対応する、下記のようなビジネスロジックがあるとする。

```scala
def hello(name: String): IO[Either[Unit, String]] =
  s"Hello, $name".asRight[Unit].pure[IO]

def hi(name: String): IO[Either[Unit, String]] =
  s"Hi, $name".asRight[Unit].pure[IO]

def bye(name: String): IO[Either[Unit, String]] =
  s"Bye, $name".asRight[Unit].pure[IO]
```
エンドポイントと組み合わせると各々 `HttpRoutes[IO]` になるが、`HttpRoutes` は `Kleisli` の型エイリアスなので、`SemigroupK` の `combineK` が使える[^kleisli_monoid]。たとえば以下のように書ける。

```scala
extension (ep: PublicEndpoint[String, Unit, String, Any])
  def toRoute(logic: String => IO[Either[Unit, String]]) =
    Http4sServerInterpreter[IO]().toRoutes(ep serverLogic logic)

val helloRoute:   HttpRoutes[IO] = helloEP toRoutes hello
val hiRoute:      HttpRoutes[IO] = hiEP    toRoutes hi
val goodByeRoute: HttpRoutes[IO] = byeEP   toRoutes goodBye

val greetingService: HttpApp[IO] =
  helloRoute   combineK
  hiRoute      combineK
  goodByeRoute orNotFound
```
[^kleisli_monoid]: cats でKleisli の MonoidK が提供されていて、MonoidK は SemigroupK の派生型だから。

さらに以下のような `HttpRoutes` の `Semigroup` 定義をスコープ内で見えるようにすれば、、、

```scala
given Semigroup[HttpRoutes[IO]] = _ combineK _
```

`NonEmptyList` を使って以下のように `reduce` することもできる。

```scala
val greetingService: HttpApp[IO] = NonEmptyList.of(
  helloEP toRoutes hello,
  hiEP    toRoutes hi,
  byeEP   toRoutes goodBye
).reduce.orNotFound
```

[Main3.scala](https://github.com/yasuabe/qiita-cats3-sample/blob/main/src/main/scala/tapir_sample/Main3.scala)

#### Swagger／OpenAPI
[Swagger／OpenAPI](https://swagger.io/docs/specification/about/) についてもざっと見てみる。

Swagger が必要とする API 情報はエンドポイントの記述だけがあれば良いので、ビジネスロジックから分離しておいたことによる扱いやすさが、ここでも利いてくる。例えば以下のように、上の3つのAPIエンドポイントから簡単に Swagger 用エンドポイントが得られる。

```scala
val swaggerEPs = SwaggerInterpreter().fromEndpoints[IO](
  List(helloEP, hiEP, byeEP),
  "http4s × tapir × Swagger",
  "1.0")
```
古い Tapir では Swagger と http4s を連携させる部分は若干自前のコードを書く必要があったが[^swagger-ui]、最新では tapir-swagger-ui-bundle を依存ライブラリに加えるだけで使えるようになる[^other-apidoc-usage]。

以下のような画面になる。
![Screenshot from 2022-08-06 00-58-56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/9633/c094cc32-a2f7-061c-c946-3394428b4e04.png)

[SwaggerMain.scala](https://github.com/yasuabe/qiita-cats3-sample/blob/main/src/main/scala/tapir_sample/SwaggerMain.scala)


## おわりに
Clojure の作者 Rich Hickey の有名なプレゼン[Simple Made Easy](https://www.infoq.com/presentations/Simple-Made-Easy/) でも、プログラムの構成要素を編み合わせる(complect)のを避けて、合成（compose）しやすい形で分離したままシンプルさを保つことの重要性が力説されていたが、関数型なスタイルに慣れていると tapir の記述と実行の分離といった考え方も自然に馴染むと思う。http4s だけでもすでにかなり関数型だけど、さらにワンランク上の関数型の良さを引き出すために併用してみたい。

※ Scala 3 と Cats Effect 3に合わせてサンプルコードを改め、本文も少し直した（2022-08-05）

## 参考
- [tapir github](https://github.com/softwaremill/tapir)
- [tapir documentation](https://tapir-scala.readthedocs.io/en/latest/)
- [Describe, then interpret: HTTP endpoints using tapir](https://blog.softwaremill.com/describe-then-interpret-http-endpoints-using-tapir-ac139ba565b0)
- [Three easy endpoints](https://blog.softwaremill.com/three-easy-endpoints-a6cbd52b0a6e)
- [Embedding SwaggerUI into http4s projects](http://www.aimplicits.com/posts/2018-08-31-embedding-swagger-ui/)

[^about-effect]: もちろん実務のコードであればよほど小さくて単純なプログラムでない限り `F[_]` がよい。
[^swagger-ui]: 例えば[このように](https://github.com/yasuabe/http4s-trial/blob/master/swagger_tapir/src/main/scala/trial/tapir/swagger/SwaggerUI.scala)
[^other-apidoc-usage]: swagger-ui だけではなく redoc も使える。また OpenAPI yaml に変換する部分と表示する部分を分けることもできる。→[公式](https://tapir.softwaremill.com/en/latest/docs/openapi.html#generating-and-exposing-documentation-in-a-single-step)