LoginSignup
1
0

More than 3 years have passed since last update.

GoでAPI Clientのframeworkを考えてみた

Posted at

今回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番異なるのが、レスポンスを受けたあとの処理とエラーレスポンスの処理方法です。
エラー対応はCheckResponseSetResponseCheckerで差し替えることができるので、独自の処理に置き換えて使ってください。
boneでstatus codeをチェックするだけの処理を用意しているので、それを使っても構いません。

ErrorResponseはエラーレスポンスをエラーとして扱うためのものです。
こちらはinterfaceを用意していませんが、errorのダックタイピングになるので、Error()メソッドが用意されていれば同様に扱えます。エラーレスポンスのボディの内容を格納するなどして使ってください。

client.go
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.JSONDecodeResponseDecodeという関数型なので、ResponseDecodeを満たすように作れば、XMLのレスポンスボディをDecodeすることも簡単にできます。

さらにDoはClientで定義されているわけではなく、ResponseResolverという別のinterfaceで定義されているので、リクエストを投げっぱなしにして結果を受け取りたくないのであれば、Doをつくらずに独自処理で解決することもできます。

example_service.go
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を書いた感じだと開発工数は減らせそうな感触は得られたので、引き続き開発したいと思います。

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