🔥

Sumaregi-go: GoでのSumaregi APIクライアントライブラリ開発入門 (Part 2)

2024/09/15に公開

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エンドポイントに対してリクエストを送信するための主要な関数が含まれています。

1. 環境変数の管理 (EnvironmentVariable構造体)

クライアントの初期化や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構造体に格納します。
    • 引数developmenttrueの場合、開発用の環境変数を読み込み、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を取得します。
    • 処理の流れ:
      1. EnvironmentVariableからAPIエンドポイント(SmaregiAPIHost)と契約ID(SmaregiContractID)を取得します。
      2. これらの値を用いてConfig構造体を初期化します。
      3. 初期化された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との通信に必要なアクセストークンです。

4. クライアントの初期化 (NewClient関数)

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やシークレットなど)を保持する構造体。
  • 処理の流れ:

    1. getAccessToken関数を呼び出してアクセストークンを取得します。
    2. 新しいClient構造体を初期化し、HTTPクライアント、設定、トークンを設定します。
    3. 初期化に成功した場合は、新しいClientインスタンスを返します。エラーが発生した場合はエラーを返します。

5. APIリクエストを送信する (call関数)

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: レスポンスデータを格納する構造体。
  • 処理の流れ:

    1. HTTPメソッドがDELETEでない場合、postBodyをJSONにシリアライズし、リクエストボディとして設定します。
    2. newRequest関数を使用して新しいHTTPリクエストを作成します。
    3. do関数を呼び出してリクエストを実行し、レスポンスを処理します。

6. リクエストの作成 (newRequest関数)

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ヘッダーを設定します。

  • 処理の流れ:
    1. c.config.APIEndpointを基にAPIのエンドポイントURLを構築します。
    2. パスとクエリパラメータを設定して、完全なURLを作成します。
    3. http.NewRequestで新しいHTTPリクエストを作成し、コンテキスト、メソッド、URL、ボディを設定します。
    4. コンテンツタイプが指定されている場合、Content-Typeヘッダーを設定します。(今回は、application/json)
    5. アクセストークンを使用してAuthorizationヘッダーを設定します。

7. リクエストの実行 (do関数)

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リクエストを実行し、レスポンスを処理します。

  • 処理の流れ:
    1. httpClientを使ってリクエストを実行し、レスポンスを受け取ります。
    2. レスポンスのステータスコードが200 OKでない場合、エラーメッセージを返します。
    3. レスポンスボディをデコードし、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と連携したアプリケーションを開発するための次のステップを学ぶ機会ですので、ぜひお楽しみにしてください!

https://zenn.dev/ttsbs/articles/75a177a875ba38

Discussion