読者です 読者をやめる 読者になる 読者になる

Septeni Engineer's Blog

セプテーニエンジニアが綴る技術ブログ

Dispatch(HTTPクライアントライブラリ)を使ったサンプルコードを書いてみた

Scala

ども、@kimutyamです。

業務の一部で外部API基盤開発に携わったんで、ブログに書こうと思いました。
開発メンバーがライブラリの知識がなくても簡単にAPIリクエストを行い、レスポンスをクラスにマッピングされた形で返ってくるような基盤を作成しようと思いました。
今回はDispatchを使ってAPIクライアントを作成し、それを利用したサンプルを書いてみました。

API

サンプルAPIサーバーからユーザー一覧を取得するようなAPIを想定

URL

https://sample.jp/api/users

リクエストヘッダー

GET /api/samples
Accept: application/json
Authorization: Basic 

レスポンス(成功時)

{
  "status": 200,
  "message": "success!"
  "version": "1.0",
  "datas": [
    {
      "id": 1,
      "name": "kimura"
    },
    {
      "id": 2,
      "name": "akihiro"
    }
  ]
}

レスポンス(失敗時)

{
  "status": 500,
  "message": "internal server error!"
  "version": "1.0"
}

クラス図(簡易)

f:id:septeni-original:20150722164732p:plain

基盤

APIClient

dispatchをラップしたAPIクライアントの基盤

package infrastructure.http.base

import dispatch._
import org.json4s._
import scala.concurrent.ExecutionContext.Implicits.global

trait APIClient[R] {

  protected val configuration: HttpConfiguration

  protected implicit val responseManifest: Manifest[R]

  private lazy val http = Http.configure(_.setConnectionTimeoutInMs(configuration.requestTimeout))

  def execRequest(implicit responseManifest: Manifest[R]): Future[R] = {
    http(buildRequest > as.json4s.Json).map(jsonExtract)
  }

  protected def buildRequest: Req = {

    var req = host(configuration.hostName + configuration.path)
      .setMethod(configuration.method)

    if (configuration.basicAuthenticationConfiguration.nonEmpty) {
      val basicAuthenticationConfiguration = configuration.basicAuthenticationConfiguration.get
      req = req.as_!(
        basicAuthenticationConfiguration.user,
        basicAuthenticationConfiguration.password
      )
    }

    if (configuration.isSecure)
      req = req.secure

    if (configuration.headers.nonEmpty)
      req = req <:< configuration.headers

    if (configuration.queryString.nonEmpty)
      req = configuration.method match {
        case "GET" => req <<? configuration.queryString

        case _ => req << configuration.queryString
      }

    req
  }

  protected def jsonExtract(result: JValue)(implicit responseManifest: Manifest[R]): R
}


HttpConfiguration

HTTP設定のインターフェース

package infrastructure.http.base

trait HttpConfiguration {
  val hostName: String
  val path: String
  val isSecure: Boolean
  val method: String
  val requestTimeout: Int
  val queryString: Map[String, String]
  val headers: Map[String, String]
  val basicAuthenticationConfiguration: Option[BasicAuthenticationConfiguration]
}


サンプルAPIサーバーの処理

CommonResponse

共通レスポンス

package infrastructure.http.sample.response

trait CommonResponse {
  val status: Int
  val message: String
  val version: String
}


ErrorResponse

エラーレスポンス

package infrastructure.http.sample.response

case class ErrorResponse(
  status: Int,
  message: String,
  version: String
) extends CommonResponse


SampleAPIClient

APIクライアント

package infrastructure.http.sample

import org.json4s.JValue
import infrastructure.http.base.APIClient

trait SampleAPIClient[R] extends APIClient[R] {

  protected def jsonExtract(result: JValue)(implicit responseManifest: Manifest[R]): R = {

    val status = (result \ "status").extract[Int]

    if (status == 200) {
      result.extract[R]
    } else {
      val errorResponse = result.extract[ErrorResponse]
      throw new Exception(s"${errorResponse.status},${errorResponse.message},${errorResponse.version}")
    }
  }
}


SampleHttpConfiguration

HTTP設定具象クラス

package infrastructure.http.sample

import infrastructure.http.base.HttpConfiguration
import infrastructure.http.BasicAuthenticationConfiguration

case class SampleHttpConfiguration(
  hostName: String,
  path: String,
  isSecure: Boolean,
  method: String,
  requestTimeout: Int,
  queryString: Map[String, String],
  basicAuthenticationConfiguration: Option[BasicAuthenticationConfiguration],
  headers: Map[String, String] = Map("Accept" -> "application/json")
) extends HttpConfiguration

サンプルAPIサーバーのユーザー一覧APIの実装

UserSeqResponse

ユーザー一覧APIのレスポンスクラス

package infrastructure.http.sample.response

import infrastructure.http.sample.CommonResponse

case class UserSeqResponse(
  status: Int,
  message: String,
  version: String,
  datas: Seq[User]
) extends CommonResponse

case class User(
  id: Long,
  name: String
)


UserListAPIClient

ユーザー一覧APIクライアント実装クラス

package infrastructure.http.sample

import infrastructure.http.BasicAuthenticationConfiguration
import infrastructure.http.sample.{SampleAPIClient, SampleHttpConfiguration}
import infrastructure.http.sample.response.SampleResponse

case class UserListAPIClient(
  basic: BasicAuthenticationConfiguration
) extends ConcreteAPIClient[UserSeqResponse] {

  protected val responseManifest = Manifest.classType[UserSeqResponse](UserSeqResponse.getClass)

  protected val configuration = SampleHttpConfiguration(
    "sample.jp",
    "/api/users",
    true,
    "GET",
    100,
    Map(),
    Some(basic)
  )
}


ベーシック認証

BasicAuthenticationConfiguration

ベーシック認証の設定クラス

package infrastructure.http

case class BasicAuthenticationConfiguration(
  user: String,
  password: String
)


クライアント

import infrastructure.http.BasicAuthenticationConfiguration
import infrastructure.http.sample.UserAPIClient

object Client {
  val basic = BasicAuthenticationConfiguration(
    "user",
    "password"
  )
  UserListAPIClient(basic).execRequest
}

dispatchの世界はAPIClientでしか利用してないところが味噌ですかね。
ただ、ラッピングしているAPIClient#buildRequestは若干妥協。
ちゃんとやろうとするとasync-http-clientの知識のいるのかな。もう少しいい方法がないものか。。