はじめに
あるきっかけで Finch を使う機会があったのですが、
なれない形式なのか色々と四苦八苦。そのまとめになります。
Finch と書いていますが、finchx を使っています。
version 0.25 から finch, finchx の2種類になり、
finchx のほうは Cats Effects を使えるようになっています。
Cats とかよくわからないけど、とりあえず動かしてみたい。
私がそういう人だったのでそういう人向けになれば幸いです。
コード
localhost:8080/api/sample に GET, POST でリクエストしたときに
文字列を返すような API を作成する場合
アプリケーションコードは以下
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 は以下
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 について
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 について
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 の結合
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 サーバー起動
object App extends scala.App with Endpoint.Module[IO] {
アプリケーション起動させる object にEndpoint.Module[F[_]]
をミックスインします。
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 の結果を返すようにします。
val server: ListeningServer = Http.server.serve(":8080", services)
Await.ready(server)
8080 ポートと用いて API サーバーを起動。
Await.ready
で待ち受けするようにすればサーバーとして稼働を開始。
最後に
初めて使うライブラリは最初は罠にはまる感じがあるのは当然として、
finch は Endpoint の定義や、取得できた値の扱いがちょっと慣れない感じでした。
Cats.effect を使えるので、いろいろより便利に出来る方法もあるのですが、
今回は活用できるようになっていないのがちょっと残念になっていました。