今回Advent CalendarでいくつかAPI Clientを作って感じたことは、GoでAPI Clientの構成は細部は違くともほぼ同じ作りで大抵の箇所は前に作ったもののコピーでできてしまうということです。
しかしながら、外部パッケージではなくコピーしたところも動作保証しなければならないため、それらの箇所に関してもすべてテストする必要があります。
API ServerやApplicationを作るときにFrameworkを使うと最低限の箇所を作れば、いいのと同じように共通化できる箇所を1つのパッケージにまとめたbone
を作りました。
Bone
https://github.com/usk81/bone
Boneは殆どのものにInterfaceを用意しています。
そのため、
最小限の構成を考えた場合、以下のようになります。
クライアントの作成は、bone.NewClient
がほとんど対応してくれます。
足りない部分を記載してあげるだけで完了します。
APIごとに1番異なるのが、レスポンスを受けたあとの処理とエラーレスポンスの処理方法です。
エラー対応はCheckResponse
をSetResponseChecker
で差し替えることができるので、独自の処理に置き換えて使ってください。
boneでstatus codeをチェックするだけの処理を用意しているので、それを使っても構いません。
ErrorResponse
はエラーレスポンスをエラーとして扱うためのものです。
こちらはinterfaceを用意していませんが、errorのダックタイピングになるので、Error()
メソッドが用意されていれば同様に扱えます。エラーレスポンスのボディの内容を格納するなどして使ってください。
const (
defaultBaseURL = "https://httpbin.org/"
userAgent = "example"
tokenKey = "Authentication"
)
func New(httpClient *http.Client, token string) (c *bone.DefaultClient, err error) {
c = &bone.DefaultClient{
TokenKey: tokenKey,
Token: token,
}
if err = bone.NewClient(c, httpClient); err != nil {
return nil, err
}
c.SetBaseURL(defaultBaseURL)
c.SetUserAgent(userAgent)
c.SetResponseChecker(CheckResponse)
es := &ExampleService{}
es.SetClient(c)
c.SetService("example", es)
return
}
// An ErrorResponse reports the error caused by an API request
type ErrorResponse struct {
Response *http.Response
}
func (r *ErrorResponse) Error() string {
// Error message
}
// CheckResponse checks the API response for errors, and returns them if present
func CheckResponse(r *http.Response) error {
if c := r.StatusCode; c >= 200 && c <= 299 {
return nil
}
errorResponse := &ErrorResponse{Response: r}
// set optional parameters
return errorResponse
}
DefaultClientには、NewRequestが用意されているので、これを使えば、リクエストするAPIのURLやHeaderの設定、リクエストボディの設定をよしなにやってくれます。
DoでAPIリクエストを行って、bone.JSONDecode
でレスポンスボディをJSONをDecodeしてresult
に格納しています。
bone.JSONDecode
はResponseDecode
という関数型なので、ResponseDecode
を満たすように作れば、XMLのレスポンスボディをDecodeすることも簡単にできます。
さらにDoはClient
で定義されているわけではなく、ResponseResolver
という別のinterfaceで定義されているので、リクエストを投げっぱなしにして結果を受け取りたくないのであれば、Do
をつくらずに独自処理で解決することもできます。
type ProfileService struct {
client bone.Client
}
func (s *ProfileService) SetClient(c bone.Client) {
s.client = c
}
func (s *ProfileService) Get() (result ExampleResult, err error) {
c, ok := s.client.(*bone.DefaultClient)
if !ok {
err = errors.New("client is invalid")
return
}
req, err := c.NewRequest(http.MethodGet, "anything"), nil, nil)
if err != nil {
return
}
if _, err = c.Do(nil, req, bone.JSONDecode, &result); err != nil {
return
}
return result, nil
}
Boneは1日ぐらいで作ったもので、試用はできますが、まだコンセプトレベルです。
(テストも書いてませんし)
ですが、exampleを書いた感じだと開発工数は減らせそうな感触は得られたので、引き続き開発したいと思います。