読者です 読者をやめる 読者になる 読者になる

golang で urf8 でない文字列を for range で回すとどうなるか

概要

golang の string は意識しなければたいていの場合 utf8 な文字列なわけですが, ファイルからテキスト読んできたりすると,非 utf8 な文字列が混じることとかあります. そんな 非 utf8 な文字列を for range で回した場合の挙動でハマったのでメモ.

端的に言うとRob Pike 氏のこれをよく読もう:

Strings, bytes, runes and characters in Go - The Go Blog

文字ごとに処理する

golang で string の長さをとると,それは byte 長です.たとえば len("日本語") の値は 9 になります. いわゆる文字()のことを golang では rune と定義していますが,文字ごとに処理したい場合には, for range で文字列を回してやればうまくいきます.(以下,文字とは rune の事と読み替えて下さい)

const s = "日本語"
for i, r := range s {
    fmt.Printf("%#U starts at byte position %d\n", r, i)
}

の結果は,

U+65E5 '日' starts at byte position 0
U+672C '本' starts at byte position 3
U+8A9E '語' starts at byte position 6

のようになります.文字の開始位置と文字が取れる仕組みです.

文字列が utf8 として正しくない場合はどうなるか?

golang のプログラムは utf8 で書くことになっているので,普段意識せずに string を使っていれば, string は utf8 のバイト列で表現されています.しかし,string は単に変更できない byte 列のようなものなので, utf8 として正しくないものも設定することが出来ます.また,ファイルから文字列を読み込んでくる場合など, 日本語環境では sjis などの文字列を読み込んで文字列に設定してしまうかも知れません.

そのようなとき,上の for range によるループでは返値はどうなるでしょうか?

const s = "\x93\xFA\x96\x7B\x8C\xEA" // "日本語" の sjis 表現
for i, r := range s {
    fmt.Printf("%#U starts at byte position %d\n", r, i)
}

答えはこうなります.

U+FFFD '�' starts at byte position 0
U+FFFD '�' starts at byte position 1
U+FFFD '�' starts at byte position 2
U+007B '{' starts at byte position 3               // ← ここだけたまたま文字として取れてる
U+FFFD '�' starts at byte position 4
U+FFFD '�' starts at byte position 5

for range の処理は,文字として取れなければ,1 byte ごと進める. そして,文字として取れないときは,RuneError という「文字」を返す,という動作になります. この RuneError は コードポイントとしては U+FFFD で表されて,utf8 の表現では, 0xEF 0xBF 0xBD という byte 列になり, 長さは 3 byte です.

なので,文字が返ってきたと思って,この文字の utf8 の長さ基準に処理を進めると,for は 1 byte しか進まないのに,文字の長さは 3 byte あって おかしな事になります.# そんな処理を書いていて,バグを生みました orz

これは,utf8.DecodeRune(String) などの文字列から文字をとる関数でも同様です.

実は,Rob Pike 氏の詳しい解説 Strings, bytes, runes and characters in Go にもこのことが触れられているのですが,

f:id:ikawaha:20170519144942p:plain

練習問題になっているのでした.答えを知りたかったです,先生.

ANTLR のターゲットに Go が追加されたので Gogland とあわせて遊んでみる

概要

ANTLR ってのは,いわゆるパーサジェネレーターです. 去年の年末に出たバージョン 4.6 からターゲットに Go が追加されました 🙌 . かなり昔に使ったことあったんですが,v4 になってだいぶ整理されて洗練された感じになってました.

この記事は結構適当にやってしまっていると思うので,ちゃんとやりたいならリファレンスを読んだ方がよさそうです(ぉ.

The Definitive ANTLR 4 Reference

The Definitive ANTLR 4 Reference

ANTLR の簡単な紹介

入力のあるプログラム書いていると,入力が well-formed であるかどうかをチェックする必要があることがあります. 正規表現とか使って自分で入力が正しいかどうかチェックする程度の時は良いんですけど,複雑になってくると フルスクラッチで書いてメンテしていくのはつらい感じになってきます.

そんなときのための,そういった parser を作るフレームワークANTLR です.

ANTLR 自体は Java で書かれていますが,生成する parser は

に対応しています.JavaScript とかかなりクールだと思うのですが,まったく詳しくないので 気になる記事だけ貼っておきます. きっと JavaScripter の id:takuya-a =san がそのうちなんかやってくれると思います.

ANTLR and the web: a simple example - Federico Tomassetti - Software Architect

ANTLR を特におすすめするのは,作った文法を試しながら修正したりできるというところです. v4 より前までは ANTLR Works という IDE が別に作られていたんですけど,v4 は EclipseIntelliJ 用のプラグインが配布されています. 勘のよい方ならお気づきかもしれませんが,IntelliJプラグイン使えるんなら Gogland でも使えて,Go の開発はかどるんじゃないかと思ったら, まさにいけました ╭( ・ㅂ・)و ̑̑ グッ !.

f:id:ikawaha:20170318161618p:plain:w300

インストール

ANTLR

本家でダウンロードするか,mac だったら brew でも入ります.

今回は Go をターゲットにするので,Go 用のプラグインをダウンロードします.もちろん go get でいけます

go get github.com/antlr/antlr4/runtime/Go/antlr

ここで注意しなきゃいけないのは,go get した ライブラリにタグが振ってあるので,こいつを 4.6 に合わせておかなければいけない ということです.これでずいぶんハマりました.ベンダリングして管理するのが吉かもしれません.

✔ ~/go/src/github.com/antlr/antlr4 [master ↓·57|…1]
$ git checkout tags/4.6

Gogland

いまは Eary Build が出てるのでこれをインストールしてください.

www.jetbrains.com

ANTLRプラグインを追加する

Gogland で ANTLR 用のプラグインを追加します.

preferenes >> plugins から ANTLR プラグインを選択するだけです.簡単.

f:id:ikawaha:20170318163636p:plain:w300

文法を書く

では,ANTLR で文法を書いてみましょう.四則演算を受理する簡単な文法を書いてみます.

ファイル名は grammar で指定する文法名と同じにする必要があります.下の例だと Calc.g4 としてください.

grammar Calc;

prog:   expr NEWLINE*;
expr:   '(' expr_=expr ')'
    |   left=expr op=('*'|'/') right=expr
    |   left=expr op=('+'|'-') right=expr
    |   atom=INT
    ;

NEWLINE : [\r\n]+ ;
INT     : [0-9]+ ;

パーサーを生成する

Gogland から Go で書かれたパーサーを生成できます. まずは,ALTLR の 設定をしておきます.

f:id:ikawaha:20170318200209p:plain:w300

f:id:ikawaha:20170318200555p:plain:w300

Language のところに出力言語である Go を指定します.出力されるコードのパッケージは parser がデフォルトのパッケージ名になるので, 出力パスを parser として適当なところに出力させます.ListenerVisitorチェックボックスは後で説明しますが,とりあえずチェックしておいてください.

この状態で,Generate ANTLR Recognizer を選ぶとコードが出力されます.

パーサーを使ってみる

上の設定だと parser フォルダ以下にファイルが出力されます.

package main

import (
    "fmt"

    "github.com/antlr/antlr4/runtime/Go/antlr"

    "github.com/ikawaha/antlr/parser"
)

func main() {
    input := antlr.NewInputStream("12+3*4") // ← 入力

    lexer := parser.NewCalcLexer(input)
    stream := antlr.NewCommonTokenStream(lexer, 0)
    p := parser.NewCalcParser(stream)
    tree := p.Prog()  // ← ルールのトップに対応する関数が用意されてるのでそれを起動すると解析して解析木を返す

    fmt.Println(tree.ToStringTree([]string{}, p)) ← 解析木を LISP-style で表示する,最初の文字列配列をなぜ与えるのかはよく分からん
}

出力

(prog (expr (expr 12) + (expr (expr 3) * (expr 4))))

Listener と Visitor

ANTLR では,構文解析木を作ってから解析をするのではなくて,Listener や Visitor と呼ばれるものをあらかじめ Parser にセットしておいて解析しながら処理を行うことができる仕組みがあります.Listener は解析木を深さ優先で処理していく場合に使い,解析木の形に合わせてたどるノードを変えたい場合には Visitor を使います. Listener も Visitor もひな形ができているのでそれを使うだけです.

解析しながら式を計算するように Listener を用意してみます.基本的には parser フォルダ以下のファイルは編集しなくて良いはずです. Listener はノードに入るときと出るときにあわせてメソッドが用意されているので,それを上書きして処理を書きます.

parser 下にできる calc_base_listener.go がそれです.

type CalcListener interface {
        antlr.ParseTreeListener

        // EnterProg is called when entering the prog production.
        EnterProg(c *ProgContext)

        // EnterExpr is called when entering the expr production.
        EnterExpr(c *ExprContext)

        // ExitProg is called when exiting the prog production.
        ExitProg(c *ProgContext)

        // ExitExpr is called when exiting the expr production.
        ExitExpr(c *ExprContext)
}

こいつを利用して処理を書きます.自前の CalcListener を用意してみます. (Antlr4 で電卓を作る : 0xdeadbeef を参考にさせていただきました)

package main

import (
    "fmt"
    "strconv"

    "github.com/ikawaha/antlr/parser" // ← ここは生成したコードがある場所を指定
)

type CalcListener struct {
    *parser.BaseCalcListener // ← こいつを上書きして処理を書く
    Stack []int // ← 計算用のスタック
    Error error // ← エラーの記録用(こうするのが正しいのかはよく分かってない)
}

func NewCalcListener() *CalcListener {
    return &CalcListener{Stack: []int{}}
}

func (l *CalcListener) ExitExpr(ctx *parser.ExprContext) { // 解析木の expr ノードを出るときに呼ばれる
    if l.Error != nil {
        return
    }
    if ctx.GetExpr_() != nil {
        return
    }
    if ctx.GetOp() != nil {
        rhs := l.Stack[len(l.Stack)-1]
        l.Stack = l.Stack[:len(l.Stack)-1]
        lhs := l.Stack[len(l.Stack)-1]
        l.Stack = l.Stack[:len(l.Stack)-1]

        switch ctx.GetOp().GetText() {
        case "*":
            l.Stack = append(l.Stack, lhs*rhs)
        case "+":
            l.Stack = append(l.Stack, lhs+rhs)
        case "/":
            l.Stack = append(l.Stack, lhs/rhs)
        case "-":
            l.Stack = append(l.Stack, lhs-rhs)
        default:
            l.Error = fmt.Errorf("unknown op, %v", ctx.GetOp().GetText())
        }
        return
    }
    if ctx.GetAtom() != nil {
        i, err := strconv.Atoi(ctx.GetAtom().GetText())
        if err != nil {
            l.Error = fmt.Errorf("invalid value, %v", err)
        }
        l.Stack = append(l.Stack, i)
        return
    }
}

main.go をチョット書き換える.

package main

import (
    "fmt"

    "github.com/antlr/antlr4/runtime/Go/antlr"

    "github.com/ikawaha/antlr/parser"
)

func main() {
    input := antlr.NewInputStream("1+(2+3)*4-5")

    lexer := parser.NewCalcLexer(input)
    stream := antlr.NewCommonTokenStream(lexer, 0)
    p := parser.NewCalcParser(stream)

    listener := NewCalcListener() // Listener を parser にセットする
    p.AddParseListener(listener)

    tree := p.Prog()
    fmt.Println(tree.ToStringTree([]string{}, p))
    fmt.Printf("---> %+v\n", listener.Stack) // Listener の中身を出力する
}

結果

$ go run *.go
(prog (expr (expr (expr 1) + (expr (expr ( (expr (expr 2) + (expr 3)) )) * (expr 4))) - (expr 5)))
---> [16]

Visitor を使えばもっと凝ったことができると思います.使い方はおおむね Listener と同じです. Vistor や Listener を使う場合には,結果としての解析木を構築しなくてもいいわけなので,これを off にする方法が提供されています. parser の BuildParseTreesfalse にセットしてください.

Gogland で ANTLR preview を使う

GUI で任意の入力をあたえてインタラクティブに解析木をチェックすることができます. チョットわかりにくいのは,文法ファイルを開いて,テストの対象とするノードを選択することです. これが選択されてないと赤字で select from navigator or grammar とエラーが出ると思います.

ノードを選択したら,入力欄に好きなように文字列を入れてください.右側にどんどん解析木が組み上がってくと思います.

f:id:ikawaha:20170319000506p:plain:w300

僕もチョット触ってみただけで,たいしたこと書いてない割に長くなってしまいましたが,いろいろ遊べるツールだと思うので,是非リファレンスをあたってみてください.#そして便利な使い方を教えて欲しい

Happy Hacking !

Tips: goa で Consumes 指定したときはデフォルトの Content-Type タイプが読み込まれなくなるので注意

goa はデフォルトで json / xml / gob を受け付けてデコードしてくれるようになっているので特に意識することないと思うのですが,'application/x-www-form-urlencoded' とか,独自のデコーダー作りたいときとかには,Consumes 関数を API の中に書いて指定する必要があります.

たとえば,application/x-www-form-urlencoded をデコードするようにしたいときは,次のようにします.

var _ = API("myapi", func() {
        Scheme("http")
        Host("localhost:8080")

        Consumes("application/x-www-form-urlencoded", func() {  // ★ ← こんなかんじ
                Package("github.com/goadesign/goa/encoding/form")
        })
})

こうしておくと,あとは何も変更しないで指定した Payload に詰めてくれます.

ただそうすると,今まで読めてた json / xml は読めなくなってしまうので (この動作わかりにくい!)

var _ = API("myapi", func() {
        Scheme("http")
        Host("localhost:8080")

        Consumes("application/x-www-form-urlencoded", func() {
                Package("github.com/goadesign/goa/encoding/form")
        })
        Consumes("application/xml") // ★
        Consumes("application/json") // ★
})

としておくといいです.

ハマったので φ(..)メモ

( ‘-`).oO( ・・・あと,goa はデフォルトで gob を受け付けてくれるんですけど,これ使ったことある人いますかね.いたら教えて欲しい.

goa で Type と MediaType を混ぜないでうまく再利用する

概要

goa では レスポンスの形式を MediaType で定義します.一方,Type は Payload の形式などを定義します. MediaType は Type の特殊な形で,Veiw とか Link が増えたやつなので,Type の代わりに使えるんですけど, Type の代わりに使うと振る舞いが微妙に違うことから変なことになったりするので,代わりに使うのはやめておきたいところです.

でも,Payload で使う要素とMediaTypeで使う要素は大体一緒のことが多く,同じ事を何度も書いたりするのはダルいこともありますし, Attribute を流用したいということも多いのではないでしょうか.

Attribute を流用するいくつかの場面,方法について説明したいと思います.

Reference 関数を使う

Reference 関数を使うと,すでに定義済みの Type や MediaType の Attribute を選んで利用することができます.

次のタイプ定義があるとします:

var CreatePayload = Type("CreatePayload", func() {
        Attribute("no", Integer, "Number")
        Attribute("name", String, "Name of thingy", func() {
                MinLength(5)
                MaxLength(256)
                Pattern("^[a-zA-Z]([a-zA-Z ]+)")
        })
})

MediaType で上で定義した CreatePayload を指定することで,CreatePayload の Attribute をそのまま利用できます. 利用するためには,Attribute の名前を指定するだけで大丈夫です.

var MT = MediaType("application/vnd.app.mt", func() {
        Reference(CreatePayload)
        Attributes(func() {
                Attribute("name") // ← ★ ここで指定すると再利用される.再利用されるのは指定されたものだけ.
        })
        View("default", func() {
                Attribute("name")
        })
})

再利用した Attribute は必要に応じて,タイプ,詳細,バリデーションなどを上書きすることもできます.

DefaultMedia が定義されているときはそれを流用できる

var Results = MediaType("vnd.application/goa.results", func() {
    Description("The results of an operation")
    Attributes(func() {
        Attribute("value", Integer, "Results value")
        Attribute("requester", User)                 
    })
    Links(func() {
        Link("requester")
    })
    View("default", func() {
        Attribute("value")    
        Links()               
    })
    View("extended", func() {
        Attribute("value")    
        Attribute("requester")
    })
})

上のような MediaType があるときに,Action 内のパラメータを MediaType で定義されている Attribute で流用できます.

var _ = Resource("Operands", func() {
    DefaultMedia(Results) // ← ここで デフォルト MediaType を指定している
    Action("add", func() {
        Routing(GET("/add/:left/:right"))
        Params(func() {
            Param("left")    // ★ タイプ,詳細などがデフォルト MediaType から流用できる
            Param("right")   // ★
        })
        Response(OK)     
    })
})

一度定義したものをうまく流用して何度も同じものを定義して使うのを避けたいところですね.

vaaaaanquishさんの名前を間違えると指摘してくれる slack bot を goa で書く

概要

この記事は Go(その3) Advent Calendar の19日目に間に合わなかった今更ながらの記事です。

goa の紹介のために,slack の Outgoing-Webhooks を使って mattn さんの書かれた 「deeeetさんの名前を間違えると指摘してくれるbot」を goa で書くつもりで, 去年のアドベントカレンダー用に進めてたネタだったんですが, slack が投げてくるデータの形式が分からなくて頓挫していたのを最近解決したので,今更ながらに書いてみました.

slack で vaaaaanquish さんの名前を間違えると指摘してくれる bot を作ります.

おことわり

これはいわゆるネタですので,slack-bot を goa で作るのをおすすめしている訳ではないことにご注意ください. goa の機能の一端を例を通しながら見ていただければ幸いです.

vaaaaanquish さんとは

名前に a が異様に多くて一発では書けない,いま注目すべき(と勝手に僕が思ってる)一人鍋エンジニアさんです.

これまた勝手にお名前拝借いたします🙇.

twitter.com

slack の Outgoing-Webhooks について

slack の Outgoing-Webhooks は設定しておくと,チャンネルで発言した内容を設定しておいた URL に application/x-www-form-urlencoded 形式で メッセージを投げてくれて(なぜ json じゃないのか),これに json 形式で答えると,その答えを slack に表示してくれます.

slack が投げてくるデータは以下のようなサンプルが例示されています:

token=XXXXXXXXXXXXXXXXXX
team_id=T0001
team_domain=example
channel_id=C2147483705
channel_name=test
timestamp=1355517523.000005
user_id=U2147483697
user_name=Steve
text=googlebot: What is the air-speed velocity of an unladen swallow?
trigger_word=googlebot:

発言を投げるタイミングをキーワードを含むときだけにしたり,bot のアイコンを変更したり,いくつか機能がありますが,その辺は slack のドキュメントを参照してください.

api.slack.com

goa で API サーバのデザインを作る

完成品はこちらになります:https://github.com/ikawaha/vaaaaanquish-bot

goa では API デザインを書いて,そこからモックを生成し,ビジネスロジックを埋めます.

まずは API デザインから進めます.

API 定義

var _ = API("vaaaaanquish-bot", func() {
        Title("vaaaaanquish-bot")
        Description("vaaaaanquish さんの名前を間違って発言すると訂正してくれる slack bot です")
        Scheme("http")
        Host("localhost:8080")

        Consumes("application/x-www-form-urlencoded", func() {
                Package("github.com/goadesign/goa/encoding/form")
        })
})

Outgoing-webhooks は API へ送ってくるデータの形式が json ではなく application/x-www-form-urlencoded でくるので, Consumes 関数を使って www-form-urlencoded 用の Decoder を指定します.(デフォルトで jsonxml は受け付けるので普段はあまり設定しないです.ちなみに何も指定しないと json, xml を受け付けるように設定されますが,何か設定するときは,指定したものしか受け付けないようになります.なので,必要なら,jsonxml を受け付けるように指定する必要があります)

Consumes("application/xml")
Consumes("application/json")

こうしておくと,データの形式を jsonxml か www-form-encoded かということを特に気にすることなくいつも通りに Payload 指定などを書けます.どの Decorder を使うかはアクセス時の Content-Type をみて適切に切り替えてくれます(というコードが生成されます).

Incoming API

var _ = Resource("message", func() {
        BasePath("/v1/slack")
        DefaultMedia(MessageMedia)
        Action("inbound", func() {
                Routing(POST("/inbound"))
                Payload(SlackMessage)
                Response(OK)
        })
})

endpoint は /v1/slack/inbound とします.BasePath 関数を使うとこのリソースで共通する prefix path を指定できます. ここでは /v1/slack がエンドポイントの共通の接頭辞になります.なので Routing 関数で指定するパスは /inbound の部分だけで OK になります. Payload として,SlackMessage をしてしていますが,これはユーザー定義のペイロードです.下記で説明します Response は単に OK を返すように設定されています.これは DefaultMedia 関数でデフォルトのレスポンス・メディアタイプが指定されているので,省略して書けるようになっているからです.指定されている MessageMedia もユーザー指定のレスポンス・メディアタイプで,下記で説明します.

Payload

Payload は slack から送られてくるデータに対応した形式を設定する必要があります. www-form-encoded のデコード指定は API 関数で設定済みですので,ここではデータタイプだけを指定すれば大丈夫です.

var SlackMessage = Type("SlackMessage", func() {
        Attribute("token", String, "Slack Token")
        Attribute("team_id", String, "Team ID")
        Attribute("team_domain", String, "Team Domain")
        Attribute("channel_id", String, "Channel ID")
        Attribute("channel_name", String, "Channel Name")
        Attribute("service_id", String, "Service ID")                  // ← slack のサンプルにないけど,これがないとダメ
        Attribute("timestamp", Number, "Timestamp")
        Attribute("user_id", String, "User ID")
        Attribute("user_name", String, "User Name")
        Attribute("trigger_word", String, "Trigger Word")
        Attribute("text", String, "Message Text") 
})

slack のサンプルに従って項目を列挙していけばいいです.

ただし,service_id は slack のサンプルには出てこないので注意してください.

これではまってアドベントカレンダーには間に合いませんでした. んなもん気づけんわー

MediaType

レスポンスは json で返します.json の形式は bot に slack で発言させたいメッセージを {"text" : "hogehoge"} として返せば最低限事足ります. 必要に応じてレスポンスの項目を追加してください.

var MessageMedia = MediaType("application/vnd.vaaaaanquish.bot.message+json", func() {
        Attributes(func() {
                RequiredAttribute("text", String, "Message Text")
                Attribute("icon_url", String, "ICON URL")
                Attribute("icon_emoji", String, "ICON Emoji")
                Attribute("username", String, "User Name")
                Attribute("channel", String, "Other Channel")
        })
        View("default", func() {
                Attribute("text")
                Attribute("icon_url")
                Attribute("icon_emoji")
                Attribute("username")
                Attribute("channel")
        })
})

静的なファイルをサーブする

mattn さんの deeeet-bot ではトップページにアクセスすると下記のようなページを表示してくれます.

f:id:ikawaha:20170110233958p:plain:w300

こういった静的なファイルをサーブするには goa では Files 関数を利用できます.

var _ = Resource("public", func() {
        Files("/", "./templates/index.tmpl.html")
        Files("/static/*filepath", "./static/")
})

ビジネスロジックを実装する

goagen するとリソースに対応して message.go というファイルが,またメイン関数用に main.go というファイルができるので,これを編集していきます.リソースに対応するファイルには,エンドポイントのアクションのモックが用意されています.

goa で生成したコードで編集するファイルは,リソース関係のモックと main.go だけで,あとは手を入れる必要がありません.(もし,DO NOT MODIFY と書かれたファイルに手を入れようとしてる場合は,goa と達したい目的とが合わない可能性が高いです)

slack のメッセージを受けて応答する (message.go)

slack から来たメッセージは ctx.Payload にすでにデコード済みになりますので,そのテキストを正規表現でチェックして, 名前が間違っていたらそれをレスポンスとして指定して返します.

func (c *MessageController) Inbound(ctx *app.InboundMessageContext) error {
        m := re.FindAllString(ctx.Payload.Text, -1)
        ctx.Payload.Text = ""
        for _, t := range m {
                if t[1:] != "aaaaanquish" {
                        ctx.Payload.Text = t[0:1] + "aaaaanquish です..."
                        break
                }
        }
        res := toAppVaaaaanquishBotMessage(ctx)
        return ctx.OK(res)
}

heroku 用にポートを指定できるようにする (main.go)

デザインではポートは :8080 指定でしたが,heroku で公開できるように PORT という環境変数があれば, ポートをこの値に指定できるように生成されたコードをちょっと変更します.

var addr = flag.String("addr", defaultAddr(), "server address")

func defaultAddr() string {
        if s := os.Getenv("PORT"); s != "" {
                return ":" + s
        }
        return ":8080"
}

こうしておいて,サービスを開始するところを以下のようにします.

// Start service
if err := service.ListenAndServe(*addr); err != nil { // ← addr が適切にセットされているはず
        service.LogError("startup", "err", err)
}

ゴゴゴゴゴ・・・

f:id:ikawaha:20170110235823p:plain

まとめ

今回は goa で slack-bot 作ってみましたが,なんとなく goa の使い方が伝われば幸いです. まぁ,例として適当かどうかはアレですが,雰囲気が伝われば嬉しいです.

heroku にアップできるように heroku ボタンを付けておきましたので, goa を使って足りない機能を追加したり,改造したりして遊んでみてください.

去年ずっと引っかかっていたバグの原因が分かったうれしさで,ざっと記事にまとめてみましたが,やはりアドベントカレンダーの記事にするには微妙だったかな.

今年も Happy Hacking!

goa tips: 小ネタ (swaggerドキュメントの抑制とパラメータ必須要素について)

概要

goa のちょっとしたネタです.

swagger ドキュメントに出したくない要素を抑制する

swagger ドキュメントに出したくない要素を Metadata() を利用して抑制することができるようになりました. 特定の Resource を出力したくなければ Resource() の下で Metadata("swagger:generate", "false") を設定します. ただし,配下の要素で Metadata("swagger:generate", "true") を指定すると,出力の指定を上書きして,そちらが優先になります.

https://github.com/goadesign/goa/pull/863

Resource("invisible", func() {
    Metadata("swagger:generate", "false")
    Action("visible", func() {
        Metadata("swagger:generate", "true") // Override resource swagger:generate metadata
   })
})

これで何がうれしいかというと,swagger-ui を立ち上げるときにうれしいです. swagger-ui を利用するときに Files() を利用してサービスを立ち上げることができるんですが, この swagger-ui をサービスしている API 自体が swagger ドキュメントに記載されてしまってもんにょりする問題がありました.

swagger-ui を立ち上げる方法については過去の記事を参照ください.

var _ = Resource("swagger", func() {
        Metadata("swagger:generate", "false")
        Files("/swagger.json", "swagger/swagger.json")
        Files("/swaggerui/*filepath", "swaggerui/dist")
})

こうしておくと,もんにょりする事態が回避できそうです.

# この機能使って panic が起こるようなら,最新の goa に update してみてください.

パラメータや Payload などの必須要素を忘れないようにする

Attribute を設定して,その後に Required() で必須要素を選択しますが,パラメータをいじったり,必須要素を変更してたりすると齟齬が出やすくなります. そこで,Attribute と Required を同時に設定するハックです.

func RequiredAttribute(name string, args ...interface{}) {
        Attribute(name, args...)
        Required(name)
}

こういう便利関数を用意しておくと,どれが必須要素かわかりやすいですし,オプションにするときも変更しやすいです.

var MyPayload = Type("My Payload", func() {
        RequiredAttribute("id", Integer, "required item") // 必須
        Attribute("name", String, "optional item")
})

こんなかんじ.

goa tips: swagger-ui がサービスできないときのドキュメントどうする問題

概要

goa は swagger ドキュメントを生成してくれるので,これを swagger-ui をつかってサービスしてやると API が分かりやすく,お試しも出来てかなりいいかんじになります.しかし,環境によってはサービスを立ち上げることが出来ないとか,ドキュメントを確認して欲しい人がサービスにアクセスできないとかいうことが結構あります.そんなとき swagger.(json|yaml) をそのまま渡して 「swagger editor でみてね!」が通じない場合の対処方法です.

ブラウザで見れるように加工する

bootprint を使う

swagger.json を変換してブラウザで見れるように変換したものを用意します.利用するのは bootprint と bootprint-swagger です. これは npm で最初に一回インストールしておけばいいです.

Makefile に入れておいて,毎回生成しておくと吉だと思います.

準備

npm i -g bootprint
npm i -g bootprint-swagger

実行

$ bootprint swagger swagger/swagger.json api-doc

swagger-codegen-cli を使う

参考:https://github.com/swagger-api/swagger-codegen/blob/master/README.md#generating-static-html-api-documentation

準備

wget http://repo1.maven.org/maven2/io/swagger/swagger-codegen-cli/2.2.1/swagger-codegen-cli-2.2.1.jar -O swagger-codegen-cli.jar

nodeでサービスできる状態にする

以下でコードを生成した後,同じディレクトリで npm -installnode . でサービス立ち上げられます.localhost:8002にアクセスすると見た目キレイなドキュメント見られる.でも,結局手元で node 立ち上げてもらって閲覧してもらわないといけないので,ちょっと敷居高い.

実行

$ java -jar swagger-codegen-cli.jar generate -i swagger.json -l dynamic-html
$ npm install
$ node .

f:id:ikawaha:20161024154332p:plain

static な html として出力

こっちはホントに静的なhtmlドキュメント.

実行

$ java -jar swagger-codegen-cli.jar generate -i swagger.json -l  html

asciidoc / markdown に変換する

なにかこう,wiki的なものに markdown で残しておきたいときは swagger2markup を使うといいです.

http://swagger2markup.github.io/swagger2markup/1.0.1/#_command_line_interface

CLI用のツールがあるのでダウンロード

使い方

fooディレクトリを作ってそこに変換したドキュメントを生成します. 実行時のプロパティとして config.properties に書かれた値を利用します.

$ java -jar ~/lib/java/swagger2markup-cli-1.0.1.jar convert -i ./swagger/swagger.json -d ./foo -c ./config.properties

プロパティ

上で config.properties に書かれたプロパティを利用すると書きましたが,ファイル名は任意です. 指定できる値は下記にまとまっています.

http://swagger2markup.github.io/swagger2markup/1.0.1/#_swagger2markup_properties

とりあえず,markdown か asciidoc かだけを設定しておけばいけそう.デフォルトは asciidoc. なので,markdown にしたければこれで指定して下さい.

例:

swagger2markup.markupLanguage=MARKDOWN

とりあえず,こんな方法がありますが,もっといい方法があれば教えて欲しいです ╭( ・ㅂ・)و ̑̑

goa の controller を実装する

概要

goa のデザインが出来たら goagen でコードを生成しましょう. コードが生成できたら,次にコントローラ部分を書いていく必要があります.これはビジネスロジックにあたる部分です.

というか,goa はデザインを書いて,goagen してコード生成後は,ここしか編集する部分がないです.他の生成されたファイルには DO NOT MODIFY のコメントがヘッダについています.

準備:コントローラのコードはどこにある?

コントローラーのコードがどのコマンドでどこに配置されるかについては,

でまとめてありますので詳しくはこちらからあたってください.

以下では,おなじみの最小構成のデザインをサンプルにどのようにメディアタイプやペイロードを扱えばいいかを説明したいと思います.

最小構成のデザインサンプル:

package design                                     // The convention consists of naming the design
                                                   // package "design"
import (
        . "github.com/goadesign/goa/design"        // Use . imports to enable the DSL
        . "github.com/goadesign/goa/design/apidsl"
)

var _ = API("cellar", func() {                     // API defines the microservice endpoint and
        Title("The virtual wine cellar")           // other global properties. There should be one
        Description("A simple goa service")        // and exactly one API definition appearing in
        Scheme("http")                             // the design.
        Host("localhost:8080")
})

var _ = Resource("bottle", func() {                // Resources group related API endpoints
        BasePath("/bottles")                       // together. They map to REST resources for REST
        DefaultMedia(BottleMedia)                  // services.

        Action("show", func() {                    // Actions define a single API endpoint together
                Description("Get bottle by id")    // with its path, parameters (both path
                Routing(GET("/:bottleID"))         // parameters and querystring values) and payload
                Params(func() {                    // (shape of the request body).
                        Param("bottleID", Integer, "Bottle ID")
                })
                Response(OK)                       // Responses define the shape and status code
                Response(NotFound)                 // of HTTP responses.
        })
})

// BottleMedia defines the media type used to render bottles.
var BottleMedia = MediaType("application/vnd.goa.example.bottle+json", func() {
        Description("A bottle of wine")
        Attributes(func() {                         // Attributes define the media type shape.
                Attribute("id", Integer, "Unique bottle ID")
                Attribute("href", String, "API href for making requests on the bottle")
                Attribute("name", String, "Name of wine")
                Required("id", "href", "name")
        })
        View("default", func() {                    // View defines a rendering of the media type.
                Attribute("id")                     // Media types may have multiple views and must
                Attribute("href")                   // have a "default" view.
                Attribute("name")
        })
})

コントローラのコード

コントローラのコードは,デザインの Resource 関数で定義したリソースごとに,リソース名をファイル名として,ファイルに出力されます.上のデザインサンプルでは,Resource("bottle", func(){ ... とリソースがひとつ定義されているので,このリソースに対応して bottle.go というファイルが生成されます.

goagen main を実行するときに,リソースに対応するファイルがすでに生成されている場合には,gogen main サブコマンドは,そのファイルを上書きしません.なので,何度生成し直しても編集したものが失われることはないです.逆に,リソースに対応する Action 関数を追加しても,その endpoint に対応する関数が追加されないので,Action を追加したときなどは注意が必要です.--force オプションを使うと強制的に上書きしてくれるので,ファイルを待避したりして上書きしてみてひな形を確認してもいいかもしれません.なれれば(慣れなくても)Action に対応するひな形の関数は簡単に類推してかけると思うので,それほど心配しなくても大丈夫です.

bottle リソースには show というアクションがありますが,このとき,コントローラーのコードは以下のように生成されます.

生成されるコントローラーのコード:

package main

import (
        "cellar/app"
        "github.com/goadesign/goa"
)

// BottleController implements the bottle resource.
type BottleController struct {
        *goa.Controller
}

// NewBottleController creates a bottle controller.
func NewBottleController(service *goa.Service) *BottleController {
        return &BottleController{Controller: service.NewController("BottleController")}
}

// Show runs the show action.
func (c *BottleController) Show(ctx *app.ShowBottleContext) error {
        // BottleController_Show: start_implement

        // Put your logic here

        // BottleController_Show: end_implement
        res := &app.GoaExampleBottle{}
        return ctx.OK(res)
}

アクションに対応するメソッドが出来ています.これがそのアクションの endpoint と対応しますので,この関数を実装することを目指します.

実装すべき関数とコンテキスト

// Show runs the show action.
func (c *BottleController) Show(ctx *app.ShowBottleContext) error {
        // BottleController_Show: start_implement

        // Put your logic here

        // BottleController_Show: end_implement
        res := &app.GoaExampleBottle{}
        return ctx.OK(res)
}

丁寧に「ここを実装してね」と書いてあります.実装に必要なパラメータはすべてコンテキスト ctx で運ばれてきます. app.ShowBottleContext というのがそれです.これは app フォルダ配下の contexts.go で定義されています.ちょっと覗いてみると次のようになっています.

// ShowBottleContext provides the bottle show action context.
type ShowBottleContext struct {
        context.Context
        *goa.ResponseData
        *goa.RequestData
        BottleID int                // ←デザインで設定していたパスパラメータ
}

デザインで,パスパラメータとして定義していた BottleID が入っていることが分かります.これを利用してビジネスロジックを作成します.BottleID の値はデザインに描いてあるバリデーションは通過してきた後の値になるので,再度チェックする必要はありません.デザインではチェックできないようなバリデーションが必要な場合にだけパラメータをチェックするコードを書きましょう.

実際には,DB を引いたりする作業が必要になるかも知れません.DB を引き回したりするには色々方法があるとは思うのですが,ここではちょっと置いておいて,後で説明することにしたいと思います.

Payload を設定したらどうなるか?

作ろうとしているものが Web App ならば,JSON を Payload にして各種パラメータを受ける場合も多いと思います. Payload を設定した場合は,それらのパラメータはコントローラでどのように利用できるでしょうか?

※ Payload の設定法法自体は下記で解説しているので,詳細はこちらを見て下さい.

デザインで Payload を下記のように設定したとします.

BottlePayload = Type("BottlePayload", func() {
        Member("bottleID", Integer, "Bottle ID", func(){
                Minimum(0)
                Maximum(127)
        })
        Member("category", String, "Category", func(){
                Enum("red", "whilte", "rose")
                Default("red")
        })
        Member("comment", String, "Comment", func(){
                MaxLength(256)
        })
        Required("bottleID", "category")
})

var _ = Resource("bottle", func() {
        BasePath("/bottles")              
        DefaultMedia(BottleMedia)   

        Action("show", func() {          
                Description("Get bottle by id")
                Routing(GET("/:bottleID"))       
                Params(func() {                    
                        Param("bottleID", Integer, "Bottle ID")
                })
                Payload(BottlePayload) // ← ここに設定
                Response(OK)                       
                Response(NotFound)            
        })
})

すると,コントローラのコンテキストに Payload が含まれるようになります.

// ShowBottleContext provides the bottle show action context.
type ShowBottleContext struct {
        context.Context
        *goa.ResponseData
        *goa.RequestData
        BottleID int
        Payload  *BottlePayload   // ← ここ!
}

この Payload に含まれる値も,すでにバリデーションを通過してきた値なので,デザインで設定したバリデーションはコントローラ内でチェックする必要はありません.また,Payload はポインタになっていますが,nil にはならないので,nil チェックする必要はないです. 注:Payload が nil になる可能性があるのは,Payload をデザインで OptionalPayload 関数で設定した場合だけ.

レスポンスのデータを用意する

レスポンスデータの形式はで残で既に設定しているので,データの形式は app/media_type.go に生成されています. コントローラのひな形にも,OK の場合のレスポンスが用意されてますから,これを埋めてやれば,レスポンスが返ります.

        // BottleController_Show: end_implement
        res := &app.GoaExampleBottle{
                ID: ctx.BottleID,
                Name: "Akadama",
        }
        return ctx.OK(res)

レスポンス

$ curl -v -XGET localhost:8080/bottles/1
Note: Unnecessary use of -X or --request, GET is already inferred.
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET /bottles/1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.49.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/vnd.goa.example.bottle+json
< Date: Tue, 11 Oct 2016 10:37:39 GMT
< Content-Length: 36
<
{"href":"","id":1,"name":"Akadama"}

デザインには,NotFound もレスポンスに定義されていますが,これは,コントローラに紐付いた NotFound() を呼んでやればいいです. データが存在しない場合などに,これを呼んで関数を抜ければいいです.

// NotFound sends a HTTP response with status code 404.
func (ctx *ShowBottleContext) NotFound() error {
    ctx.ResponseData.WriteHeader(404)
    return nil
}

ちなみに,コントローラーの返値を何か適当な Error 型のエラーで返すと,goa 的には InternalError として扱います.

// Show runs the show action.
func (c *BottleController) Show(ctx *app.ShowBottleContext) error {
        return fmt.Errorf("よく分からない不具合")
}

InternalServerErrorなレスポンス:

$ curl -v -XGET localhost:8080/bottles/1
Note: Unnecessary use of -X or --request, GET is already inferred.
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET /bottles/1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.49.1
> Accept: */*
>
< HTTP/1.1 500 Internal Server Error
< Content-Type: text/plain
< Date: Tue, 11 Oct 2016 10:05:33 GMT
< Content-Length: 33
<
"よく分からない不具合"
* Connection #0 to host localhost left intact

このようにエラーが漏れてしまっても,InternalServerError になるので問題はないのですが,よりお行儀よく処理するには, goa.ErrorResponse に詰め直して返してやるのがいいでしょう.

// Show runs the show action.
func (c *BottleController) Show(ctx *app.ShowBottleContext) error {
        return &goa.ErrorResponse{
                ID:     middleware.ContextRequestID(ctx),  // ← これは何か適当な値でいい
                Code:   "500",
                Status: 500,
                Detail: fmt.Errorf("よく分からないエラー").Error(), // ← エラーの詳細をここに入れておく
        }
}

レスポンス

$ curl -v -XGET localhost:8080/bottles/1
Note: Unnecessary use of -X or --request, GET is already inferred.
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET /bottles/1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.49.1
> Accept: */*
>
< HTTP/1.1 500 Internal Server Error
< Content-Type: application/vnd.goa.error
< Date: Tue, 11 Oct 2016 10:27:17 GMT
< Content-Length: 90
<
{"id":"ENNzomNWkA-1","code":"500","status":500,"detail":"よく分からないエラー"}
* Connection #0 to host localhost left intact

ID に設定した middleware.ContextRwquestID(ctx) は,main.go でミドルウエアとして middleware.RequestID が設定されていれば(特に設定してなければデフォルトで設定されるはず.下記参照)使えます.設定されてなければ何か適当に unique な値を指定してやればよさそうです.

func main() {
        // Create service
        service := goa.New("cellar")

        // Mount middleware
        service.Use(middleware.RequestID())             // ← これ
        service.Use(middleware.LogRequest(true))
        service.Use(middleware.ErrorHandler(service, true))
        service.Use(middleware.Recover())

        // Mount "bottle" controller
        c := NewBottleController(service)
        app.MountBottleController(service, c)

        // Start service
        if err := service.ListenAndServe(":8080"); err != nil {
                service.LogError("startup", "err", err)
        }
}

もっと簡単に単純な JSON を返す構造体を定義して Error interface を満たすようにしてやってそれを返すのでもいいかもしれません.

アプリケーションで設定を引き回す

さて,コントローラで DB を引きたい場合など,アプリケーションで設定を引き回したい場合にはどうしたらいいでしょうか? といっても,どうするのが正解なのかよくわからんですが,いまのところ

  • ミドルウエアを自分で用意してコンテキストで引き渡す方法
  • コントローラーに引き渡したい要素を追加して,main 関数で,コントローラーをマウントする際に一緒に設定する方法

の2つがあるかなと思ってます.

ミドルウエアを用意してコンテキストで引き渡す

コンテキストに context.Context が埋め込まれてるので,こいつに渡すためのミドルウエアを用意しておいて毎回そこでコンテキストに渡してやる.

// ShowBottleContext provides the bottle show action context.
type ShowBottleContext struct {
        context.Context                // ← ここに入れる
        *goa.ResponseData
        *goa.RequestData
        BottleID int
        Payload  *BottlePayload
}

擬似コードですが,ミドルウエアはこんな感じになると思います.

package mymiddleware

func Database(db *DB) goa.Middleware {
        return func(h goa.Handler) goa.Handler {
                return func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
                        ctx = context.WithValue(ctx, "DB", db)   // ← コンテキストに"DB"という文字列をキーにして埋め込む
                        return h(ctx, rw, req)
                }
        }
}

で,main.go でつくったミドルウエアをマウントします.

snip...
        service.Use(middleware.RequestID())
        service.Use(middleware.LogRequest(true))
        service.Use(middleware.ErrorHandler(service, true))
        service.Use(middleware.Recover())
        service.Use(mymiddleware.Database(db))   // ← こんな感じ
snip...

利用するときは,コンテキストから取り出して利用します.

コントローラに紐づける

コンテキストでなくて,コントローラに DB を紐づけておく方法もあります.goa-cellar のサンプルはこんな感じのコードが書いてあります. 先ほどの例ですと,次のようなコードがコントローラとして生成されるわけですが,

// BottleController implements the bottle resource.
type BottleController struct {
        *goa.Controller
}

// NewBottleController creates a bottle controller.
func NewBottleController(service *goa.Service) *BottleController {
        return &BottleController{Controller: service.NewController("BottleController")}
}

これを,次のように DB がとれるように修正します.

// BottleController implements the bottle resource.
type BottleController struct {
        *goa.Controller
        db *DB                    // ← ここ追加
}

// NewBottleController creates a bottle controller.
func NewBottleController(service *goa.Service, db *DB) *BottleController {
        return &BottleController{Controller: service.NewController("BottleController"), DB: db}   // ← 引数でもらってセットするように
}

で,main.go でコントローラをマウントするときに DB もセットするように修正.

        // Mount "bottle" controller
        c := NewBottleController(service, db)    // ← db を引数に追加
        app.MountBottleController(service, c)

で,利用するときは,コントローラから利用します. 修正箇所はコントローラの分だけ必要になるので,ちょっとめんどくさい感じもしますが,コンテキストから取り出す手間がないので,こっちの方が利用はしやすいかもしれません.

コンテキストに埋め込む方法でも,コントローラに渡しておく方法でも,渡しておくモノの性質や使いやすさなどを考慮して選んでもらえればなと思います. また,これ以外の方法で良いやり方があったら教えて欲しいです.

形態素解析器 kagome を Google App Engine で動かす

概要

kagome を GAE で動かしたいってのをチラホラ耳にしてたのですが,先日ついに issue が投げられたので対応してみました. 正確には UniDic をあきらめて IPADic だけ対応してる kagome.ipadic で動かせることを確認しました.

何が問題なのか?

golang のプログラムを GAE で動かすには以下の2点の制限があります.

  • GAE では unsafe, syscall, cgo を使ってると動かせない
  • GAE では1ファイル32MBのサイズ制限がある

kagome を最初に作ったときは辞書を有限状態トランスデューサー(fst)で構築してて,どうしてもこいつが unsafe 使う実装になってたので対応あきらめてたんですが,なんかの時に辞書を double array trie になおして,辞書からは殆ど unsafe をなくしていたので,今回は1カ所直しただけでいけました. unsafe 使わなくてもパフォーマンスもそれほど変わるわけではないみたい・・・なのは意外でした(もちろん使った方が速いけど

これで対応できたかなと思ったんですけど,GAE には1ファイル32MBのサイズ制限があってそもそもGAEにデプロイ出来ないと教えてもらい (kaneshinさんありがとうございます),kagome から UniDic を削除した kagome.ipadic の方で対応する方針に切り替えました.

kagome.ipadic は blevesearch で「辞書重すぎで go get できんぞな」という声があって用意してたやつなんですけど,こちらに kagome の差分をバックポートして unsafe を取り除く対応しました.

github.com

ではGAE で動かしてみよう

アカウント作成

そもそも GAE の環境がなかったので,アカウント作るところからはじめました. 登録にクレカが必要ですが,試用期間があるのと試用期間後に勝手に課金しないというのを信じて登録.

cloud.google.com

SDK のダウンロード/設定

GAE/Go 用の SDK をダウンロードして設定します.

Download the App Engine SDK for Go  |  App Engine standard environment for Go  |  Google Cloud Platform

★ 注: macOS serria だと SDK がうまく動きません.VM 用意するか,Docker でやるかする必要があります.

qiita.com

プログラム作成

色々やり方あると思いますが,手っ取り早く $GOPATH/src 以下に myapp というフォルダ作って,そこに app.yaml と hello/hello.go を設定します. これはサンプルで用意されてるプログラムで kagome を呼ぶようにしただけの動作確認用のものです.

application: kagome-test
version: 1
runtime: go
api_version: go1
handlers:
- url: /.*
  script: _go_app
package hello

import (
        "fmt"
        "net/http"
        "strings"

        "github.com/ikawaha/kagome.ipadic/tokenizer"
)

var dic tokenizer.Dic

func init() {
        http.HandleFunc("/", handler)
        dic = tokenizer.SysDic()
}

func handler(w http.ResponseWriter, r *http.Request) {
        t := tokenizer.NewWithDic(dic)
        tokens := t.Tokenize("寿司が食べたい。")
        for _, token := range tokens {
                if token.Class == tokenizer.DUMMY {
                        fmt.Fprintf(w, "%s\n", token.Surface)
                        continue
                }
                features := strings.Join(token.Features(), ",")
                fmt.Fprintf(w, "%s\t%v\n", token.Surface, features)
        }
        fmt.Fprint(w, "Hello, world!")
}

まずはローカルで動かす

$ goapp serve myapp

localhost:8080 にアクセスすると動作を確認できるはずです.

デプロイ

GAE でデプロイするために先にプロジェクトを作成して,プロジェクトIDをひかえおいて,デプロイコマンドに渡してやります.

$ goapp deploy -application <プロジェクトID> myapp

ところがどっこい動かない

デプロイしたサービスにアクセスすると....

原因はインスタンスが非力すぎること.

qiita.com

application: kagome-test
version: 1
runtime: go
api_version: go1
instance_class: B8
basic_scaling:
  max_instances: 1
  idle_timeout: 1m
handlers:
- url: /.*
  script: _go_app

とりあえず B8 なら動く,動くぞ.(B4 あれば動きました

GAE で動かすモチベーションって何なの?

今回 GCPUG の slack で kagome を GAE で動かすためのアドバイスいただいたんですが,何人かの人が過去に kagome を GAE で動かしてみようとしてくれたことを教えてくれました.そのモチベーションは何か?GAE で形態素解析するといい感じなのか?と色々想像したんですが,一番のモチベーションは「動かせるか試してみたかった」のようでした.そうですよねー.わかりますorz

ということで,長いこと引っかかってた kagome を GAE で動かすアチーブメントを開放できました :p

goa tips : swagger-ui を使って手っ取り早く API を試す

はじめに

折角 API を作ったら,簡単に試して,仕様も俯瞰的に確認したいものです. そんなわけで,今回は開発環境で使える swagger-ui の tips です.

swagger-ui は swagger ドキュメントを閲覧するためのサービスを提供してくれます. しかも API コンソールがついているので,ドキュメントを確認しながらその場で API を試すことが出来ます.

github.com

これをサービスとして立ち上げて,goa で生成した swagger ドキュメントをセットするというのもまどろっこしいので, goa で生成したサービスを立ち上げると,swagger-ui も一緒にサービスするようにしてしまおう.というのが今回の目標です.

swagger-ui を配置

swagger-ui の distフォルダをコピーしてきます. 以下の説明では,作業ディレクトリの swaggerui/dist にコピーしてきたと仮定します.

デザインに swagger-ui のサービスを追加

swagger-ui を使うにはデザインに以下を追加します.

var _ = Resource("swagger", func() {
        Origin("*", func() {
                Methods("GET") // Allow all origins to retrieve the Swagger JSON (CORS)
        })
        Files("/swagger.json", "swagger/swagger.json")
        Files("/swaggerui/*filepath", "swaggerui/dist")
})

開発環境でのサービスと割り切って,CORS 対策は何もしません. とってきた dist フォルダ以下のファイルをサービスするように指定しています.

実行

これで swagger-ui を使う準備は整ったので,goagen して, プロジェクトのトップで,下記のようにしてサービスを立ち上げます.

$ go run *.go

ブラウザで,http://localhost:8080/swaggerui/ にアクセスします.

f:id:ikawaha:20160921164612p:plain

これでお手軽に API を試すことが出来ます.

また,http://localhost:8080/swagger.json にアクセスすると,swagger.json の生データが得られます.

この方法のいけてないところ / はまりどころ

  • API の一覧の中に swagger-ui をサービスしている endpoint が表示される
    • なんかちょっともんにょりする
  • 持ってきた swagger-ui/dist を swagger フォルダ以下に配置する
    • goagen するごとに swagger フォルダが消されるので配置したはずのファイルが消されて「あれ?」っとなる

とはいえ,そんなに問題になることでもない.

どうしても本番でも使いたいときは,認証入れたり,CORS を適切に設定したりして下さい.

goa tips : Type と MediaType を使い分けよう

はじめに

MediaType と Type の意味を理解してを適切に使い分けましょう(自戒.

よく間違えます.github の issue とか goa の slack channel にもよく質問が上がってます.

MediaType はレスポンスの形式

MediaType は Type としても利用できますが,本来の意味的にはレスポンスデータです.

Payload の要素に MediaType を利用することは可能ですが,MediaType で指定したいと思っているデータ形式と同等のものを Type で定義してそれで指定するのが無難でしょう.Payload の要素に MediaType を使って,これに Example を適用するとうまく設定できないケースがあるため(バグ?)その観点からも避けた方がよさそうです.

また,MediaType の定義には,MediaType を利用することは出来ません.

要するに,言いたいことは,MediaType はレスポンスデータにだけ使おうということです.

タイプ 説明
MediaType レスポンスデータを表す.MediaType を要素として含むことは出来ない
Type 型を表す.MediaType を要素として含むことが出来るがおすすめできない

かなり個人的な意見が入っているので,もっといい方法があるよ!というご意見がありましたらお教えください.

CollectionOf と ArrayOf の使い分け

CollectionOfArrayOf という配列を作る関数があります. 似たような関数で,よく使い方を混乱してしまうので MediaType と Type の使い分けと一緒に覚えておくと覚えやすいです.

関数 引数に出来る型 返値の型 説明
CollectionOf MediaType MediaType あるレスポンスデータがあるとき,そのレスポンスデータのリストを返すアクションで使うと便利.返値は MediaType
ArrayOf Type または MediaType Type Type でも MediaType でも受け付けられるが,返値は Type になる

goa tips : Attribute と Param と Member は同じもの

はじめに

API デザインの書き方を一通り説明したので,コントローラーの実装の説明する前に, goa の tips をいくつか取り上げたいと思います. という訳で,AttributeParamMember も,すべて Attributeエイリアス関数で同じものなんです.というお話.

使い分け

AttributeParamMember 同じものといっても,使う場面が切り分けられています. ちゃんと使うべきところで使うと読みやすいデザインになるようになっているはずです(たぶん).

使う場面 説明
Attribute MediaType定義の Attributes のなか レスポンスデータの要素の定義として
Param Resource定義の Action の Params のなか endpoint にアクセスする際のパスパラメータやフォームパラメータの要素の定義として
Member Type定義のなか Payloadやユーザー定義の新しい型の要素の定義として

でもまぁ,結局同じなんでどれで書いても目的は達成できます.

基本的な使い方

これらが同じものだと分かっていれば,使い方も覚えやすいです.

Param(<名前>, <型>, <説明>, func() {
        // バリデーション
        // デフォルト値の設定 (これは Payload のときのみ有効)
        // 使用例
})

具体的に書くとこんな感じ.

Param("bottleID", Integer, "Bottle ID", func() {
        Minimum(0)
        Maximum(127)
        Default(1)

        Example(3)
})

バリデーションが不要な場合はこう.

Param("bottleID", Integer, "Bottle ID")

説明が不要ならこう.

Param("bottleID", Integer)

実はもっと短く書けて,

Param("comment")

なんてのもできます.ただし,このとき,"comment" の型は String に指定されます.

バリデーション

前にも説明しましたけど,再掲しておきます.

バリデーション DSLの例 説明
最大/最小 Maximum(100) / Minimum(0) 数値 (Integer, Number) で利用できる
最大長/最小長 MaxLength(10) / MinLength(1) 文字列,配列で利用できる.文字列は rune 文字長でカウントされる
パターン Pattern(“^foo”) 文字列に対して正規表現を利用できる
列挙 Enum(3, 5, 7) / Emum(“male”, “female”) 数値や文字列に対して列挙したものだけを受け付けるようにできる
定型 Format(“date-time”) / Format(“email”) / Format(“ipv4”) / Format(“mac”) goa で定型フォームとして用意されているものがいくつかあります

型はどのようなものが指定可能か?

goa の基本型には以下のようなものがあります.

goaの基本型 golangでの表現 JSONでの表現
Integer int number
Number float number
String string string
Boolean bool boolean
DateTime time.Time RFC3339な文字列
UUID uuid.UUID RFC4122な文字列
Any interface{}

型に以下の関数を適用したものも型として利用できます.例えば,IntegerArrayOf を適用した ArrayOf(Integer)[1,2,3,4,5] のような JSON に対応します. また,Type 関数で定義したものも型として利用できます.というか,Type はユーザー定義の型を作る関数です. ややこしいですが,MediaType で定義したものも型として利用できます.ただし,Type 関数で定義する新しい型のなかに MediaType を入れてはいけません. Type で定義した型を利用して MediaType を構築するのは ok です.

関数 説明
ArrayOf(…) ArrayOf(Integer), ArrayOf(Bottle) 配列表現にした Type を返す.ex. [1, 2, 3, 4]
HashOf(…) HashOf(String, Integer) ハッシュマップ.JSON オブジェクトに相当.ex. {"value" : 123}
Type(…) 参照:Type定義 Type関数で新しく定義された型
MediaType(…) 参照:MediaType定義 MediaType関数で定義されたもの.MediaType も Type の特殊なものとして利用できる.ただし制限がある(上記参照)
CollectionOf(…) ArrayOf(BottleMedia) 配列表現にした MediaType を返す

Default について

Default はパラメータにデフォルト値を設定できますが,これは,Payload のパラメータにしか効きません. Make it possible to provide default values for query string and header params · Issue #658 · goadesign/goa · GitHub

改良されてフォームパラメータに対しても有効になりました 🙌 .

Assign to default param values. by brycereitano · Pull Request #1024 · goadesign/goa · GitHub

Example の書き方

Example は基本型に対しては値を書くだけで ok です.Example を設定しなくても,ドキュメントにはランダムな Example が設定されます. 大変親切なのですが,これが割とジャマです(^^ゞ.なので,なるべく Example で指定するか,明示的に NoExample 関数を指定してサンプルを作らないようにするかした方がいいです(個人の趣味的には).

例:レスポンスの MediaType の Example がランダムに作られた例 (図は swagger-ui で見た場合)

f:id:ikawaha:20160915181740p:plain

Type で組み上げた型について Example を適用するには,golang での型を組み合わせて値をセットしていきます. たとえば,HashOf(String, Integer) でワインの銘柄と年を Payload として与えるとします. HashOf(String, Integer)に対しては golangmap[string]int が対応するので,この型の例としては以下のように map で与えてやります.

var BottlePayload = Type("BottlePayload", func() {
        Member("bottles", HashOf(String, Integer), func() {
                Example(map[string]int{
                        "POUILLY FUISSE": 2014,
                })
        })
})

swagger-ui で見たとき(下図)で,Payload のサンプルにワインの銘柄(“POUILLY FUISSE”)と年(2014) がセットされているのが分かると思います.

f:id:ikawaha:20160915182447p:plain

このように,swagger-ui などを通じて API を叩いたりするときに分かりやすいので,Example は適切にセットしておくことをオススメします.

goa の API デザインの書き方 後編 (Resource と Payload)

概要

goa の API デザインについて,デザインを定義する4つの要素の概要説明の後半です.

  • APIAPI サーバの定義
  • ✓ MediaType … レスポンスデータの定義
  • Resource … APIが管理するデータへのアクセス方法 / エンドポイントなどを定義
  • Payload … API に送信するデータの定義

今回は残りの Resource と Payload の説明です.


準備:API サンプル

おなじみの最小構成サンプル.全体の把握のために貼っておきます.

package design                                     // The convention consists of naming the design
                                                   // package "design"
import (
        . "github.com/goadesign/goa/design"        // Use . imports to enable the DSL
        . "github.com/goadesign/goa/design/apidsl"
)

var _ = API("cellar", func() {                     // API defines the microservice endpoint and
        Title("The virtual wine cellar")           // other global properties. There should be one
        Description("A simple goa service")        // and exactly one API definition appearing in
        Scheme("http")                             // the design.
        Host("localhost:8080")
})

var _ = Resource("bottle", func() {                // Resources group related API endpoints
        BasePath("/bottles")                       // together. They map to REST resources for REST
        DefaultMedia(BottleMedia)                  // services.

        Action("show", func() {                    // Actions define a single API endpoint together
                Description("Get bottle by id")    // with its path, parameters (both path
                Routing(GET("/:bottleID"))         // parameters and querystring values) and payload
                Params(func() {                    // (shape of the request body).
                        Param("bottleID", Integer, "Bottle ID")
                })
                Response(OK)                       // Responses define the shape and status code
                Response(NotFound)                 // of HTTP responses.
        })
})

// BottleMedia defines the media type used to render bottles.
var BottleMedia = MediaType("application/vnd.goa.example.bottle+json", func() {
        Description("A bottle of wine")
        Attributes(func() {                         // Attributes define the media type shape.
                Attribute("id", Integer, "Unique bottle ID")
                Attribute("href", String, "API href for making requests on the bottle")
                Attribute("name", String, "Name of wine")
                Required("id", "href", "name")
        })
        View("default", func() {                    // View defines a rendering of the media type.
                Attribute("id")                     // Media types may have multiple views and must
                Attribute("href")                   // have a "default" view.
                Attribute("name")
        })
})

では本題です.

Resource定義

func Resource

リソースは Web API の操作の対象になるもの,操作の方法を定義していきます.

var _ = Resource("bottle", func() {                // Resources group related API endpoints
        BasePath("/bottles")                       // together. They map to REST resources for REST
        DefaultMedia(BottleMedia)                  // services.

        Action("show", func() {                    // Actions define a single API endpoint together
                Description("Get bottle by id")    // with its path, parameters (both path
                Routing(GET("/:bottleID"))         // parameters and querystring values) and payload
                Params(func() {                    // (shape of the request body).
                        Param("bottleID", Integer, "Bottle ID")
                })
                Response(OK)                       // Responses define the shape and status code
                Response(NotFound)                 // of HTTP responses.
        })
})

リソースには名前をつける必要があります.上記の例ではリソースを "bottle" と命名しています.

  • BasePath 関数はこのリソースにアクセスするための endpoint のパスの prefix 部分を定義できます.これは後で出てくる Action 関数の説明で一緒に説明します.
  • DefaultMedia 関数はこのリソースへの操作のレスポンスのデフォルトのメディアタイプを指定できます.

リソースの中心となるのは,Action です.Action はリソースに対して,複数定義することが出来ます.直感的には,API の endpoint とレスポンスの定義を行うものだと考えればいいでしょう.Action につける名前は,"add" とか,"create"とか,"list" とか "show" などリソースに対する操作を指定するものをつけることが多いようです.

ルーティングの指定

Routing は,API の endpoint を指定します.GET や POST,PUT,PATCH,DELETE などのHTTPメソッドと endpoint のアクセスパスを設定します.アクセスパスにはパスパラメータを含めることが出来ます.

Routing(GET("/:bottleID")) 

なら,endpoint /{bottleID} に GET メソッドでアクセスできることを指定しています. パラメータ bottleID については,後述のParams 関数の中でで受け付ける型などを指定します.

もし,Resource ですでに BasePath が設定されていれば,BasePath 関数で指定されているパスが prefix につきます. 上記の例では,BasePath("/bottles") で,Routing("/:bottleID") なので,endpoint は /bottles/{bottleID} となります.

Routing は要素を複数取れるので,

Routing(POST("/:id"), PATHC("/:id"))

のように POST でも PATCH メソッドでもアクセスできる endpoint を指定することも出来ます.

パラメータの指定

Params 関数は endpoint が受け付けるパラメータを列挙して定義します. Routing(/:bottleID) のようにパスパラメータで利用されたパラメータもここで定義します.

Routing(GET("/:bottleID")) 
Params(func() {                   
        Param("bottleID", Integer, "Bottle ID")
})

上の例では,パスパラメータで利用している bottleID は,Integer 型で,"Bottle ID"という説明 (これはドキュメントに記載される)が指定されています.

もし,パスパラメータとは別にクエリパラメータを指定したければ,他で利用しているパラメータと被らない名前をつけて,同じように定義を足してやればいいです.先ほどの例にワインのカテゴリ "category" を追加して "red" とか "white" とか "rose" みたいな文字列でワインの種類を指定させる例を下記に示します.

Routing(GET("/:bottleID")) 
Params(func() {                   
        Param("bottleID", Integer, "Bottle ID")
        Param("category", String, "Category")
})

category はクエリパラメータになるので,

curl -XGET localhost:8080/bottles/1?category=red

のように指定できます."category" を必須指定にしたければ Required("category")Params 関数の中で指定して下さい.(パスパラメータは何もしなくても必須要素になります)

パラメータにバリデーションを設定する

たとえば,bottleID は 0から127までの整数,category は "red" / "white" / "rose" のいずれかだったとしましょう. そのときに,これ以外の値がパラメータに入らないようにするには次のようにパラメータにバリデーションを追加できます.

Routing(GET("/:bottleID")) 
Params(func() {                   
        Param("bottleID", Integer, "Bottle ID", func() {
                Minimum(0)
                Maximum(127)
        })
        Param("category", String, "Category", func(){
                Enum("red", "white", "rose")
        })
})
バリデーション DSLの例 説明
最大/最小 Maximum(100) / Minimum(0) 数値 (Integer, Number) で利用できる
最大長/最小長 MaxLength(10) / MinLength(1) 文字列,配列で利用できる.文字列は rune 文字長でカウントされる
パターン Pattern("^foo") 正規表現を利用できる
列挙 Enum(3, 5, 7) / Emum("male", "female") 数値や文字列に対して列挙したものだけを受け付けるように出来る
定型 Format("date-time") / Format("email") / Format("ipv4") / Format("mac") goa で定型フォームとして用意されているものがいくつかあります

Payloadの設定方法

func Type

endpoint で受け付けるデータの形式を定義します.Payload を定義するには Type 関数を利用します. Params の設定方法と似たような形式なので,実際見てもらった方が理解しやすいかと思います.

BottlePayload = Type("BottlePayload", func() {
        Member("bottleID", Integer, "Bottle ID", func(){
                Minimum(0)
                Maximum(127)
        })
        Member("category", String, "Category", func(){
                Enum("red", "whilte", "rose")
                Default("red")
        })
        Member("comment", String, "Comment", func(){
                MaxLength(256)
        })
        Required("bottleID", "category")
})

このように,要素とバリデーションを設定することが出来ます.これは,下記のようなJSON データに対応します.

{
        "bottleID" : 3,
        "category" : "red",
        "comment" : "tasty!"
}

または,commentRequired ではないので,

{
        "bottleID" : 3,
        "category" : "red",
}

でも受け付けます.

Payload の Member には Default関数が設定できます.Default を指定しておくと,その要素が省略されているときに,指定したデフォルト値がセットされます.デフォルト値が設定されている要素は Required な要素でも送信時の JSONデータで省略できるので扱いが簡単になります.Payload の要素が本当に null をとる必要があるとき以外で,デフォルト値が設定できるならば,なるべく Default を設定して Required にしておくといいでしょう.

定義した Payload は ResourceAction 関数に設定して利用します.

var _ = Resource("bottle", func() {
        BasePath("/bottles")              
        DefaultMedia(BottleMedia)   

        Action("show", func() {          
                Description("Get bottle by id")
                Routing(GET("/:bottleID"))       
                Params(func() {                    
                        Param("bottleID", Integer, "Bottle ID")
                })
                Payload(BottlePayload) // ← ここに設定できる!
                Response(OK)                       
                Response(NotFound)            
        })
})

Payload は JSON である前提で話を進め来ましたが,実は,goa はデフォルトでは JSON でも XML でも Payload を受け付けます. たぶんしないと思うけど,Payload の形式を JSON のみに変更したりしたい場合は API 関数の Consumes 関数で設定することも出来ます :)

コラム:パラメータが必須要素であるときとそうでないときの扱いの違い

Payload のパラメータが必須である場合 (Required で指定されている場合)とそうでない場合で生成されるコードが変わってくるのですが,どのような違いがあるか知っておくと後々のためにも知っておいて損はありません.端的に言うと,Required されない要素は,生成されるコードでは,要素に対応する型の ポインタ型 で表現されます.

例えば,次のような Payload を考えます.

MyPayload = Type("MyPayload", func() {
        Member("reqired_member", String, "Required!")
        Member("optional_member", String, "Optional")
        Required("required_member")
})

上で定義している MyPayload の required_member は必須要素ですが,optional_member はそうではありません.このときこの Payload に対応して生成されるコードは次のようになります.

// MyPayload user type.
type MyPayload struct {
        RequiredMember string 
        OptionalMember *string
}

OptilalMember の方は,JSONnull を表現できるようにポインタになります.

同じようなことが MediaType の定義でも起きます.Payload は設定された値を読み出すだけなのでそんなに問題が起こらないのですが, MediaType はレスポンスデータなので,何かしらの値を設定してやる必要があります.そうすると,必須要素でない文字列型のパラメータがあったりすると, 文字列のポインタを値として設定したりしなければならなくなります.例としてちょっと上の MyPayload に値をセットしてみましょう.文字列のポインタは

var str := "hello"

p := MyPayload {
        RequiredMember = "aloha"
        OptionalMember = &str
}

としていったん変数に代入してから設定するか,

func toPtr(s string) *string {
    return &s
}

p := MyPayload {
        RequiredMember = "aloha"
        OptionalMember = toPtr("hello")
}

みたいに関数用意したりしないといけません.たいしたことではないんですが,やはり読み出すだけにしてもポインタは扱いが面倒なので,出来るだけ Required にしておいて,省略したいときはまずデフォルト値で対応できるか検討して,それでもやはり null で表現する必要がある要素であれば Required から外す,というのが個人的なオススメです.特に MediaTypeAttribute を指定するときには思い出して下さい.

goa の API デザインの書き方 前編 (API と MediaType)

はじめに

goa の API デザインについて,デザインを定義する4つの要素について概要を説明します.

  • APIAPI サーバの定義
  • MediaType … レスポンスデータの定義
  • Resource … APIが管理するデータへのアクセス方法 / エンドポイントなどを定義
  • Payload … API に送信するデータの定義

とりあえずこれらを押さえておけば一通りのAPIは書けるはず!(たぶん)

今回は4つのうちの API と MediaType を説明します.


準備:API サンプル

おなじみの最小構成サンプル.

デザインのパッケージ名は design.あと,goa のライブラリを dot インポートしてますが,これはそういう流儀なので呪文だと思って許して下さい.以下に出てくる API とか Resource とか MediaType といった関数は,これらのライブラリの中で定義されている関数です.

これらの関数の説明は goa :: Design-first API Generation に説明がありますので,こちらを参照しながら読んでいただけるといいかと思います.

このような関数を組み上げて API のデザインを書いていくスタイルが goa のスタイルになります.

package design                                     // The convention consists of naming the design
                                                   // package "design"
import (
        . "github.com/goadesign/goa/design"        // Use . imports to enable the DSL
        . "github.com/goadesign/goa/design/apidsl"
)

var _ = API("cellar", func() {                     // API defines the microservice endpoint and
        Title("The virtual wine cellar")           // other global properties. There should be one
        Description("A simple goa service")        // and exactly one API definition appearing in
        Scheme("http")                             // the design.
        Host("localhost:8080")
})

var _ = Resource("bottle", func() {                // Resources group related API endpoints
        BasePath("/bottles")                       // together. They map to REST resources for REST
        DefaultMedia(BottleMedia)                  // services.

        Action("show", func() {                    // Actions define a single API endpoint together
                Description("Get bottle by id")    // with its path, parameters (both path
                Routing(GET("/:bottleID"))         // parameters and querystring values) and payload
                Params(func() {                    // (shape of the request body).
                        Param("bottleID", Integer, "Bottle ID")
                })
                Response(OK)                       // Responses define the shape and status code
                Response(NotFound)                 // of HTTP responses.
        })
})

// BottleMedia defines the media type used to render bottles.
var BottleMedia = MediaType("application/vnd.goa.example.bottle+json", func() {
        Description("A bottle of wine")
        Attributes(func() {                         // Attributes define the media type shape.
                Attribute("id", Integer, "Unique bottle ID")
                Attribute("href", String, "API href for making requests on the bottle")
                Attribute("name", String, "Name of wine")
                Required("id", "href", "name")
        })
        View("default", func() {                    // View defines a rendering of the media type.
                Attribute("id")                     // Media types may have multiple views and must
                Attribute("href")                   // have a "default" view.
                Attribute("name")
        })
})

では本題.

API定義

func API

var _ = API("cellar", func() {                     // API defines the microservice endpoint and
        Title("The virtual wine cellar")           // other global properties. There should be one
        Description("A simple goa service")        // and exactly one API definition appearing in
        Scheme("http")                             // the design.
        Host("localhost:8080")
})

API全体の定義です.これは見てもらえば何となく分かると思います.関数API のようにデザインのトップレベルに書くDSL関数のことを,goa ではトップレベAPI DSL と呼んでいます.トップレベAPI は,下記の4つがあります.これらはこれから順に説明していきますが,関数Typeは Payload の説明の時に解説します.

トップレベAPI DSL

関数 API の返値は _ で捨てられています.慣れるまで気持ち悪いですけど,こういうものだと思って下さい.

API で定義されている要素の説明

要素 説明
Title APIのタイトル.ドキュメントなどで表示される
Description このAPIの詳しい説明.ドキュメントなどで表示される
Scheme "http"や"https"などの URL scheme をセットできる
Host サービスするホスト名とポート

この他にも,関数 VersionLicenceDocs なんてのも指定可能です.また,セキュリティの設定が必要な場合はここにセキュリティ設定用の要素が入ることもあります. 詳細はドキュメントを参照して下さい.細かいところはまた別の機会に説明したいと思います.

MediaType定義

→ func MediaType

// BottleMedia defines the media type used to render bottles.
var BottleMedia = MediaType("application/vnd.goa.example.bottle+json", func() {
        Description("A bottle of wine")
        Attributes(func() {                         
                Attribute("id", Integer, "Unique bottle ID")  // id は整数型
                Attribute("href", String, "API href for making requests on the bottle") // href は文字列
                Attribute("name", String, "Name of wine") // name は文字列

                Required("id", "href", "name") // 上記のうちで必須なものをここに指定する
        })
        View("default", func() {                // default View は必須
                Attribute("id")                     
                Attribute("href")                   
                Attribute("name")
        })
})

関数API と違って,関数MediaType の返値は変数に保存されています.これは,この変数を使って,別の場所(定義)でこのメディアタイプを利用できるようにするためです.

メディアタイプは,レスポンスデータの形式を定義します.メディアタイプに名前をつけて(ここでは "application/vnd.goa.example.bottle+json".これは自分で適当に定義します),関数Attributes でレスポンスに含まれるデータをすべて列挙します.この例では,id / href / name が指定されています.また,それぞれに型が定義できます.idIntegerhrefnameString で定義されています.

型として利用できる基本的なものは,Integer / Number / String / Boolean などがあります.Number浮動小数点になります.JSON データと対応してもらうと理解しやすいと思います.また,配列を表すための関数 ArrayOfや,Hash を表すための HashOf があります.例えば,ArrayOf(Integer) とすれば JSON[1,2,3,4]のような配列,HashOf(String, Integer) とすれば JSON{"orange":1, "apple":3} のようなデータ形式が表現できます.

goaの基本型 golangでの表現 JSONでの表現
Integer int number
Number float number
String string string
Boolean bool boolean
DateTime time.Time RFC3339な文字列
UUID uuid.UUID RFC4122な文字列
Any interface{} ---

ここで注意したいのは,関数Attributes で定義したのは,レスポンスデータに現れうるデータ要素であって,実際のレスポンス形式ではないということです. 実際のレスポンス形式は 関数ViewAttributes で定義したデータ要素を組み合わせて作ります. たとえば,上の例ではレスポンスは "default" View で定義されていて,

{id: 1, href: "/bottles/1", "name" : "Bottle #1"}

と返ります.関数Required はデータ要素の値がゼロ値でも,要素として必須かどうかを示しています.たとえば,name を必須要素から外した場合,返却される name の値が空文字列の場合には

{id: 1, href: "/bottles/1"}

のように name が省略されたレスポンスが返されます.ですが,まぁ,省略されちゃうと分かりにくいので,メディアタイプの定義で,Attribute を必須要素から外すことはあんまりないかなという気がします.

やるとしたら,実は同じメディアタイプに対して異なる View をいくつか定義することも可能なので,詳細なレスポンスを返したいときと,省略したレスポンスを返したいときで View をそれぞれ用意しておくという風にする方がいいかもしれません.

説明したように,View はいくつか定義可能ですが,"default" View は必須なので必ず定義して下さい.

余談:メディアタイプはどうやって決めるか?

慣習的に "application/vnd.<リソースの名前>+<形式>" で決めるようです.vnd というのはベンダー定義のメディアタイプであることを示しています.goa は特に何も指定しなければレスポンスは JSON 形式になるので,形式の部分は "+json" が指定されています.この辺の説明は Web API: The Good Parts の 4.4節に分かりやすくまとまっているのを参考にさせてもらいました.Web API: The Good Parts いい本なので是非.

Web API: The Good Parts

Web API: The Good Parts

参考

goa のインストールと実行

概要

goa は DSL で書かれた API デザインを goa のツールで変換してコードを生成します. そのコード生成ツールは goagen です.まずはこれをインストールして,実際に動かしてみるところまで説明します.

生成されるファイルとか,利用する API デザインについてはここでは触れませんが,おいおい説明していきたいと思います.

今回は,goagen のインストールと使い方の概要までです.

インストール

$ go install github.com/goadesign/goa/goagen

でインストールできます.vendoring する場合は vendor フォルダに github.com/goadesign/goa を配置してください. vendor 以下の goagen フォルダまで降りていってビルドしておきます.

$ cd ./vendor/github.com/goadesign/goa/goagen
$ go build
$ cd ../../../../../

実行

基本的な使い方

$ goagen <サブコマンド> -d <デザインの配置してあるパッケージパス>

vendoring してるなら

$ ./vendor/github.com/goadesign/goa/goagen <サブコマンド> -d <デザインの配置してあるパッケージパス>

で実行できます.goagenリポジトリgoagen 以下の gen_* というフォルダがサブコマンドに対応していて,実行するたびにプラグイン的にここの下をビルドして利用しているので,goagen をインストールしたからといってリポジトリを消してしまうとうまく動かなくなってしまうので注意です.

注意

デザインの配置してあるパッケージパスは,$GOPATH/src からの相対パスを指定する必要があります. $GOPATH/src/cellar/design なら,cellar/design を指定します.

(参考) goa/goagen 以下の構造 :

github.com/goadesign/goa/
├── goagen/
│   ├── codegen/
│   ├── gen_app/                ← gen_app は app というサブコマンドに対応している
│   ├── gen_client/
│   ├── gen_js/
│   ├── gen_main/
│   ├── gen_schema/
│   ├── gen_swagger/
│   ├── meta/
│   └── utils/
(snip...)

とりあえず動かしてみよう!

色々説明するより動かしてみるのが手っ取り早いので動かしてみましょう.

準備

自分の $GOPATH/src の下に cellar というフォルダを作ってください. 以下ではこのフォルダで作業することを前提とします.

$ mkdir $GOPATH/src/cellar
$ cd $GOPATH/src/cellar

サンプルのAPIデザインを配置します.天下り的に以下の内容を design/desing.go に保存してください. これは https://goa.design/learn/guide/ でサンプルとして紹介されているワインセラーを題材にしたAPIサーバの最小のサンプルです. /bottles/:bottleIDというエンドポイントに管理しているワインのIDをセットすると,ワインの情報が得られるというようなものです.

package design                                     // The convention consists of naming the design
                                                   // package "design"
import (
        . "github.com/goadesign/goa/design"        // Use . imports to enable the DSL
        . "github.com/goadesign/goa/design/apidsl"
)

var _ = API("cellar", func() {                     // API defines the microservice endpoint and
        Title("The virtual wine cellar")           // other global properties. There should be one
        Description("A simple goa service")        // and exactly one API definition appearing in
        Scheme("http")                             // the design.
        Host("localhost:8080")
})

var _ = Resource("bottle", func() {                // Resources group related API endpoints
        BasePath("/bottles")                       // together. They map to REST resources for REST
        DefaultMedia(BottleMedia)                  // services.

        Action("show", func() {                    // Actions define a single API endpoint together
                Description("Get bottle by id")    // with its path, parameters (both path
                Routing(GET("/:bottleID"))         // parameters and querystring values) and payload
                Params(func() {                    // (shape of the request body).
                        Param("bottleID", Integer, "Bottle ID")
                })
                Response(OK)                       // Responses define the shape and status code
                Response(NotFound)                 // of HTTP responses.
        })
})

// BottleMedia defines the media type used to render bottles.
var BottleMedia = MediaType("application/vnd.goa.example.bottle+json", func() {
        Description("A bottle of wine")
        Attributes(func() {                         // Attributes define the media type shape.
                Attribute("id", Integer, "Unique bottle ID")
                Attribute("href", String, "API href for making requests on the bottle")
                Attribute("name", String, "Name of wine")
                Required("id", "href", "name")
        })
        View("default", func() {                    // View defines a rendering of the media type.
                Attribute("id")                     // Media types may have multiple views and must
                Attribute("href")                   // have a "default" view.
                Attribute("name")
        })
})

コード生成

$ goagen bootstrap -d cellar/design
app
app/contexts.go
app/controllers.go
app/hrefs.go
app/media_types.go
app/user_types.go
app/test
app/test/bottle_testing.go
main.go
bottle.go
tool/cellar-cli
tool/cellar-cli/main.go
tool/cli
tool/cli/commands.go
client
client/client.go
client/bottle.go
client/user_types.go
client/media_types.go
swagger
swagger/swagger.json
swagger/swagger.yaml

これで APIサーバのモック,クライアント,swagger ドキュメントが生成されました.

.
├── app                            
│   ├── contexts.go
│   ├── controllers.go
│   ├── hrefs.go
│   ├── media_types.go
│   ├── test
│   │   └── bottle_testing.go
│   └── user_types.go
├── bottle.go
├── client
│   ├── bottle.go
│   ├── client.go
│   ├── media_types.go
│   └── user_types.go
├── design
│   └── design.go
├── main.go
├── swagger
│   ├── swagger.json
│   └── swagger.yaml
└── tool
    ├── cellar-cli
    │   └── main.go
    └── cli
        └── commands.go

サーバを動かしてみる

$ go run *.go

でサーバが立ち上がります.立ち上がったサーバは何も意味ある動作しませんが,/bottles/:bottleID というエンドポイントが出来ているのでここに curl とかでアクセスするとレスポンスがあります.

$ curl -XGET localhost:8080/bottles/1
HTTP/1.1 200 OK
Content-Type: application/vnd.goa.example.bottle+json
Date: Sun, 11 Sep 2016 04:31:20 GMT
Content-Length: 29

{"href":"","id":0,"name":""}

また,クライアントも同時に出来ているので,クライアントをたたけばレスポンスを得られます.

$ go run tool/cellar-cli/main.go show bottle bottles/1
2016/09/11 13:33:50 [INFO] started id=BMdh+bm3 GET=http://localhost:8080/bottles/1
2016/09/11 13:33:50 [INFO] completed id=BMdh+bm3 status=200 time=3.924862ms
{"href":"","id":0,"name":""}

goagen サブコマンド

指定できるサブコマンドは以下です.bootstrap は初回にだけ実行するサブコマンドで,main,app,client,swaggerエイリアスになってます.この辺のコマンドは Makefile 作っておいて実行するのがいいと思います.ここでは触れませんが,main コマンドで生成されるファイル以外は編集不要なコードです.実際ファイルを見てみると,先頭部分に The content of this file is auto-generated, DO NOT MODIFY と書かれているが分かると思います.

サブコマンド 説明
bootstrap 初回に実施するコマンド (main/app/client/swaggerを実行する)
main APIサーバの main とコントローラーを作業ディレクトリに生成.このコマンドで生成されるファイルは上書きされない (--forceオプションで上書きを強制可能)
app アプリケーションのテンプレートを app ディレクトリ以下に生成
client APIサーバに対応するクライアントのコードを client / tool ディレクトリ以下に生成
swagger APIサーバの使用を swagger 形式で swagger ディレクトリ以下に出力
js JavaScriptAPIサーバクライアントを js ディレクトリ以下に生成
schema APIJSON スキーマschema ディレクトリに生成
gen サードパーティの generator を利用するときに指定する.gorma とか使いたいときに利用

参考