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

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

golang Go言語 goadesign heroku

概要

この記事は 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 は受け付けるので普段はあまり設定しないです)

こうしておくと,データの形式を 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ドキュメントの抑制とパラメータ必須要素について)

golang Go言語 goadesign

概要

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 がサービスできないときのドキュメントどうする問題

Go言語 golang goadesign

概要

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 を実装する

golang Go言語 goadesign

概要

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 で動かす

golang Go言語 形態素解析 GAE

概要

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 を試す

golang Go言語 goadesign

はじめに

折角 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 を使い分けよう

golang Go言語 goadesign

はじめに

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 は同じもの

golang Go言語 goadesign

はじめに

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

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)

golang Go言語 goadesign

概要

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)

golang Go言語 goadesign

はじめに

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 のインストールと実行

golang Go言語 goadesign

概要

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 とか使いたいときに利用

参考

goa をはじめよう

golang Go言語 goadesign

はじめに

goa ってのは golangAPIデザインを書くと,そこから API サーバのモックとかクライアントとかドキュメントとか一通り生成してくれるマイクロサービス用のフレームワークのことです.

とてもすばらしいプロダクトなのですが,goa という名前のググラビリティが非常に悪く,日本語の情報もほとんどありません.

そんなこともあり,すこしでも goa の良さが伝わればと先日『 golang: goa勉強会』を開催しましたが,かなり駆け足になってしまったので,何回かに分けて goa 情報をまとめていきたいと思います.

今回は goa に関する情報のまとめ.

goa 情報源

本家

このサイトの

は少し goa に慣れてからでもいいので読む価値ありです.

Slack

goa の機能に関する議論や,質問などがおこなわれています. 入っておくといいでしょう.

Twitter

goa 公式の twitter アカウント.一度も発言されたことない(^^ゞ

Slide

GopherCon 2016 での発表スライド.

Video

Blog

Go Time FM

Raphael さんのインタビューが聞けます.

Sample Codes

golang: goa勉強会を開催しました

golang Go言語 goadesign

connpass.com

goa って何?

goa ってのは golangAPIデザインを書くと,そこから API サーバのモックとかクライアントとかドキュメントとか一通り生成してくれるマイクロサービス用のフレームワークのことです.

goa は golang のソースとして DSL が書けて,そいつを goa にかけてやることでコードを生成します.ぱっと聞いた感じ,最初に抱く印象は「キモい」だと思うんですが,jsonyaml を書いてコードを生成するよりかなり見通しよくAPIがデザインできると思いますし,なにより,goa の生成するコードがすごく読みやすいコードで,使ってるウチにだんだん「goa (キモ)かわいいな」と思うようになってきました.goa 自体のコードも変態的(コード生成プログラムなのでしょうがない)でありながら読みやすいコードで書かれているのも好印象です.

Web APIgolang で書こうと思えば,極論 net/http でも構築できますが,データの受け取りとかバリデーションとか割と同じようなコードを沢山書くことになったり,ちょっとした仕様変更でドキュメント修正したりするのが結構つらいです.

goa 使うと,データの受け取りとかバリデーションの定型的なプログラムはほぼほぼ生成してくれて,同時にドキュメント(仕様)も swagger 形式で生成してくれます.埋めなければならないのは生成されないビジネスロジック部分だけです.一説にはビジネスロジックはプログラムコード全体の20%にも満たないなんて話もありますから,ビジネスロジックに集中できるのは効率の面でも重要です.

goa の開発をおこなってる raphael さんが,gophercon 用に準備した goa のステッカーを twitter にあげていて,遠目に欲しいなーとイイねしたら,ホントにステッカーを送ってもらえることになりまして

海を越えて送ってもらっちゃいました!「送ってあげるからアドレス教えて」と twitter で DM が来て,あまりの嬉しさで舞い上がってしまってメールアドレス教えちゃったのは秘密です.

沢山いただいてしまったので,この機会に goa の魅力を知ってもらおうとステッカー頒布会 兼 goa 勉強会を開催することになりました.

発表

#1

speakerdeck.com

発表はまず,@ikawaha から goa について,簡単なイントロをまとめました. goa のインストールから API デザインの説明一通り.時間があれば Basic認証,テストの話も・・・と, 当初からだいぶ絞ったので20分で普通に終わるかと思ってたんですけど,かなり駆け足で時間もオーバーしちゃいました. はじめて goa 触る人の助けになれば幸いです.

#2

How I create a Microservice using goa - Slideck

@tchssk さんからは goa をつかって実際のサービスを作る話をしていただきました. gorma という goa で使える準標準的なプラグインがあるんですけど,これをつかうと,DB のモデルとかも API デザインから作ってくれます.ずっと気になってたプラグインだったのでホント俺得でした.機会があれば使ってみたい.また,Docker を使ったテストの話や swagger UI の話もありました.goa は API デザインの swagger定義をドキュメントとして生成してくれるので,swagger UI があると,UI 上から作成した Web API の endpoint をポチポチ叩いて確認出来るので,ホントに便利です.

#3

Development using goa and golang on Pacificporter inc.

@haruyama さんからは,実際の現場での goa や golang の Tips をお話しいただきました. goa のミドルウエア使ってセキュリティヘッダ用意する話とか,go の linter を実際どうやって使ってて,どういうところが使い勝手悪いかなど紹介して下さいました.

#4

speakerdeck.com

最後に @dead_cheat さんから「Go のバイナリ配布関する色々」というお題で LT をして下さいました. アセットをバイナリに埋め込むのに go-bindata はよく使ってたんですが,zgok ってのもあるんですね. 発表後,graceful restart の疑問についていくつか twitter で回答も寄せられていたみたいで,やっぱ発表したりするの大事だなと思いました(小並

感想

初めて勉強会なんてものを主催してみたんですが,これは結構大変ですね.今回,質疑応答の時間の時間を全然盛り込んでなく,懇親会も開催しなかったので,いろいろお話ししてみたい方々が集まって下さったのにお声がけできなかったのが非常に心残りでした.

次回,もし勉強会主催することあれば懇親会までなんとか頑張ってみたいものです.

お集まり下さった皆様ありがとうございました!

goa は pr 送ると,申し訳程度の修正でも "great!" とか "perfect!" とか全力で励ましてくれて骨身にしみてありがたい感じになります. issue の中には初心者向けの issue (help wanted beggineres) なんてものもあるので,これを機会に,使うのはもちろん,コントリビュートも是非!

goa.design

それはたぶんあなたの欲しかった名詞ではない

形態素解析 nlp golang Go言語

概要

形態素解析してテキストの中から名詞っぽいところだけを抜き出したい.ってのはよくある話だと思うのですが,単純にやるといろいろ混じってます.

( '-`).oO( そもそも抜き出してるのは名詞の形態素であって,名詞句じゃないもんな・・・.

名詞を抜き出す kagome の単純なサンプル

package main

import (
        "fmt"

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

func main() {
        t := tokenizer.New()
        tokens := t.Tokenize("寿司が食べたい。")
        for _, token := range tokens {
                if token.Pos() == "名詞" {
                        fmt.Printf("%s\n", token.Surface)
                }
        }
}

output

$ go run main.go
寿司

以上! 簡単ですね

ところがどっこい

これでいろいろ解析してみると分かると思いますが,おもってたのと違う感じの形態素が取れてたりしないでしょうか?

たとえば,こんなのが入ります.

の,1310,1310,5893,名詞,非自立,一般,*,*,*,の,ノ,ノ
美味しそうなのを見つけた
美味し   形容詞,自立,*,*,形容詞・イ段,ガル接続,美味しい,オイシ,オイシ
そう  名詞,接尾,助動詞語幹,*,*,*,そう,ソウ,ソー
な 助動詞,*,*,*,特殊・ダ,体言接続,だ,ナ,ナ
の 名詞,非自立,一般,*,*,*,の,ノ,ノ
を 助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
見つけ   動詞,自立,*,*,一段,連用形,見つける,ミツケ,ミツケ
た 助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
EOS

とか

行ったのは僕だ
行っ  動詞,自立,*,*,五段・カ行促音便,連用タ接続,行く,イッ,イッ
た 助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
の 名詞,非自立,一般,*,*,*,の,ノ,ノ
は 助詞,係助詞,*,*,*,*,は,ハ,ワ
僕 名詞,代名詞,一般,*,*,*,僕,ボク,ボク
だ 助動詞,*,*,*,特殊・ダ,基本形,だ,ダ,ダ
EOS

連体助詞の「の」ってやつですね.なので,単純にやっちゃうといろいろ混じっちゃいます.

名詞にはどんなのがあるの?

名詞にも色々あります.名詞のカラムの後に続いてる品詞情報がそれです.

辞書によって違いますが,IPADic (kagomeのデフォルト辞書) では,名詞はこんな感じに区分されてます.

IPADic の品詞の説明はこちらの仕様書を御覧下さい. http://www.gavo.t.u-tokyo.ac.jp/~mine/japanese/nlp+slp/NAIST-JDIC_manual.pdf

品詞細分類 おおざっぱな説明
七,千,万
一般 キーボード,暖かみ,饅頭
特殊 そう,そ 例に挙げたこの2つだけ
代名詞 彼,あいつ,何,おれ
非自立 かぎり,ため,まんま,以下 連体詞,「の(格助詞)」,活用語の基本形に接続して使われるもの
サ変接続 解析,大破,ピンぼけ 後ろに「する」「できる」「なさる」「くださる」などをつけられるもの
副詞可能 全員,平日,以来,いつか 曜日,月,量,割合などを表す副詞的な用法を持つ名詞
固有名詞 ノーベル,JR四国,電気通信大学,徳島 人名,地名,組織名など
接続詞的 VS,対,兼 例に挙げた3つのみ
引用文字列 いわく 例に挙げた1つのみ
動詞非自立的 ご覧,ちょ,御覧,頂戴,ちょうだい,ごらん 例に挙げた6つのみ
形容動詞語幹 きらびやか,違法,無限 「きらびやか」な,とか「違法」なとか,ナ形容動詞の活用しない部分
ナイ形容詞語幹 味け,だらし,大人気 ナイ形容詞の活用しない部分

まとめ

用途によりますが,詳細品詞まで見ていらないものは省いた方がよさそうですね. あと,UniDic 使うとまた品詞体系がちょっと異なるので注意が必要です.

ほんとは,形態素を適当な単位につなぎ合わせる文節認定の処理を入れて,名詞句を抜き出したいですね.

たとえば,

15:29 $ kagome
形態素解析にかける
形態素   名詞,一般,*,*,*,*,形態素,ケイタイソ,ケイタイソ
解析  名詞,サ変接続,*,*,*,*,解析,カイセキ,カイセキ
に 助詞,格助詞,一般,*,*,*,に,ニ,ニ
かける   動詞,自立,*,*,一段,基本形,かける,カケル,カケル

なら,「形態素解析」を取り出したいです・・・.

とはいえ,大分類で名詞だけ抜き出しておけば,ゴミ混じっていても特に困らない用途が多いのかもですね.

要するに『形態素抜き出したら,データの中身も見てみてね!』というお話でした.

kagomeでNeologdを無理矢理つかう

golang Go言語 形態素解析

概要

サポートしてるわけでもないし,テストしてるわけでもないんだけど,Hackしてくれるひとがいるみたいだからメモ.なにかあったらフィードバックしてくれるとうれしいです.

個人的な考えですけど,今時点でneologdをサポートするのはちょっと躊躇してます.

理由は

  • Neologdに含まれてるエントリーは英アルファベットがいわゆる半角に寄せられてて mecab 用の辞書と統一が取れてない
  • 短めのエントリーが割とあって,精度に影響が出そうな気がする.適当な長さで切った方がよさそうだけど実験出来てないし,よくわからない
  • カテゴリ分けされてないので地名だけ加えるとかできない
  • めっちゃ長いエントリーとか,断片的な年月日とか不要そうなエントリーが結構ある(解析的な悪さはしないかもだけど

といったところです.でも世の中的には使いたい人も結構いるみたいだからやっぱり対応は考えていきたい.

いいかげんな手順

準備

  • kagome を go get する
  • kagome/cmd/_dictool というディレクトリがあるのでそこで以下を作業する
    • mecab の辞書を持ってきて解凍しておく(以下,フォルダ名は mecab-ipadic-2.7.0-20070801 と仮定)
    • neologd のリポジトリを clone する
    • neologd/seed/mecab-user-dict-seed.YYYYMMDD.csv.xz というファイルがあるので解凍しておく

ビルド

$ go run main.go ipa -mecab mecab-ipadic-2.7.0-20070801 -neologd mecab-ipadic-neologd/seed/mecab-user-dict-seed.20160418.csv

フォルダに ipa.dic というファイルが出来ます(手元では140MBくらいでした). コマンドではファイル名が決め打ちになってしまっているので,適当に名前を変えて下さい.

使う

$ kagome -dic ipa.dic

覚え書き

neologd/seed の中にはいくつかファイルがあるけど,user-dict-seed しか想定してない.適当にマージしてひとつのファイルにしておけばツールに食わせられると思う.

Happy Hacking!

追記

ということなので、mecab-ipadic の方にNeologdのパッチを当ててから辞書をビルドするのが良さそう。(未検証)