概要
Go から Microsoft Azure を利用するの方法でソースコードを生成すると,スライス型の構造体メンバに omitempty が付かず API 呼び出しがエラーになる場合があった.
その原因ととりあえずの対策をまとめる.
go-swagger が生成する構造体
例えば,
$ swagger generate client \
-f https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/batch/2016-07-01.3.1/swagger/BatchService.json \
-t batch
上記のコマンドで Batch API のバインディングを作った場合,
ジョブプールの追加に必要なパラメータを表す構造体として,下記のような PoolAddParameter が生成される.
type PoolAddParameter struct {
// The list of application packages to be installed on each compute node in the pool.
//
// This property is currently not supported on pools created using
// the virtualMachineConfiguration (IaaS) property.
ApplicationPackageReferences []*ApplicationPackageReference `json:"applicationPackageReferences"`
AutoScaleEvaluationInterval strfmt.Duration `json:"autoScaleEvaluationInterval,omitempty"`
AutoScaleFormula string `json:"autoScaleFormula,omitempty"`
// 以下略
}
ここで,ApplicationPackageReferences のタグに omitempty が付いていないため,
この構造体インスタンスを Marshal すると,ApplicationPackageReferences はデフォルトで null が書き出されてしまう.
Azure は null が含まれているリクエストを無条件にリジェクトするので,
null が書き出されないように要素数 0 のスライスを設定する必要がある.
たいていの場合は,この空のスライスを設定しておけば問題ないのだが,上記の PoolAddParameter は注釈に
virtualMachineConfiguration (IaaS) property とは競合すると書かれていて,
実際 virtualMachineConfiguration に値が設定してあると,空のスライスであってもエラーになる.
(どちらか片方だけ設定するように言われる)
go-swagger 的には未設定と空を区別するために omitempty をあえて付けないでいるようなので,
(参考: can't distinguish between an empty array and null array for non-required array fields)
Azure サーバの方で null を認めるかプログラムの方でなんとかするしかない.
とりあえずの対策
自動生成されたソースコードを調べて omitempty をつけて回るか,
リフレクションや ast を使って動的に対処すれば良いのだが,
OAuthアクセストークンを使ってMicrosoft Azureにアクセスするで扱ったクライアントの Transport に,
リクエストの構造体をテキストメッセージ(この場合は JSON 文書)に変換する Producer を登録できるので,
この Producer を書き換えて対応することにする.
Producer は,go-openapi/runtime パッケージにて,
type Producer interface {
// Produce writes to the http response
Produce(io.Writer, interface{}) error
}
と定義されている.
今回は,とりあえず動けば良いと思って下記のような Producer を用意した.
type MinimalJSONProducer struct {
regexp *regexp.Regexp
blank []byte
}
func NewMinimalJSONProducer() *MinimalJSONProducer {
return &MinimalJSONProducer{
regexp: regexp.MustCompile("(\"[^\"]+?\":null,?|,\"[^\"]+\":null)"),
blank: []byte(""),
}
}
func (p *MinimalJSONProducer) Produce(out io.Writer, msg interface{}) (err error) {
data, err := json.Marshal(msg)
if err != nil {
return
}
data = p.regexp.ReplaceAllLiteral(data, p.blank)
_, err = out.Write(data)
return
}
Producer の登録は,OAuthアクセストークンを使ってMicrosoft Azureにアクセスするの時と同様にクライアントを *httptransport.Runtime にキャストして登録する.
API 呼び出しの通信で使われているリクエストのコンテンツタイプは application/json; odata=minimalmetadata
なので,
このコンテンツタイプ用の Producer として先ほど定義した MinimalJSONProducer を与える.
import(
httptransport "github.com/go-openapi/runtime/client"
"github.com/go-openapi/strfmt"
// go-swagger が生成したパッケージ
"github.com/jkawamoto/roadie/cloud/azure/batch/client"
)
cli := client.NewHTTPClient(strfmt.NewFormats())
switch transport := cli.Transport.(type) {
case *httptransport.Runtime:
transport.Producers["application/json; odata=minimalmetadata"] = NewMinimalJSONProducer()
// 子クライアントに登録
cli.Accounts.SetTransport(transport)
cli.Jobs.SetTransport(transport)
}
Transport をいじったら,各子クライアントに設定するのを忘れないように.