Sumaregi-go: GoでのSumaregi APIクライアントライブラリ開発入門 (Part 2)
1. はじめに
前回の記事では、Sumaregi APIとやり取りするためのGoライブラリ「Sumaregi-go」の基礎について説明し、token.go
ファイルを使ってAPIのアクセストークンを取得する方法を解説しました。今回は、Sumaregi APIと対話するクライアントの設計と実装について詳しく説明します。クライアントの初期化からAPIリクエストの送信、エラーハンドリングまで、具体的な実装方法を紹介します。
2. 対象読者
この記事は、Sumaregi APIをGoで活用したい開発者を対象にしています。前回の記事を読んで、基本的なプロジェクト構造とトークン管理の仕組みを理解していることを前提としています。GoでのHTTPリクエストやエラーハンドリングの基礎を理解していると、この記事の内容をよりスムーズに把握できます。
3. 記事を読むメリット
この記事を読むことで、以下のメリットがあります:
- Sumaregi-goクライアントの初期化方法を理解できる
- APIエンドポイントへのリクエストを送信する方法を学べる
- クライアントのリクエストとレスポンスのハンドリングの実装を理解できる
これらの知識を習得することで、Sumaregi APIを使ったアプリケーション開発がより効率的に行えるようになります。
4. 本文
クライアントの設計
client.go
ファイルは、Sumaregi APIとやり取りするための中心的な役割を果たします。クライアントを初期化し、APIエンドポイントに対してリクエストを送信するための主要な関数が含まれています。
EnvironmentVariable
構造体)
1. 環境変数の管理 (クライアントの初期化やAPIとの通信には、認証情報やAPIエンドポイントなどの設定が必要です。EnvironmentVariable
構造体とLoadEnv
関数は、このような設定を環境変数から読み込むために使用されます。
type EnvironmentVariable struct {
SmaregiClientID string
SmaregiClientSecret string
SmaregiIDPHost string
SmaregiAPIHost string
SmaregiContractID string
}
-
EnvironmentVariable
構造体:-
SmaregiClientID
: Sumaregi APIのクライアントID。 -
SmaregiClientSecret
: Sumaregi APIのクライアントシークレット。 -
SmaregiIDPHost
: IDプロバイダーのホストURL。 -
SmaregiAPIHost
: Sumaregi APIのホストURL。 -
SmaregiContractID
: Sumaregiの契約ID。
-
func LoadEnv(development bool) (EnvironmentVariable, error) {
envVari := EnvironmentVariable{}
err := godotenv.Load()
if err != nil {
return EnvironmentVariable{}, err
}
if development {
envVari.SmaregiClientID = os.Getenv("SMAREGI_CLIENT_ID_DEV")
envVari.SmaregiClientSecret = os.Getenv("SMAREGI_CLIENT_SECRET_DEV")
envVari.SmaregiIDPHost = os.Getenv("SMAREGI_IDP_HOST_DEV")
envVari.SmaregiAPIHost = os.Getenv("SMAREGI_API_HOST_DEV")
envVari.SmaregiContractID = os.Getenv("SMAREGI_SANDBOX_CONTRACT_ID_DEV")
} else {
envVari.SmaregiClientID = os.Getenv("SMAREGI_CLIENT_ID")
envVari.SmaregiClientSecret = os.Getenv("SMAREGI_CLIENT_SECRET")
envVari.SmaregiIDPHost = os.Getenv("SMAREGI_IDP_HOST")
envVari.SmaregiAPIHost = os.Getenv("SMAREGI_API_HOST")
envVari.SmaregiContractID = os.Getenv("SMAREGI_SANDBOX_CONTRACT_ID")
}
return envVari, nil
}
-
LoadEnv
関数:-
.env
ファイルから環境変数を読み込み、それをEnvironmentVariable
構造体に格納します。 - 引数
development
がtrue
の場合、開発用の環境変数を読み込み、false
の場合は本番用の環境変数を読み込みます。
-
2. Configの説明
クライアントの初期化に際して、config.go
ファイルにあるConfig
構造体が使用されます。この構造体は、Sumaregi APIとの通信に必要な設定情報を保持します。
type Config struct {
APIEndpoint string
Log Logger
ContractID string
}
-
Config
構造体:-
APIEndpoint
: Sumaregi APIのエンドポイントURL。APIリクエストを送信する際の基本URLを保持します。 -
Log
: ロギングのためのフィールド。Logger
というインターフェースで定義されているため、具体的なロギングの実装はこのインターフェースを実装した任意のオブジェクトを使うことができます。 -
ContractID
: Sumaregiの契約ID。APIリクエストに必要な契約情報を保持します。
-
func NewConfig(envVari EnvironmentVariable) *Config {
return &Config{
APIEndpoint: envVari.SmaregiAPIHost,
ContractID: envVari.SmaregiContractID,
}
}
-
NewConfig
関数:-
Config
構造体のインスタンスを生成するファクトリーメソッドです。 -
引数:
EnvironmentVariable
構造体を受け取り、そこからAPIエンドポイントと契約IDを取得します。 -
処理の流れ:
-
EnvironmentVariable
からAPIエンドポイント(SmaregiAPIHost
)と契約ID(SmaregiContractID
)を取得します。 - これらの値を用いて
Config
構造体を初期化します。 - 初期化された
Config
のポインタを返します。
-
-
3. クライアント構造体
type Client struct {
httpClient *http.Client
config *Config
token string
}
クライアントは、HTTPクライアント、設定情報、トークンを持つ構造体として定義されます。この構造体は、Sumaregi APIに対してリクエストを送信するために必要なすべての情報を保持します。
-
httpClient
:http.Client
のインスタンスで、APIリクエストを送信します。 -
config
: APIエンドポイントや契約IDなどの設定情報を含む構造体です。 -
token
: Sumaregi APIとの通信に必要なアクセストークンです。
NewClient
関数)
4. クライアントの初期化 (func NewClient(config *Config, scopes []string, envVari EnvironmentVariable) (*Client, error) {
token, err := getAccessToken(scopes, envVari)
if err != nil {
return nil, err
}
client := &Client{
httpClient: &http.Client{},
config: config,
token: token,
}
return client, nil
}
NewClient
関数は、クライアントを初期化するためのファクトリーメソッドです。この関数では、APIと通信するためのアクセストークンを取得し、新しいClient
インスタンスを返します。
-
引数:
-
config
: Sumaregi APIの設定情報を持つ構造体。 -
scopes
: アクセストークンのスコープ(権限)を表す文字列のスライス。 -
envVari
: 環境変数(クライアントIDやシークレットなど)を保持する構造体。
-
-
処理の流れ:
-
getAccessToken
関数を呼び出してアクセストークンを取得します。 - 新しい
Client
構造体を初期化し、HTTPクライアント、設定、トークンを設定します。 - 初期化に成功した場合は、新しい
Client
インスタンスを返します。エラーが発生した場合はエラーを返します。
-
call
関数)
5. APIリクエストを送信する (func (c *Client) call(
ctx context.Context,
apiPath string, method string,
queryParams url.Values, postBody interface{},
res interface{},
) error {
var (
contentType string
body io.Reader
)
if method != http.MethodDelete {
contentType = "application/json"
jsonParams, err := json.Marshal(postBody)
if err != nil {
return err
}
body = bytes.NewBuffer(jsonParams)
}
req, err := c.newRequest(ctx, apiPath, method, contentType, queryParams, body)
if err != nil {
return err
}
return c.do(ctx, req, res)
}
call
関数は、Sumaregi APIにリクエストを送信し、レスポンスを受け取ります。この関数は、一般的なAPIリクエストを行うための共通のロジックを提供します。
-
引数:
-
ctx
: リクエストのコンテキスト。 -
apiPath
: APIのエンドポイントのパス。 -
method
: HTTPメソッド(GET、POST、PUT、DELETEなど)。 -
queryParams
: クエリパラメータを含むurl.Values
。 -
postBody
: POSTリクエストのボディ(リクエストのペイロード)。 -
res
: レスポンスデータを格納する構造体。
-
-
処理の流れ:
- HTTPメソッドがDELETEでない場合、
postBody
をJSONにシリアライズし、リクエストボディとして設定します。 -
newRequest
関数を使用して新しいHTTPリクエストを作成します。 -
do
関数を呼び出してリクエストを実行し、レスポンスを処理します。
- HTTPメソッドがDELETEでない場合、
newRequest
関数)
6. リクエストの作成 (func (c *Client) newRequest(
ctx context.Context,
apiPath string, method string,
contentType string,
queryParams url.Values,
body io.Reader,
) (*http.Request, error) {
// construct url
u, err := url.Parse(c.config.APIEndpoint)
if err != nil {
return nil, err
}
u.Path = path.Join(u.Path, c.config.ContractID, APIPathPos, apiPath)
u.RawQuery = queryParams.Encode()
// request with context
req, err := http.NewRequest(method, u.String(), body)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
fmt.Print(u.String())
// set http headers
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
req.Header.Set("Authorization", "Bearer "+c.token)
return req, nil
}
newRequest
関数は、APIリクエストを作成し、適切なHTTPヘッダーを設定します。
-
処理の流れ:
-
c.config.APIEndpoint
を基にAPIのエンドポイントURLを構築します。 - パスとクエリパラメータを設定して、完全なURLを作成します。
-
http.NewRequest
で新しいHTTPリクエストを作成し、コンテキスト、メソッド、URL、ボディを設定します。 - コンテンツタイプが指定されている場合、
Content-Type
ヘッダーを設定します。(今回は、application/json
) - アクセストークンを使用して
Authorization
ヘッダーを設定します。
-
do
関数)
7. リクエストの実行 (func (c *Client) do(
ctx context.Context,
req *http.Request,
res interface{},
) error {
response, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
return
}
}(response.Body)
if res == nil {
return nil
}
if response.StatusCode != http.StatusOK {
bodyBytes, err := io.ReadAll(response.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %v", err)
}
fmt.Printf("HTTP %d: %s", response.StatusCode, string(bodyBytes))
return fmt.Errorf("HTTP %d: %s", response.StatusCode, string(bodyBytes))
}
return json.NewDecoder(response.Body).Decode(&res)
}
do
関数は、APIリクエストを実行し、レスポンスを処理します。
-
処理の流れ:
-
httpClient
を使ってリクエストを実行し、レスポンスを受け取ります。 - レスポンスのステータスコードが
200 OK
でない場合、エラーメッセージを返します。 - レスポンスボディをデコードし、
res
に格納します。
-
5. コード例
以下は、client.go
ファイルの完全なコードです。
package sumaregi
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"path"
)
type Client struct {
httpClient *http.Client
config *Config
token string
}
const (
APIPathPos = "/pos"
)
func NewClient(config *Config, scopes []string, envVari EnvironmentVariable) (*Client, error) {
token, err := getAccessToken(scopes, envVari)
if err != nil {
return nil, err
}
client := &Client{
httpClient: &http.Client{},
config: config,
token: token,
}
return client, nil
}
func (c *Client) call(
ctx context.Context,
apiPath string, method string,
queryParams url.Values, postBody interface{},
res interface{},
) error {
var (
contentType string
body io.Reader
)
if method != http.MethodDelete {
contentType = "application/json"
jsonParams, err := json.Marshal(postBody)
if err != nil {
return err
}
body = bytes.NewBuffer(jsonParams)
}
req, err := c.newRequest(ctx, apiPath, method, contentType, queryParams, body)
if err != nil {
return err
}
return c.do(ctx, req, res)
}
func (c *Client) newRequest(
ctx context.Context,
apiPath string, method string,
contentType string,
queryParams url.Values,
body io.Reader,
) (*http.Request, error) {
u, err := url.Parse(c.config.APIEndpoint)
if err != nil {
return nil, err
}
u.Path = path.Join(u.Path, c.config.ContractID, APIPathPos, apiPath)
u.RawQuery = queryParams.Encode()
req, err := http.NewRequest(method, u.String(), body)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
req.Header.Set("Authorization", "Bearer "+c.token)
return req, nil
}
func (c *Client) do(
ctx context.Context,
req *http.Request,
res interface{},
) error {
response, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
return
}
}(response.Body)
if res == nil {
return nil
}
if response.StatusCode != http.StatusOK {
bodyBytes, err := io.ReadAll(response.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %v", err)
}
return fmt.Errorf("HTTP %d: %s", response.StatusCode, string(bodyBytes))
}
return json.NewDecoder(response.Body).Decode(&res)
}
6. 次回予告
次回の記事では、今回解説したクライアントを実際に使用して、Sumaregi APIにリクエストを送信し、レスポンスを取得する方法を紹介します。具体的には、client.go
で実装したcall
メソッドを使って、Sumaregi APIのエンドポイントにリクエストを行い、取得したデータを処理する一連の流れを解説します。
次回の内容のハイライト:
-
Sumaregi APIへのリクエスト送信:
client.go
のクライアントを使用して、Sumaregi APIにリクエストを送信します。 - レスポンスの処理: APIからのレスポンスを受け取り、データを解析して活用する方法を説明します。
- エラーハンドリング: APIリクエスト時のエラー処理と、想定外のレスポンスが返ってきた場合の対応について詳しく解説します。
この次回の記事では、実際にSumaregi APIとやり取りする方法を実践的に理解できるように、サンプルコードも交えながら進めていきます。Sumaregi APIと連携したアプリケーションを開発するための次のステップを学ぶ機会ですので、ぜひお楽しみにしてください!
Discussion