プログラマブログ

by wacul

menu
  • プログラマ
  • JSON Hyper-Schema からAPIドキュメントとGoのコードを自動生成する

2014.10.28JSON Hyper-Schema からAPIドキュメントとGoのコードを自動生成する

3行で言うと

  • herokuが作ってる prmd を使って、JSON SchemaからAPIドキュメントを出力したよ!
  • スキーマ定義から、GoのAPI実装コードも出力するツールを作ったらめっちゃ捗るよ!
  • Goのバリデーション用のライブラリも作ったよ!

今回作ったものの概要とサンプルコード

概要

以前から、APIを開発する上で、以下のようなことが課題となっていました。

  • そもそもドキュメント書くのがつらい
  • それもあって、ドキュメントより先にコードが変わってしまう
  • ドキュメントと実装の状況の違いが把握しづらい

また、ロジックがそんなに複雑ではないAPIでは、実装の作業は

  • リクエストデータのバリデーション
  • 出力データの整形 (フィルタリング)

の2つの作業が大きな割合を占めます。

APIの定義ファイルからドキュメントと、バリデーションや出力データ整形のコードを自動生成できれば、大幅に効率が上がると思い実装してみました。

今回実装した仕組みの全体の処理の流れは以下の様になっています。

  • prmd を使って、schema.json (JSON Hyper-Schemaドキュメント) を生成 prmd combine
  • schema.json から、APIドキュメント (schema.md) を生成 prmd doc
  • schema.json から、Goのサーバー側実装を生成 (独自実装)

概要

サンプルコード

今回の記事のサンプルコードは https://github.com/wcl48/go-api-generation-sample にあります。

サンプルプロジェクト内のディレクトリ、ファイルは以下の様な構成です

1
2
3
4
5
6
7
8
9
10
11
12
13
Cakefile            # ビルドタスク定義
README.md
bin/                # ビルド用スクリプト
package.json
templates/          # 共通 go ファイル(自動生成時にパッケージ名だけ変えてコピーされる)
test/               # テスト用のスキーマ定義
    gen/            # 生成されたGoパッケージの出力先
    meta.yml        # prmd メタ情報
    overview.md     # ドキュメントのトップに挿入されるマークダウン
    schema.json     # prmd で出力される schemaファイル
    schema.md       # prmd で出力されるドキュメント
    schemata/       # prmd で使うスキーマ定義
test-build.sh       # ビルド用スクリプト

APIの定義 : JSON Hyper-Schema

Rest API を定義するための仕様はいくつか世の中にあって、代表的なものとしては

を見つけました。

そこそこ世の中に浸透している「JSON Schema」に対する拡張であり、関連ライブラリなども豊富なことから、今回は JSON Hyper-Schema を選択しました。

ドキュメンテーション: prmd by heroku

heroku が作っている、 prmd というツールがあります。prmd を使うと、APIの定義をファイルを分割して管理でき、JSON Hyper-Schema の生成とバリデーション、ドキュメントの生成が行えます。

herokuのAPIドキュメント もこのツールをベースに生成されているようです。 元のスキーマ定義 も公開されています。

サンプルコードでは、生成されたドキュメントが、 test/schema.md にあります。 元のスキーマ定義は、 test/schemata/hoge.yaml です。

どんなGoのコードを生成するか

ドキュメントは無事生成できたので、次にGoのコードの生成について考えます。

要件としては、概ね以下のようなものとしました。

  • 利用するライブラリは、標準ライブラリの net/http と Gorilla
  • 自動生成するコードは、手書きコードとパッケージレベルで分離する。
    • つまり、生成したコードは人の手でいじることなく、再生成が任意に実行できるように保つ
  • 対応するURLをあとで変更できる
  • リクエスト、レスポンスのオブジェクトに型つきでアクセスできる
  • リクエストオブジェクトのバリデーションを行う

リクエスト、レスポンスオブジェクトの定義

リクエスト、レスポンスオブジェクトは単純に、json schemaのオブジェクト定義を、Goの構造体の定義に変換するだけです。
サンプルコード: test/gen/struct.go

ロジックの注入

APIロジック部分の実装は、自動生成されたコードにハンドラを登録する形にしました。ハンドラは次のような定義を生成しています。(サンプルでは、POST /hoge というAPIを定義しています)

1
2
3
4
type HogePostParamDataHandler func(
  vars map[string]string,        // URLに含まれる、idなどのマップ
  param HogePostParam,           // リクエストオブジェクト
  r *http.Request) (Hoge, error) // レスポンスオブジェクトを返す

実装した関数を登録するには、次の関数を呼び出します。

1
2
3
4
5
func InjectHogePost(
  router *mux.Router,                                 // gorilla.mux のルーターオブジェクト
  dh HogePostParamDataHandler,                        // ハンドラの実装
  middleware func(http.HandlerFunc) http.HandlerFunc, // ミドルウェア
)

サンプルコード詳細: test/gen/hoge.go

リクエストのバリデーション

JSON Schemaには、オブジェクトのバリデーションを記述する仕様が用意されています。 ( http://json-schema.org/latest/json-schema-validation.html ) こいつら、Goのバリデーションコードを生成します。

オブジェクトをバリデーションするのにちょうどいいライブラリがなかったため、この部分については、バリデーションを定義するためのライブラリを作りました。

wcl48/valval

特徴として

  • バリデータの定義を使いまわせる
  • ネストしたオブジェクトもバリデーションできる
  • バリデータの中身はただの関数 ( func(interface{}) error )
  • 構造体と、 map[string]interface{} の両方をバリデーションできる

を備えています(実装がもうちょっと落ち着いたら別記事であげたいと思います)

バリデータとして、次のような感じで出力します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var HogePostValidator = valval.Object(valval.M{
  "Name": valval.String(),
  "Code": valval.String(
      valval.MinLength(8),
      valval.MaxLength(16),
      valval.Regexp(regexp.MustCompile(`^[a-z0-9]+$`)),
  ),
  "Email": valval.String(
      validateEmail,
  ),
    // ...長いので省略
}).Self(
  valval.RequiredFields("Email", "Name"),
)

サンプルコード: test/gen/validators.go

リクエストの中身(POSTなら r.Bodyに入っているJSON)を構造体に組み立た後、

1
2
3
4
if err := HogePostValidator.Validate(&reqData); err != nil {
  SendError(w, validateError2APIError(err))
  return
}

のようにして、バリデーションしています。
サンプルコード: test/gen/hoge.go

リクエストだけから判断できるエラーについては、自動生成側でバリデーションしてしまうことで、ロジック側のコード量をかなり抑えることができます。

Goのコードを自動生成する時の細かいTips

Goのコードを生成するときには幾つかポイントがあります。

適当に出力して、 go fmt

構文エラーのチェックと、フォーマットを自動でしてくれます。 改行だけ注意してコードを吐き出せばよしなにやってくれます。

使わない可能性があるimportは、_に代入しておく

例えば、 regexp を使う場合と使わない場合があるコードを出力するとき、import して使用していないものがあるとコンパイルエラーになります。 ちゃんとフラグ立てて出力するか、goimport など使えば綺麗になりますが、

1
2
3
4
5
6
7
import (
    ...
  "regexp"
    ...
)

var _ = regexp.Compile

のように出力してしまえば良いです。
これは、 Google APIのGoクライアントのコード をみて参考にしました。

まとめ

JSON Hyper-Schema から、APIのドキュメンテーション、Goのソースコードを生成することで、かなり効率のよいAPIの開発ができるようになりました。
静的な型付け言語とコード自動生成の組み合わせは、生成 → コンパイル(型と実装のチェック) → 修正 というサイクルが高速で回せるため、とても強力です。

まだ取り組み始めたばかりなので、これからどんどん改善していきたいと思います!

この記事を書いた人tutuming

株式会社ワカルの技術責任者です。フロントエンドからバックエンドまで、ひと通りやってます。最近の興味はチームづくりと、パンづくりです。

waculでは、プログラマを募集しています。

現在はプロダクトとして、課題発見から改善提案まで自動で行うWeb改善プラットフォーム「AIアナリスト」を開発中です。

waculの採用情報へ

ページトップへ