ども、@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" }
クラス図(簡易)
基盤
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の知識のいるのかな。もう少しいい方法がないものか。。