LoginSignup
1
0

More than 3 years have passed since last update.

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

Posted at

はじめに

あるきっかけで 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/plain200 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 を使えるので、いろいろより便利に出来る方法もあるのですが、
今回は活用できるようになっていないのがちょっと残念になっていました。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0