🥰

Scala の http クライアントを紹介する

2021/12/12に公開

Scala の http ライブラリ

http4s とSttp という 2つの Scala の http クライアントを紹介します.

github の search api を叩いて json を取得し、結果をパースして case class に流し込んでみましょう.

http4s

http4s は typelevel エコシステムの シンプルな http(サーバー、クライアント)ライブラリです. IO モナドなどのエフェクトライブラリと組み合わせて使います.

サンプルコードではしばしば BlazeHttpClient が使われていますが、EmberHttpClient のほうが新しい実装なのでこちらを使います.

import $ivy.`org.http4s::http4s-dsl:1.0.0-M30`
import $ivy.`org.http4s::http4s-ember-client:1.0.0-M30`
import $ivy.`org.typelevel::cats-effect:3.3.0`
import $ivy.`io.circe::circe-core:0.15.0-M1`
import $ivy.`io.circe::circe-generic:0.15.0-M1`
import $ivy.`org.http4s::http4s-circe:1.0.0-M30`

import cats.effect.IO
import org.http4s.ember.client._
import cats.effect.IO
import  cats.effect.unsafe.implicits.global

まず必要なライブラリをインポートします.
scala script 形式で書いているので sbt を使う場合は以下のように置き換えてください.

build.sbt
val circeVersion = "0.15.0-M1"
val http4sVersion = "1.0.0-M30"
libraryDependencies ++=Seq(
  "org.http4s" %% "http4s-dsl" % http4sVersion,
  "org.http4s" %% "http4s-ember-client" % http4sVersion,
  "org.typelevel" %% "cats-effect" % "3.3.0"
  "io.circe" %% "circe-core" % circeVersion,
  "io.circe" %% "circe-generic" % circeVersion
)

レスポンスの形式を表現する case class を作ります. Scala ではなるべく型安全にするのを好む風潮があるので AnyMap[String,Any] はあまり使われません.

次のようなレスポンスが返ってくるとします.

{
  total_count: ...,
  items: [
    {
      name: "hoge",
      stargazers_count: 100
    },
    {
      name: "fuga",
      stargazers_count: 100
    },
    ...
  ]
}

これは以下の case class にマッピングできます.

case class GithubResponse(total_count:Int,items:List[GithubItem])
case class GithubItem(name:String,stargazers_count:Int)

次に、 case class と json を変換するために decoder を定義します. 手作業でやっても良いですが、面倒なので以下のようにして自動生成します.

import io.circe.generic.semiauto._
implicit  def itemDecoder :Decoder[GithubItem] = deriveDecoder[GithubItem]
implicit def githubResponseDecoder:Decoder[GithubResponse] = deriveDecoder[GithubResponse]

ここまでは文字列と case class を変換する circe のDecoderです.

あとひとつ、Http のレスポンスボディから文字列を取り出す処理も必要です. 幸いなことにhttp4s の circe モジュールがあるので以下のコードを追加すれば大丈夫です.

import org.http4s.circe.CirceEntityDecoder._
implicit def responseDecoder = circeEntityDecoder[IO,GithubResponse]

次にリクエストを定義します.

val query = "language:scala"
val request = Request.apply[IO](Method.GET,Uri.unsafeFromString(s"https://api.github.com/search/repositories?q=$query"))

次にリクエストを送ってレスポンスを取得するためのクライアントのプログラムを書きます.

val program: IO[GithubResponse] = EmberClientBuilder
    .default[IO]
    .withTimeout(3.seconds)
    .build.use { client =>
      client.expect[GithubResponse](request))
}

最後にリクエストを実際に送信して結果を得ます. 次のような結果が得られたはずです.

import cats.effect.unsafe.implicits.global
program.unsafeRunSync()

GithubResponse = GithubResponse(
  total_count = 206637,
  items = List(
    GithubItem(name = "spark", stargazers_count = 31555),
    GithubItem(name = "prisma1", stargazers_count = 16857),
    GithubItem(name = "scala", stargazers_count = 13566),
    GithubItem(name = "predictionio", stargazers_count = 12524),
    GithubItem(name = "playframework", stargazers_count = 12039),
    GithubItem(name = "akka", stargazers_count = 11919),
    GithubItem(name = "CMAK", stargazers_count = 10537),
    GithubItem(name = "lila", stargazers_count = 10290),
    GithubItem(name = "gitbucket", stargazers_count = 8522),
    GithubItem(name = "bfg-repo-cleaner", stargazers_count = 8174)

簡単ですね.

http4s は cats-effect と組合せて自然にプログラムを書くことができます. typelevel 系のライブラリはEmberClientBuilder.default[T] のように型パラメータでエフェクトシステムを切り替えることができるようになっています. もし IO ではなく Monix Task や ZIO を使いたい場合は型パラメータを切り替えることで対応できます.

また、EmberClientBuilder.default[T].buildResource 型を返すので開発者は明示的に use メソッドを呼ぶ必要があります. Resource はリソースの生成・破棄を自動で行う機能があります. typelevel のプロジェクトの多くはこのように型で様々な機能や制約をあらわしているのが特徴です.

sttp

Sttp は シンプルで、汎用的に利用できる http クライアントです. http4s などのライブラリと比べて関数型の思想が強くない、ニュートラルなライブラリなので使いやすいと思います.

(なお、正確に言うと sttp は具体的なライブラリの実装を隠すファサードとしての側面が強いです. 実際、sttp の json パースには circe, json4s, spray-jsonなどを、 httpリクエストの送受信を行うレイヤーは cats-effect, zio, fs2, akka などを選ぶことができます. 非同期IOが必要なら cats-effect や zio などを検討しましょう.)

まず必要なライブラリをインポートします.

import sttp.client3.quick._
import sttp.client3.circe._
import io.circe.generic.auto._

つぎにリクエストを組み立てます. asJson メソッドを使うことでレスポンスをGithubResponseに変換して返します. レスポンスボディの文字列から GithubResponseへの変換は circe のマクロによって自動で処理されます.

val query = "language:scala"
val sort: Option[String] = Some("stars")
val request = basicRequest
  .get(uri"https://api.github.com/search/repositories?q=$query&sort=$sort")
  .response(asJson[GitHubResponse])

request.send メソッドでバックエンド実装を指定してリクエストを送ります.

val response = request.send(backend)
response.body match {
  case Left(error) => ???
  case Right(data) => // GithubResponse が返ってくる.
}

Sttp には 非同期IO のバックエンド実装とブロッキングIOのバックエンド実装があります. この例ではブロッキングなものを使っています.
レスポンスは Either に包まれいるのでパターンマッチしてレスポンスを取り出しています.

sttp は地味に便利な機能がいくつかあります. 例えば、レスポンスボディがステータスを含んでいて、そのステータスに応じて Left/Right を分けたいというユースケースにも対応しています.
以下のようにパースの失敗とドメインのエラーを分けることができます.

val myRequest: Request[Either[ResponseException[String, io.circe.Error], MyModel], Nothing] =
  basicRequest
    .get(uri"https://example.com")
    .response(fromMetadata(
        asJson[ErrorModel], 
        ConditionalResponseAs(_.code == StatusCode.Ok, asJson[SuccessModel])
    ))

他にもリトライ処理、ストリーミング、websocket などの機能を(バックエンドによっては)使うことができます.

Scala のエコシステムは分厚いフレームワークよりもコンポーザブルなライブラリを各々が選んで使う傾向があるので特定のライブラリ・フレームワークに縛られることが無くて快適です.(デファクトが無い辛さもありますが...)

また、型安全なプログラムを好む傾向があるので最初はコンパイルエラーが出てイライラするかもしれませんが、慣れてくるといちいち実行してエラーを修正しなくていいので生産性が高まります. Metals と ammonite script を使えばエディタの恩恵をしっかり受けながらスクリプトをシュッと書くこともできますよ(^ω^)
こまったらとりあえず . をうって候補を出して、定義にジャンプしてドキュメントを読んでみましょう.

シンプルに http リクエストを送信するなら http4s を、少し複雑なまとまった処理を書きたければ sttp つかうといいのではないでしょうか.

Discussion