このエントリーをはてなブックマークに追加

goaのdesignからもっといいJSコードを生成する

goa便利ですね。designからサーバーコードが生成できるし、Swaggerでドキュメントも生成できるし。

でもちょっと待って下さい。サーバーのコードが生成できたとしても、結局フロントエンドのコードは書かないといけないですよね。うーむ。と思っていたところ、goのgoaでAPIをデザインしよう(クライアント編)という記事を拝見しました。

$ goagen js -d github.com/tikasan/goa-simple-sample/design

でJSコードが生成される、という話です。おお、素晴らしい、と思って試したのですが、残念ながらあれ…という印象でした。

  1. 引数の渡し方が、Path Paramとdataの差がなくて、必要なだけ渡したい場合にちょっと使いにくい
  2. クライアントサイドValidationがない

という具合です。特にクライアントサイドでのValidationがないのがあかんですね。せっかくdesignでいろいろ制約書けるのに。

ということで作ってみました

goaにはコードを生成する部分をpluginとして自前のパッケージを指定できます。これを使ってJSコードを生成するパッケージを作成してみました。

https://github.com/shirou/goagen_js

goagen gen --pkg-path=github.com/shirou/goagen_js -d github.com/some/your/great/design

で、指定したdesignをjsディレクトリ以下に書き出します。

特徴としては、

  1. ES 2015準拠でfetch APIを使ったPromiseを返す。名前もResource名 + Action名と、分かりやすく
  2. クライアントサイドValidation
  3. flowtypeやTypeScript形式での生成も指定可能

それぞれ順番に説明します。例として、以下のようなデザインを使うとします。

var UserCreatePayload = Type("UserCreatePayload", func() {
        Attribute("name", String, "name", func() {
            MinLength(4)
            MaxLength(16)
        })
        Attribute("age", Integer, "age", func() {
            Minimum(20)
            Maximum(70)
        })
        Attribute("email", String, "email", func() {
            Pattern(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`)
        })
        Attribute("sex", String, "sex", func() {
            Enum("male", "female", "other")
        })
    Required("name")
})

var _ = Resource("user", func() {
    BasePath("user")
    Response(InternalServerError)

    Action("create", func() {
        Routing(POST("create/:Type"))
        Params(func() {
            Param("Type", String, "type of user", func() {
                Enum("normal", "admin")
            })
            Required("UserID")
        })
        Payload(UserCreatePayload, func() {
            Example(map[string]interface{}{
                "name": "fooboo",
            })
        })
        Response(OK)
    })

使い方

goagen genでコードを生成すると、以下のような関数を持つ、api_request.jsが生成されます。

// UserCreate
// type_(string): type of user
// payload(object): payload
export function UserCreate(type_, payload) {
  const url = urlPrefix + `/user/create/${type_}`;
  let e = undefined;
  e = v.validate(v.UserCreate.Type, type_);
  if (e) {
    return Promise.reject(e);
  }
  e = v.validate(v.UserCreate.payload, payload);
  if (e) {
    return Promise.reject(e);
  }
  return post(url, payload);  // 同時に生成されるヘルパー関数
}

ですので、以下のように使えます。

import * as api from "./api_request.js";

const payload = {age: 30, name: "shirou"}
api.UserCreate("admin", payload).then((response) => {
    ...
});

クライアントサイドValidation

先程の関数の中で、v.validate(v.UserCreate.Type, type_)というのがありました。これがValidationです。api_validator.jsというファイルに以下のコードが生成されています。

export const UserCreate = {
   "Type": {
     "kind": "string",
     "enum": [
       "normal",
       "admin"
     ]
   },
   "payload": {
     "age": {
       "kind": "number",
       "minimum": 20,
       "maximum": 70
     },
     "email": {
       "kind": "string",
       "pattern": "^[a-z0-9._%+-]+@[a-z0-9.-]+.[a-z]{2,4}$"
     },
     "name": {
       "kind": "string",
       "min_length": 4,
       "max_length": 16
     },
     "sex": {
       "kind": "string",
       "enum": [
         "male",
         "female",
         "other"
       ]
     }
   }
 };

これを、同時に生成されているvalidateというヘルパー関数に食わせてあげます。返り値がundefinedであれば問題なし。問題があれば文字列が返ってきます。

リクエストとvalidationを分離しているのは、例えば入力のたびにon-the-flyでチェックする、という用途がフロントエンドではよくあるからです。

import * as v from "../api_validator.js";

// name だけチェックする
if v.validate(v.UserCreate.payload.name, value) !== undefined {
   // validationエラー
}

という感じで使えます。エラー時にどういう文言が返ってくるかはapi_validator.jsの中に定義されてるので、適宜使ってください。

これにより、フロントエンドとサーバーとで同じvalidationを二回書く、ということがなくなります。

flowtypeやTypeScript形式

goaはgolangで書いていますので、型情報があるわけです。しかし、JavaScriptにした瞬間にせっかくの型情報が失われてしまいます。あまりにももったいない。ということで、flowtypeやtypescriptで生成することもできるようにしました。

 goagen gen --pkg-path=github.com/shirou/goagen_js -d github.com/shirou/goagen_js/example/design -- --target flow

or

 goagen gen --pkg-path=github.com/shirou/goagen_js -d github.com/shirou/goagen_js/example/design -- --target type --genout ts

typescriptの場合、tsというディレクトリ名が普通かな、と思いますのでgenoutオプションで吐き出すディレクトリ名を指定しています。なお、拡張子もtsになります。

こうすると、

export function UserCreate(type_: string, payload: UserCreatePayload): Promise<any> {

という感じで型情報がくっつきます。UserCreatePayloadは同時に生成されるapi.d.tsファイルに以下のように定義されます。

interface UserCreatePayload {
  name: string;
  age: number;
  email: string;
  sex: ["male","female","other"];
}

本来はResponseの型情報も定義したいところですが、まだそこまで手が回っていません。

発表資料

ということをgoa勉強会で発表しました。

資料はGenerate better JavaScript From Goa Designです。

swaggerから生成したほうが良くね?

といったところで、ふと気が付きました。

goaはswaggerを吐き出せるんだから、swaggerからJSコード生成すればよくね?

調べてみると、swagger-codegenでJSどころかTypeScriptのコードも生成できるではないですか。

おおう。orz...

ということで、ここまで作ったところで実はちょっと継続して作成するモチベーションが下がってしまいました。codegenだとValidationがないので、Validationだけ別で生成するかもしれません。

まとめ

  • 標準のJSコード生成は正直微妙
  • goagen_js作ったYO
  • クライアントValidationやTypeScript生成もできちゃうー
  • あれ、swagger-codegenのほうがよくね…?
  • goaはいいぞ!