こんにちは。 kimihiro_nです。
Microsoftから「Kiota」というOpenAPIの定義からクライアントコードを生成するツールが公開されていたのでちょっと触ってみました。
Kiota の特徴
JSON、YAMLで書かれたOpenAPIのAPI仕様から、APIを呼び出すためのクライアント部分を自動生成してくれるツールです。 特徴としてはGoやPythonなど様々な言語への書き出しに対応していて、似たようなインターフェースで扱える点になります。 API仕様を一度共通の内部的なモデルに変換し、そこから各言語のクライアントを生成する面白いアプローチを取っています。
似たようなツールだとOpenAPI Generatorという有名なものがありますが、 ツール自体が巨大になってきてしまっているのと、CLIからだとGo言語でクライアントのみのコードを生成する方法が分からなかった(サーバー側のコードも一緒に生成されてしまう)ため、クライアントに特化したKiotaを試してみることにしました。
Kiotaを使ってみる
インストール
インストール手順はこちら。 コミュニティ作成によるものですがbrewによるインストールもできます。
OpenAPI の用意
クライアントを作るOpenAPI仕様を用意します。
swagger: "2.0" basePath: /api/v1 definitions: main.ErrResponse: properties: detail: type: string type: object main.Post: properties: content: type: string id: type: integer title: type: string required: - content - title type: object main.PostListResponse: properties: posts: items: $ref: '#/definitions/main.Post' type: array type: object info: contact: {} title: ExampleWebAPI version: "1.0" paths: /posts: get: consumes: - application/json description: get posts parameters: - description: limit in: query name: limit type: integer - description: offset in: query name: offset type: integer produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/main.PostListResponse' "500": description: internal server error schema: $ref: '#/definitions/main.ErrResponse' summary: Get List of Posts tags: - posts post: consumes: - application/json description: add post parameters: - description: Post in: body name: post required: true schema: $ref: '#/definitions/main.Post' produces: - application/json responses: "201": description: created schema: $ref: '#/definitions/main.Post' "400": description: invalid params schema: $ref: '#/definitions/main.ErrResponse' "500": description: internal server error schema: $ref: '#/definitions/main.ErrResponse' summary: Create new post tags: - posts
投稿の一覧取得と投稿が出来る簡単なAPIです。 諸事情(後述)でOpenAPI 3.xではなくてOpenAPI 2.0の仕様に合わせて作っています。 2.0の方はSwaggerとも呼ばれてます。
Goクライアントの生成
こちらからKiotaのGoクライアントを生成してみます。
kiota generate -l go -d ./swagger.yaml -o ./client -n ${レポジトリ名}/client
-l
オプションでGo言語を、-d
オプションでOpenAPI(Swagger)ファイルの置き場を、-o
で出力先ディレクトリを、
そして -n
で生成されるクライアントのフルパッケージ名を指定します。
フルパッケージ名は生成されたコードのimportが正しく動くために必要で、go mod init
したときのレポジトリ名 + 出力時のディレクトリ(/client)を指定します。
実行するとこのようなファイルが生成されます。
これらのファイルは触らなくて大丈夫ですがimportのエラーが出ている場合は-n
オプションに渡している値が適切かどうかを確認してみてください。
Goクライアントを使ってみる
まずはAPIクライアントを初期化から。
package main ... import "github.com/microsoft/kiota-abstractions-go/authentication" import "github.com/microsoft/kiota-http-go" import ${レポジトリ名}/client func main() ctx := context.Background() authProvider := authentication.AnonymousAuthenticationProvider{} adapter, err := http.NewNetHttpRequestAdapter(&authProvider) if err != nil { log.Fatalf("Error creating request adapter: %v\n", err) } // サーバーのURLをセット adapter.SetBaseUrl("http://localhost:8080/api/v1") apiClient := client.NewApiClient(adapter)
API認証が必要なケースに対応するためのauthProviderを生成し、そこから通信用のadapterを作っています。 今回は認証不要のAPIを叩くのでAnonymousAuthenticationProviderを利用しました。 adapterはAPI通信部分を吸収する部分で、ここで独自のHTTP通信クライアントを組み込んだりも出来ます。 OpenAPIにサーバーのエンドポイントの記載がない場合はこのようにadapterで指定します。 adapterからクライアントを生成したら準備完了です。
limit := int32(3) params := posts.PostsRequestBuilderGetQueryParameters{ Limit: &limit, } // リクエストを送信 result, err := apiClient.Posts().Get(ctx, &posts.PostsRequestBuilderGetRequestConfiguration{ QueryParameters: ¶ms, })
リクエストを組み立ててAPIを呼び出す部分のコードはこのような形です。 APIごとに専用の構造体が生成されているのでそれを利用して組み立てる形になります。 パラメータが未指定の場合と区別するためすべてポインタ型で渡すようなインターフェースになっています。 数値から直接ポインタを取ることはできないので、一度代入が必要な点は少し手間になります。 lo.ToPtr みたいな関数を利用してもいいかもしれません。
result, err := apiClient.Posts().Get(/* 省略 */) if err != nil { // OpenAPIで定義されているエラーは errors.As でキャスト出来る var errResp *models.ErrResponse if errors.As(err, &errResp) { fmt.Printf("request error: %s\n", *errResp.GetDetail()) return } fmt.Printf("Error getting inference result: %+v\n", err) return } // Post の一覧を出力 for _, post := range result.GetPosts() { fmt.Printf("id: %d, title: %s, content: %s\n", *post.GetId(), *post.GetTitle(), *post.GetContent()) } /* 実行結果例 id: 1, title: First Post, content: This is first test post. id: 2, title: Second Post, content: This is second test post. id: 3, title: Third Post, content: This is third test post. */
リクエストを行うと、パースされたレスポンスとエラーにアクセスすることが出来ます。 エラーはOpenAPIに定義されていれば専用のモデルが生成されるので、キャストすることでエラーオブジェクトにもアクセスすることが可能です。 定義されていないエラーの場合は汎用的なエラーが返ってきます。 正常時のレスポンスもパースされた状態で入っておりGetter経由で好きに取り出すことが出来ます。 JSONのパースなどを自分で記述しなくていいのはコード生成の大きなメリットです。
// リクエストデータの作成 requestData := models.NewPost() title := "Hello world" requestData.SetTitle(&title) content := "This is a test from kiota apiClient." // リクエストに追加 requestData.SetContent(&content) // リクエストを送信 result, err := apiClient.Posts().Post(ctx, requestData, &posts.PostsRequestBuilderPostRequestConfiguration{ Headers: header, Options: []abstractions.RequestOption{ kiotaHttp.NewCompressionOptions(false), // リクエストBodyのgzipをオフに }, }) // 作成した Post の ID を出力 fmt.Printf("saved id: %d\n", *result.GetId()) /* 実行結果例 saved id: 4 */
POSTを使って送信する例はこちら。 こちらも専用のモデルが定義されているのでSetterを利用して値をセットしていく形になります。
試してみたときのはまりどころとしては、デフォルトだとリクエストが圧縮されて送られてしまうことでした。
サーバー側でリクエストが弾かれてしまい、原因を探っていったところBodyをgzipして送信していることが分かりました。
リクエスト時のオプションとしてCompressionをfalseにセットしてあげることで Content-Encoding: gzip
に未対応なサーバーでも適切にリクエストを送ることが出来ます。
OpenAPIの生成もコードファーストでやってみる
OpenAPI 3.xではなくOpenAPI 2.0の形式でYAMLを生成していた部分の答え合わせなのですが、今回OpenAPIの生成もGoのコードから作ってみることにしました。
Python だとFastAPIのようなフレームワークで簡単に出力できますが、Goだとフレームワークと一体でOpenAPIを生成してくれるものは見当たりませんでした。 OpenAPIを用意してサーバー側のコードを生成する「スキーマファースト」なやり方であれば、OpenAPI Generatorをはじめとして複数候補がありますが、 OpenAPIを手で書くのが辛い、ファイル分割の対応具合がツールによってまちまちなどの理由で「コードファースト」なやり方を模索してました。
最終的にたどり着いたのがSwagというツールでした。 こちらはAPIサーバーにコメントの形でOpenAPIに必要な情報を埋め込むことでOpenAPIのスキーマを生成してくれるツールです。 gin, echo, fiberなど主要なWebフレームワークにも対応しています。
type Post struct { ID int `json:"id" binding:"-"` Title string `json:"title" binding:"required"` Content string `json:"content" binding:"required"` } // AddPost // @Summary Create new post // @Schemes // @Description add post // @Tags posts // @Accept json // @Produce json // @Param post body Post true "Post" // @Success 201 {object} Post "created" // @Failure 400 {object} ErrResponse "invalid params" // @Failure 500 {object} ErrResponse "internal server error" // @Router /posts [post] func (s *Server) AddPost(g *gin.Context) { post := Post{} if err := g.ShouldBindJSON(&post); err != nil { g.JSON(http.StatusBadRequest, ErrResponse{Detail: err.Error()}) return } if err := s.postRepository.AddPost(g, &post); err != nil { g.JSON(http.StatusInternalServerError, ErrResponse{Detail: "internal server error"}) return } g.JSON(http.StatusCreated, post) }
このような形で各ハンドラにコメントを付与し、swagコマンドを叩く事でOpenAPIのスキーマを生成できます。 (上記はginの例になります。) Postなどの構造体名をコメントに入れておくとOpenAPIに反映してくれるのでスキーマの更新漏れがなくて便利です。
欠点としては生成されるOpenAPIが2.0相当になってしまう点です。 3.0が公開されたのが2017年なので、OpenAPIまわりのツール対応状況を考えると3.xに移行しておきたいところです。 v2という形でSwagのOpen API 3.x対応が進められていますが現時点ではリリースされていません。 RCまでは来ているようなのでそのうち公開されると思いますが。
まとめ
SwagとKiotaを利用することで、システム間の連携を扱いやすくすることが出来ました。 OpenAPIを利用してスキーマを管理しつつ、プログラムの上ではOpenAPIを意識せず扱えるので改修やメンテナンスも行いやすくなりそうです。
弊社の場合PythonとGoをよく利用するので、「PythonのFastAPIでOpenAPIを生成してKiotaのGoでクライアント生成」、「SwagでOpenAPIを生成してKiotaのPythonクライアントを生成」みたいな言語を跨いでの連携もしやすそうです。 似たようなシステム連携をするのに「Protocol Buffers」も選択肢に上がると思いますが、REST APIでの資産が既にある場合、現状のコードベースを生かしつつSwagなどでOpenAPI化し、自動生成されたクライアントで連携を堅固にしていくのも実用的かと思いました。
今回解説用に作成したサンプルコードはこちら github.com
The Go gopher was designed by Renée French.