spf13/cobra で HTTP Client を作成する

はじめに

OpenAPI 3.0 の yaml ファイルをベースに CLI を自動生成するにあたり spf13/cobra を利用しようと思っています。
今回は spf13/cobra の復習をかねて環境構築と HTTP Client を作成してみます。

spf13/cobra

spf13/cobra はgoで書かれたCLI applicationsを作成するためのライブラリです。
Kubernetes や Hugo、 CockroachDB などで利用されているようです。

spf13/cobra のインストール

go get でインストールすることができます。

$ go get -u github.com/spf13/cobra/cobra

spf13/cobra で雛形の生成

cobra にはコマンドで雛形を生成する機能があります。
viper というファイル等から設定を読み込むライブラリは今回利用しない想定なので--viper=falseを指定しcli-test という名前で雛形を生成しています。

$ cobra init --pkg-name=github.com/<YOUR PATH>/cli-test --author=<YOUR NAME> --license=mit --viper=false

以下のような構成で雛形が生成されました。

$ tree .
.
├── LICENSE
├── cmd
│   └── root.go
└── main.go

サブコマンドの生成

以下のコマンドでサブコマンドを生成することができます。

$ cobra add users
$ tree .
.
├── LICENSE
├── cmd
│   ├── root.go
│   ├── users.go
└── main.go

生成されたusers.goに対してfmt.Println("users")するだけのRunEを追加します。

package cmd

import (
    "fmt"

    "github.com/spf13/cobra"
)

var usersCmd = &cobra.Command{
    Use:   "users",
    Short: "A brief description",
    Long:  `A longer description`,
    RunE: func(cmd *cobra.Command, args []string) error {
        fmt.Println("users")
        return nil
    },
}

func init() {
    rootCmd.AddCommand(usersCmd)
}

サブコマンドとして users を渡して main.go を実行するとusersが表示されることが確認できます。

$ go run main.go users
users

特定のサブコマンドに紐づくサブコマンドを生成

特定のサブコマンドに紐づいたサブコマンドは--parent=usersCmdのようにparentとして親のサブコマンドを指定します。
ファイル名を指定できるオプションがないようなのでコマンドで生成後手で変更してます。

$ cobra add list --parent=usersCmd
$ mv cmd/list.go cmd/users_list.go

生成されたusers_list.goに対してfmt.Println(""list called")するのRunEを追加します。

package cmd

import (
    "fmt"

    "github.com/spf13/cobra"
)

var listCmd = &cobra.Command{
    Use:   "list",
    Short: "A brief description",
    Long:  `A longer description`,
    RunE: func(cmd *cobra.Command, args []string) error {
        fmt.Println("list called")
        return nil
    },
}

func init() {
    usersCmd.AddCommand(listCmd)
}

サブコマンドとしてusers listを渡して main.go を実行するとlist calledが表示されることが確認できます。

$ go run main.go users list
list called

サブコマンドの利用を強制する

先ほどのusers.goRunEを以下のように削除します。

package cmd

import (
    "fmt"

    "github.com/spf13/cobra"
)

var usersCmd = &cobra.Command{
    Use:   "users",
    Short: "A brief description",
    Long:  `A longer description`,
}

func init() {
    rootCmd.AddCommand(usersCmd)
}

すると実行する対象が存在していないのでusersCmd配下に紐づいているコマンドを実行してね!とエラーを出してくれます。

$ go run main.go users
A longer description

Usage:
  cli-test users [command]

Available Commands:
  list        A brief description

Flags:
  -h, --help   help for users

Use "cli-test users [command] --help" for more information about a command.
subcommand is required
exit status 1

HTTP Client を実装する

users list を叩くと example.com へGETリクエストを出す Client を作ってみましょう。
以下のようにcmd配下にapiclient.goを作成します。

$ tree .
.
├── LICENSE
├── cmd
│   ├── apiclient.go
│   ├── root.go
│   ├── users.go
└── main.go

以下のように*http.Client を埋め込んだapiClient構造体を作りdoRequestメソッドとしてHTTPのリクエストを実行します。
また、各パラメータを指定するapiParams構造体を作ります。ここのパラメータを OpenAPI3.0 の YAML ファイルをparseし値を決定していく感じです。

package cmd

import (
    "bytes"
    "io/ioutil"
    "log"
    "net"
    "net/http"
    "net/url"
    "strings"
    "time"
)

type apiClient struct {
    httpClient *http.Client
    Logger     *log.Logger
}

type apiParams struct {
    method      string
    url         *url.URL
    path        string
    query       url.Values
    contentType string
    body        string
}

func newAPIClient() *apiClient {
    client := &http.Client{
        Transport: &http.Transport{
            Proxy: http.ProxyFromEnvironment,
            DialContext: (&net.Dialer{
                Timeout:   30 * time.Second,
                KeepAlive: 30 * time.Second,
                DualStack: true,
            }).DialContext,
        },
        Timeout: 10 * time.Second,
    }

    return &apiClient{
        httpClient: client,
    }
}

func (ac *apiClient) doRequest(params *apiParams) (*http.Response, string, error) {
    req, _ := http.NewRequest(params.method, params.url.String(), strings.NewReader(params.body))
    res, _ := ac.httpClient.Do(req)
    defer res.Body.Close()

    b, _ := ioutil.ReadAll(res.Body)

    return res, bytes.NewBuffer(b).String(), nil
}

今回は yaml ファイルを読みこむことはしませんが、以下のようにapiParamsを埋めてdoRequestに渡すと実行してくれるはずです。

var listCmd = &cobra.Command{
    Use:   "list",
    Short: "A brief description",
    Long:  `A longer description`,
    RunE: func(cmd *cobra.Command, args []string) error {
        ac := newAPIClient()

        u, _ := url.Parse("http://example.com")

        params := &apiParams{
            method: "GET",
            url:    u,
        }

        res, str, _ := ac.doRequest(params)

        fmt.Println("res:", res)
        fmt.Println("str:", str)

        return nil
    },
}

まとめ

spf13/cobra で HTTP Client を作成する方法をまとめました。