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<UserCreateMedia> {

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

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

interface UserCreateMedia {
  email: string;
  age: number;
  sex: string;
  name: string;
}

Responseは *Media として定義されます。

発表資料

ということを 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はいいぞ!

Comments

comments powered by Disqus