context.WithTimeout()を使うとタイムアウトが想像以上にきれいに書けた

概要

slack の Real Time Messaging を使って bot を書いてたんですが,しばらくすると websocket が切断されてて bot が落ちてしまってました.今回はこれにタイムアウト処理を追加してリカバリできるようにした防備録です.

問題のコード

func (c *Client) GetMessage() (Message, error) {
    var msg Message
    if err := websocket.JSON.Receive(c.socket, &msg); err != nil { // ← 切断されちゃうとこいつが沈黙する
        return msg, err
    }
    return msg, nil
}

タイムアウト処理を入れる

context.Context にはタイムアウト処理をうまくやってくれる仕組みがあるのでこれを使います.

func (c *Client) GetMessage() (Message, error) {
    ctx, cancel := context.WithTimeout(context.Background(), Timeout)  // ← タイムアウト付きの context を設定
    defer cancel()
    ch := make(chan error, 1)

    var msg Message
    go func() {
        ch <- websocket.JSON.Receive(c.socket, &msg)
    }()

    select {
    case err := <-ch: // websocket からメッセージがとれればここへ来る.関数を抜けるときに cancel() が defer で呼ばれて context が解放される
        return msg, err
    case <-ctx.Done(): // ← websocket が沈黙してても,時間が来たら Done() が close される
        return msg, fmt.Errorf("connection lost timeout")
    }
    return msg, nil
}

これで時間が来たら timeout でエラーが返るようになります.エラーが返ってきたら websocket をつなぎ直せば解決です.

が,これだと websocket が生きててもタイムアウトして再接続しちゃいます.タイムアウトする前に websocket を ping で叩いてみて,反応があったら単に関数を抜けるようにすれば無駄な再接続を防げそうです.

func (c Client) GetMessage() (Message, error) {
    ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
    defer cancel()
    ch := make(chan error, 1)

    go func(ctx context.Context, waiting time.Duration) {
        ctx, cancel := context.WithCancel(ctx)  // ← 親の context から新しい context を作ります
        defer cancel()
        select {
        case <-ctx.Done(): // ← 親の context が close したらここにメッセージが来て何もしないで抜ける
        case <-time.After(waiting): // timeout するちょっと前に ping を打つ
            websocket.JSON.Send(c.socket, &Message{Type: EventTypePing, Time: time.Now().Unix()})
        }
    }(ctx, c.timeout-time.Second)

    var msg Message
    go func() {
        ch <- websocket.JSON.Receive(c.socket, &msg)
    }()

    select {
    case err := <-ch: // ← メッセージが無くても websocket が生きてればタイムアウト前には pong が返ってくるはず
        return msg, err
    case <-ctx.Done():
        return msg, fmt.Errorf("connection lost timeout")
    }
    return msg, nil
}

とりあえずこれで安定して動いているようです.context.Context を使うと複雑な処理になりそうなところが想像以上に見通しのよいコードがかけて幸せです.

( '-`).oO( うまいことできた!と思ってるけど,もっとうまいことやれるような気もしてて,なにか方法があれば教えて欲しいです

f:id:ikawaha:20181018223422p:plain

Happy hacking!

Travis-CI で go の特定バージョンでだけテストカバレジを取りたい

概要

go の 1.11 がリリースされました 🙌.それに伴って,テストカバレッジの指定方法が変更されて,いままでパッケージひとつごとにしかテストカバレッジ取れなかったのが,パッケージをまとめて指定出来るようになりました.

いままでのテストカバレッジ,地味にめんどくさくて,今までは

#!/bin/bash

COV_FILE=coverage.txt
COV_TMP_FILE=coverage_tmp.cov
ERROR=""

echo "mode: count" > $COV_FILE

for pkg in `go list ./...`
do
    touch $COV_TMP_FILE
    go test -covermode=count -coverprofile=$COV_TMP_FILE $pkg || ERROR="Error testing $pkg"
    tail -n +2 $COV_TMP_FILE >> $COV_FILE || (echo "Unable to append coverage for $pkg" && exit 1)
done

rm $COV_TMP_FILE

if [ ! -z "$ERROR" ]
then
    echo "Encountered error, last error was: $ERROR"
    exit 1
fi

GOPATH=$HOME/gopath
GOVERALLS=$(echo $GOPATH | tr ':' '\n' | head -n 1)/bin/goveralls
$GOVERALLS -coverprofile=$COV_FILE -service=travis-ci

こんな感じのスクリプトを置いておいて,これを travis.yaml から呼び出してたんですが,1.11 では,

go test -covermode=count -coverprofile=$COV_TMP_FILE ./...

のようにまとめて指定出来ます.

がしかし,travis-ci では go のいくつかのバージョンをまとめて指定出来るので,前のバージョンもテストしてカバレッジを取るようにしておくと,go のバージョンを見て,どっちの方式でやるか,あるいは指定バージョンでのみカバレッジ取る,みたいな設定が必要になってきます.

やりたいことは

です.至極単純なことなんですが,設定方法をかなり試行錯誤したので,メモ代わりに置いておきます.

Travis-CI での go のバージョン判別法

変数 $TRAVIS_GO_VERSION に納められているのでこれで判別出来ます. travis-ci では 1.11.x のように末尾をワイルドカードに出来るのですが,このとき,$TRAVIS_GO_VERSION に入っているのは 1.11.x です.実際のバージョンが入っている訳ではないので気をつけてください.

どうやるか

結論から言うとこういう感じです.テストが終わったら,after_success でテストカバレッジを取って coveralls に送信してます.

language: go

go:
  - "1.9.x"
  - "1.10.x"
  - "1.11.x"
  - tip
sudo: false
cache: bundler

git:
  depth: 1

before_install:
  - pip install --user codecov
  - go get github.com/axw/gocov/gocov
  - go get github.com/mattn/goveralls
  - go get golang.org/x/tools/cmd/cover

install:

script:
  - go test ./...
  - cd tokenizer; go test -benchmem -bench .; cd ..
  - cd internal/dic; go test -benchmem -bench .; cd ../..

after_success:
  - |
    if [[ $TRAVIS_GO_VERSION = 1.11.x ]]
    then
      go test -covermode=count -coverprofile=coverage.txt ./...
      $GOPATH/bin/goveralls -coverprofile=coverage.txt -service=travis-ci
    else
      echo skip coverage test, $TRAVIS_GO_VERSION
    fi

うまくいかなかった方法

travis-ci で用意されてる condition とか on とか if を利用するのはうまくいかなかったです.こう云うのでうまくやれた方がスッキリしそうなんですが,もしもっといい書き方あるよ!というのをご存じの方がいらっしゃいましたらお教えください.

Happy hacking!

kagomeが go get できない問題

概要

kagome が go get できない!ということがある模様.

たしかに kagome は辞書をソースコードに変換して保持してるので,重いのは重いのですが,いつまでたっても終わらない・・・ということはないはずと思います. 手元でやってみたら2分〜3分,会社の回線で5分くらいでした.回線が細いと厳しいかもしれません. といっても kagome のレポジトリは 400MB 程度.今時の携帯アプリでも大きめのはこれくらいある時代ですし・・・

原因はレポジトリのサイズの他にもあるかもしれません.もし他にもいらっしゃったら教えてください.

申し訳なさ

go は go get でサクッと使えるように便利にできています.なのに kagome がこのユーザー体験を破壊していると思うと申し訳ない限り. kagome はかなり特殊な重たいライブラリなので,kagome の事を嫌いになっても go を嫌いにならないでください!みたいなお気持でいっぱいです.

回避策

1. go get コマンドの最後の ... は意味があります

$ go get -v github.com/ikawaha/kagome/...

基本はこれでいけるはずです.最後の ... がないと失敗するので注意してください.(でも失敗と分かるので落ちてこないというのは多分これではない

2. 直接 git clone する

$GOPATH/src 以下に github.com/ikawaha/kagome として git clone してみてください.このとき depth を 1 にしてください. (go get はデフォルトで depth 1 で clone してくれます)

$GOPATH設定してない場合は ~/go/src 以下に読みかえてください.

$ git clone --depth 1 https://github.com/ikawaha/kagome.git $GOPATH/src/github.com/ikawaha/kagome
Cloning into '/Users/ikawaha/go/src/github.com/ikawaha/kagome'...
remote: Counting objects: 116, done.
remote: Compressing objects: 100% (103/103), done.
remote: Total 116 (delta 21), reused 60 (delta 6), pack-reused 0
Receiving objects: 100% (116/116), 127.47 MiB | 1.61 MiB/s, done.
Resolving deltas: 100% (21/21), done.
Checking out files: 100% (93/93), done.

3. 軽量版 kagome を使う

kagome には IPADic と UniDic の2種類の辞書が入っています.が,ぶっちゃけ普通の人は UniDic は使わないでしょう. 利用方法が同じで IPADic だけを同梱した kagome.ipadic がありますのでこちらを利用してみてください. こちらは GAE でも動作するように調整してありますので,GAE で使いたい場合にもこちらをご利用ください.

$ go get github.com/ikawaha/kagome.ipadic/...

何かいい方法とかあれば教えて欲しいです・・・.

追記

前より遅くなったんじゃないかとの感想にコメントいただきました.なるほどなー

でも全く落ちてこないってのはやはり変.

追記2

github から直接落としても同じだけ時間がかかるという報告いただきました.なので落とせないときは方法の2を試しても,ダメな可能性がありそう. でもまぁ,そのときは github の転送量のせいだと問題が分離できるのでよしとする(ぉ.

追記3

解決したようです.ヤター

会社引っ越し前の新宿ランチ棚卸し

3年ほど,新宿高島屋前のオフィスで働いてきましたが, 今年の3月くらいに会社が引っ越すことになったので,新宿三丁目のランチでお世話になったお店を棚卸し.

ここも行っておいた方がいいよ!ってところがあったら是非教えてください.

( '-`).oO( とか言って次も新宿だったりするかもだけど

思いついたら追加してく.


tabelog.com

アイリッシュパブ.夜は行ったことないんですが,ランチでよくお世話になりました. ランチは950円で,週替わり3種類とカレーから選ぶ.コーヒー付く.


tabelog.com

夜は焼き鳥屋さん.新宿御苑の新宿側の入り口のところ. ランチは唐揚げかチキン南蛮を週替わりでやっていて,親子丼はレギュラーメニュー.970円.


tabelog.com

ベトナム料理のお店で,ランチはフォーのセットがおすすめ.練乳入りでとっても甘いベトナムコーヒーが付いてくるのも楽しみ.


tabelog.com

最近出来たシュラスコのお店.ランチは,一串にチキン,ソーセージ,牛肉2種x2とズッキーニ,ナスをひとかけずつ焼いたものを出してくれる. ローストビーフ丼もおいしい.どちらも1000円.時間があれば食べ放題のコースもあります.


tabelog.com

カレー2種類とナン,ドリンクが付いて950円ぐらいだった気がする.お腹いっぱいで午後は破滅するがカレー欲は満たされる.


tabelog.com

羊っぽいものが食べられる.夜も来たことあるけど,この店安いよね!


tabelog.com

パスタのお店.うまいです.定番のミートソースにするか,ピリ辛のアラビアータにするか,カルボナーラにするか迷う.麺の量をが200gの大盛りまで同じ金額で選べるので200gしか食べたことがない.+100円すれば 250g もいける.むしろ増量して食べようかな,と思うくらいおいしい.


tabelog.com

北海道スープカレー東京ドミニカ.固有表現抽出泣かせの店名.


tabelog.com

生醤油うどん,すだち,ちくわ天トッピングが大体いつものメニュー.大盛りにしてもお値段そのままだった気がする. おいしい.ただ,出汁の感じは関西風で讃岐うどんではない.とおもう.


tabelog.com

ちょっと遠いけどたまに行くイタリアン.お持ち帰りできるドリンクがサービスされてる.


tabelog.com

夜はオイスターバーで,ランチにも牡蠣入りのカレーとか食べられる. でも過去にノロのトラウマがあるので牡蠣は食べたことない. ランチは1000円くらいから.


tabelog.com

お肉が食べたいときに行くとちょっといい感じの肉が食べられる.


tabelog.com

ピザ,ハーフアンドハーフで頼んで,エスプレッソを飲む.しあわせ.

番外

tabelog.com

今はランチやってないけど,ときどき飲みに行くのに使ってた.やさい中心でおじさんの胃袋にも優しい.


tabelog.com

有名なカレー屋さん.らしい.おいしかったと思うけど,記憶が定かでない.何回かしか行ってなかった.


tabelog.com

牛カツのお店.出来た頃に何回か行ったが,いつも行列してるので通り過ぎるとき眺めるくらいになってしまった.


tabelog.com

知らないとたどり着けない感じの奥まった感じの場所にあるお店.なんとなく健康そうなものが食べられる. 夜もおすすめ.


tabelog.com

いつも混んでる.エビつけ麺のお店.


tabelog.com

電源が充実しているカフェ.


tabelog.com

ひとりの時に時々行くラーメン屋.15時にお店にいるとサービスでチャーシューの切れ端がもらえる.これマメな.


tabelog.com

もっと近くにあったら足繁く通うのにな.ちゃんとした(?)中華が食べられる.


tabelog.com

ちょっと変わった豚汁のお店.1回しか行ってない.もっと行きたかった.

形態素解析器 Sudachi の辞書が手に入ったので Go で遊んでみた

はじめに

Sudachi はワークスアプリケーションズが絶賛開発中の形態素解析器で,Java で書かれています.

今一番新しい注目すべき形態素解析器で,次のような機能が特徴としてあげられています.

  • 複数の分割単位の併用
    • 必要に応じて切り替え
    • 形態素解析と固有表現抽出の融合
  • 多数の収録語彙
    • UniDic と NEologd をベースに調整
  • 機能のプラグイン
    • 文字正規化や未知語処理に機能追加が可能
  • 同義語辞書との連携 (後日公開予定)

github.com

開発のステータスは公式には発表されていない(?)ようですが,開発中で本リリースはこれからかな・・・と思います.(実際のところ,スケジュールなどはどうなんでしょうか?)

最近,Sudachi の辞書がレポジトリからダウンロードできるようになったので,kagome の辞書に加工して少し遊んでみました. 以下,公式情報ではない&開発で変更されるかもしれないので,正しいところは元コードを当たってください.

辞書の構造

辞書構造は UniDic とほぼ同じです.

カラム 内容 備考
0 headword 新国立美術館 正規化された見出し語.アルファベットが小文字に統制されてるとか
1 left-id 5962 -1が入ってることがあるが,その場合は辞書項目として無効のようだ
2 right-id 5861
3 cost 21119
4 headword 新国立美術館 こっちの見出し語は正規化されてない見出し語っぽい
5-10 part-of-speech 名詞,固有名詞,一般,,,*
11 reading form シンコクリツビジュツカン
12 normalized form 新国立美術館 動詞だったら食べ -> 食べる みたいな
13 dictionary form word id * normalized form に相当する辞書項目があればその ID が入ってるっぽい
14 分割情報 (A/B/C) C
15 A ユニット分割 2008495/1573531/2528962/2843222 数値は形態素のIDっぽいので,これで分割される形態素を引けばよさそう
16 B ユニット分割 2008495/1573531/2528997
17 word structure 2008495/1573531/2528997 これは何に使うのかな?

見出し語

Neologd からも項目を持ってきてるみたいで,結構おもしろいエントリが入ってます. たとえば,

エターナルフォースブリザード,5144,5144,7882,エターナルフォースブリザード,名詞,固有名詞,一般,*,*,*,エターナルフォースブリザード,エターナルフォースブリザード,*,C,515901/962003/982867,515901/962003/982867,515901/962003/982867

なんてのも入ってます.UniDic では短単位に切れすぎていたので,固有名詞の長い形態素とかがキレイにとれるようになるのかもしれません.Neologd は機械的にエントリを作ってるとのことなので,Sudachi に取り込まれる際にはどのような基準で取り込まれるのか,機械的にやるのか,人目でチェックするのか,などなど気になります.まだこの辺は調整されるのかもしれないので今後の辞書開発に期待です.

あと,遊ぶ上での注意点としては見出し語にユニコードリテラルがコードポイントで表現されていることがあります.

!!\u0028 ; ロ\u0029゚ ゚,5977,5977,5000,!!\u0028 ; ロ\u0029゚ ゚,記号,一般,*,*,*,*,カオモジ,!!\u0028 ; ロ\u0029゚ ゚,*,A,*,*,*

こんなエントリがありますが,\u0028ユニコードリテラルとして解釈してやる必要があります.

分割単位

Sudachi では短い方から A, B, C の3つの分割モードが提供されています. A は UniDic 短単位相当,C は固有表現相当,B は A, C の中間的な単位とのことです.

README.md に掲載されていた例を示します。

A:医薬/品/安全/管理/責任/者
B:医薬品/安全/管理/責任者
C:医薬品安全管理責任者

A:自転/車/安全/整備/士
B:自転車/安全/整備士
C:自転車安全整備士

A:消費/者/安全/調査/委員/会
B:消費者/安全/調査/委員会
C:消費者安全調査委員会

A:新/国立/美術/館
B:新/国立/美術館
C:新国立美術館

辞書では長い(分割単位BとかCの)エントリに分割情報がついているので,表示する際に分割してやれば目的を達成できそうです.

とりあえず動かしてみる

kagome は UniDic を扱えるので,UniDic の辞書だと思って Sudachi を辞書をコンパイルして動かしてみました.

お約束の すもももももももものうち です.

f:id:ikawaha:20171018180742p:plain

Sudachi には すもももももももものうち というずばりそのもののエントリもあるのですが,そちらは選ばれませんでした. Sudachi 自体で動かしてみましたが,同じ解析結果になったので,やり方を外しているわけではないみたいです.

f:id:ikawaha:20171018180638p:plain

TODO

kagome にそのまま取り込んで,分割情報で調整してやれば大体うまく動きそうな感触を得ました. kagome ではやってない Sudachi の以下の機能は別途開発しないとだめかな・・・ という感じです.

  • [ ] 入力テキスト修正
    • 文字列正規化 (全半角、大文字/小文字、異体字)
  • [ ] 長音正規化 (「~」や長音記号連続の正規化.まだリリースされてない?)
  • [ ] 未知語処理
    • 1文字未知語
  • [ ] 単語接続処理 (品詞接続禁制)
  • [ ] 出力解修正
    • カタカナ未知語まとめ上げ
    • 数詞まとめ上げ
  • [ ] 分割粒度調整 (未知語/既知語の分割粒度の平滑化.まだリリースされてない?)
  • [ ] 数詞正規化 (漢数詞や位取りの正規化.まだリリースされてない?)
  • [ ] 人名補正 (敬称や前後関係から人名部を推定.まだリリースされてない?)

Sudachi の辞書使って遊んでみたいと思ってる人の一助になれば幸いです. Happy Hacking!

goa tips: 要素がポインタになるのを避ける

はじめに

Payload の Member が必須要素でない場合,生成される対応するコードはポインタで表現されます. 文字列なら,string のポインタで表されるわけで,ポインタを渡すために変数を用意してポインタで渡してやるみたいなひと手間が必要になってきます. これはややめんどくさいので,要素がポインタになるのを避けよう・・・というものぐさな方針です.

※ もっといい方法があればしりたいです・・・

Payload や MediaType の要素がポインタになるのはどんなときか

Payload や MediaType の要素が(生成されたコードで)ポインタで表現されるのは,要素が必須要素でないときです. たとえば,下記のような Payload では,bottleIDcategory は required ですが,comment は省略可能です.

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") // ← 必須要素の指定
})

生成される対応するコードは,下記のようになります.Comment*string で表現されてます.

// BottlePayload user type.
type BottlePayload struct {
        // Bottle ID
        BottleID int `form:"bottleID" json:"bottleID" xml:"bottleID"`
        // Category
        Category string `form:"category" json:"category" xml:"category"`
        // Comment
        Comment *string `form:"comment,omitempty" json:"comment,omitempty" xml:"comment,omitempty"` // ← こいつがポインタあつかい
}

必須要素にしないときの振る舞いは?

Payload で要素を必須要素にしない場合,その要素は省略できます. つまり,json で渡そうと思ったとき,

{ "bottleID" : 1, "category": "white" }

の様に(省略可能な) comment は書かなくても大丈夫です.必須要素の bottleID を省略すると,goa が生成するバリデーションではねられます.

MediaType の場合は required でなければ,ゼロ値であればレスポンスから項目が省略されます.

必須要素でも記述を省略することは可能

大体問題になるのは,Payload の要素で省略したいものがある場合だと思います. しかし,単に記述を省略したいのでしたら,必須要素にして Default DSL を利用することで,あたかも要素を省略したように記述できます.

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(){
                Default("")                       // ← ★
                MaxLength(256)
        })
        Required("bottleID", "category", "comment") // ← 必須要素の指定
})

こうしておくと,comment の値の初期値は空文字列になります.なので,

{ "bottleID" : 1, "category": "white" }

という Payload を渡しても,初期値が設定されるので,required の要件を満たします(詭弁ぽいけど). なので,初期値を設定できる要素であれば,必須要素にしておいて,Default を指定することで記述は省略可能になります.

ただし,null と空文字列を区別したいような場合にはこの方法は出来ません.

逆に言えば,null と初期値値を区別したいような場合だけ,必須要素でなくす必要がある・・・という方針でとりあえずうまくいっています.

といっても,これは goa でポインタ使いたくないと云うためのものぐさな手法で, API の作法的にどうなのかと常々疑問に思っているので,何か知見がある方いらしたらコメントいただければ嬉しいです.

goa tips : v1.3 で追加された HashOf のバリデーションを使う

はじめに

goa のこと書くのずいぶん久しぶりになってしまいました. 仕事でも使ってて安定して動いていて,あんまり目新しいこともなくなってきて, API はこれで書くのが当たり前になりつつあり・・・.

まぁ,そんな言い訳はどうでもいいんですが, うかうかしているうちに v1.3 がリリースされました! 🙌

goa v1.3.0 · goa :: Design-first API Generation

v1.3 では,いくつか機能が追加されたんですが 密かに期待していた HashOf のバリデーションを設定する機能が追加されたのでこれを紹介しようと思います.

HashOf の機能

HashOf は key と value からハッシュマップをつくる DSL です.

使い方とかはこの辺を参照してください http://ikawaha.hateblo.jp/?page=1473951553

たとえばワインを評価するようなデータが,ワインの銘柄と5段階評価のペアの集合で受け渡されるとしたときに, goa だったら,こんな感じのエンドポイントが用意できます.

Action("updateRatings", func() {
    Payload(func() {
        Member("ratings", HashOf(String, Integer))  // ← ワインの銘柄(文字列)と評価(数字)のペアみたいなのを受け取りたい
})

いままでは,これはデータを受けとった後,コントローラーで入っている銘柄や評価をバリデーションしてました. たとえば,評価が,1〜5 に収まってるかどうか,とか.

でも,これって goa っぽくないですよね :p

v1.3 では,このバリデーションをデザインに書けるようになりました.

Action("updateRatings", func() {
    Payload(func() {
        Member("ratings", HashOf(String, Integer,
            func() { Pattern("[a-zA-Z]+") },  // key のバリデーション
            func() { Enum(1, 2, 3, 4, 5) },    // value のバリデーション
    ))
})

めっちゃスッキリです.

この機能,実は v1.2 の時点で既に ArrayOf に似たような機能が実装されてて,ArrayOf の要素はバリデーション出来るようになってました.HashOf についても同様に出来るようなことがドキュメントに書かれてて,これは使える!とウキウキしてたんですが,実際使ってみると,バリデーションは受けとるんだけど,生成されるコードにバリデーションする部分のコードがなかったのでした orz

そんなわけで,この機能の中身は自分で pr しました.なんとも OSS っぽい体験.

開発もだいぶ落ち着いてる感じのある goa ですが,v2 の開発がかなり進んで,もうだいぶ形になってるみたいです. v2 では gRPC にも対応してくとのことなので,今からリリースが楽しみですねー.

形態素解析器 kagome を Google App Engine の最も安いインスタンスで動かす

概要

前回までのあらすじ: kagome を GAE で動かしたいという話があり,kagome.ipadic という IPA 辞書だけを収めたコンパクト版を用意して,GAE 上で動作させることに成功したわけですが,メモリの消費量が多くて B4インスタンス(メモリ 512MB)以上じゃないと動作しなかったわけです.正直,kagome を GAE で動かしたいという話をチラホラ聞くものの,それは「動くかどうか試してみたい」的なやつで,実際使ってらっしゃるという話は聞こえてこなかったわけです.

ところが・・・

kagome はちと動作に敷居が高いとのご指摘を受け,

不幸にも黒塗りの高級車に追突してしまう後輩をかばいすべての責任を負った三浦に対し,車の主,暴力団員谷岡に言い渡された示談の条件とは…。

参考:

Instance Class Memory Limit CPU Limit
B1 128 MB 600 Mhz
B2 256 MB 1.2 Ghz
B4 512 MB 2.4 Ghz
B4_1G 1024 MB 2.4 Ghz
B8 1024 MB 4.8 Ghz
F1 128 MB 600 Mhz
F2 256 MB 1.2 Ghz
F4 512 MB 2.4 Ghz
F4_1G 1024 MB 2.4 Ghz

何が問題か

メモリ使用量です.Java 実装の gomoku (Igo の派生) では動くとのことなので,ここにヒントがありそうです.

github.com

gomoku の特徴にはこうあります

  • Igo( http://igo.sourceforge.jp/ )から派生した形態素解析
  • 辞書データがあらかじめjarファイルに含まれていることが特徴
  • デフォルトではIPADIC(mecab-ipadic-2.7.0-20070801)を使用している
  • common lispで書かれた辞書構築コマンドを使うことでカスタマイズは可能
  • 現状ではIPADIC以外に対応しようとする場合は(おそらく)ソースコードの修正が必要
  • 素性としては品詞情報のみを保持
  • 原型や読みの情報などの情報は破棄している
  • その他、形態素解析器としての特徴は、おおむねIgoと同様

なるほど.たしかに辞書の付加項目をなくせば使用メモリが減らせそうです.

品詞
品詞細分類1
品詞細分類2
品詞細分類3 ↑ ここまでは保持
活用型    ↓ ここから先は別管理にして必要なときだけロードする
活用形
基本形
読み
発音

削減結果

変更前

実は F2 インスタンスでもぎりぎり動いたらしい.253 MB 必要. f:id:ikawaha:20170602004106p:plain

変更後

97.5 MB まで削減され,B1/F1 インスタンスで動くようになりました 🙌 ヤター

f:id:ikawaha:20170602004321p:plain

実際に動作確認してくださったり報告してくださった Kazzz さん,ありがとうございました!

ぜひ GAE/Go で形態素解析してみてください.Happy Hacking ! github.com

golang で utf8 でない文字列を 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

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