形態素解析器 kagome v2 をリリースした

概要

ホント誰得でもないのは重々承知していますが、思い立って 形態素解析kagome v2 をリリースしました。とはいっても、だいたいの機能は今ある kagome でも実装済みで、今さら変更してもどうよ・・・という感じではあります。

なので、モチベーションを維持するのが非常に難しくて、だらだらと時間だけがかかってしまいました。

折角作ったのでリリースノートです。

TL;DR;

v2 で実現した事

  • 辞書の分離 / バージョン管理
  • 辞書毎に異なる素性項目の扱いの共通化
  • 韓国語辞書対応

辞書の分離

辞書を別リポジトリに分離しました。これにより、長年(?)懸案だった辞書のバージョン管理が可能になりました。go.mod で指定すれば、どのバージョンの辞書を利用しているかがわかります。

また、これにより、これまで kagome.ipadic のような単独辞書を利用するだけのためのライブラリを別に切り出していましたが、必要な辞書だけを import することでこれが実現できるようになりました。形態素解析器を組み込んだアプリのバイナリサイズも最適化されるはずです。

今使える辞書は

の3種類です。GAE で利用したり、WebAssembly で利用したりと容量が少ない方がいい場合は IPADIC だけ import するようにするといいでしょう。IPADIC だけを import した時のだいたいのバイナリサイズは 15 MB くらいと思います。

v1 では、辞書形式を internal で隠蔽していましたが、これを明にしましたので、自分で好きなように辞書を構築する事が出来ると思います。juman の mecab 形式辞書や neologd なども構築できるのではないかと思います。興味ある人はトライしてみてください。

辞書毎に異なる素性項目の扱いの共通化

kagomemecab 形式の辞書を利用して辞書をビルドしていますが、 IPADic も UniDic も同じ mecab 形式ではありますが、素性の形式は少し異なります。

これまで kagome では、素性部分は辞書毎に異なるため、ユーザー任せに素性の配列(文字列の配列)が取れるようになっていましたが、辞書を分離して、それぞれの辞書の情報を package 毎に持たせるようにして、これらの扱いを出来るだけ共通化しました。

辞書組み込みな共通の素性

対象 関数 IPADIC での例
品詞 func (t Token) POS() []string 品詞階層が配列で取れます。e.g. [動詞,非自立可能,*,*]
活用型 func (t Token) InflectionalType() (string, bool) e.g. 五段・カ行促音便
活用形 func (t Token) InflectionalForm() (string, bool) e.g. 連用タ接続
基本形 func (t Token) BaseForm() (string, bool) e.g. 行っ(た) -> 行く
読み func (t Token) Reading() (string, bool) e.g. コウエン
発音 func (t Token) Pronunciation() (string, bool) e.g. コーエン

辞書によって、記載されている項目が異なるので、辞書毎に取れたり取れなかったりします。 また、その項目がセットされていない場合もあります。

また、辞書によっては独自の素性を拡張している場合もあるので、辞書毎に素性の定数が定義されています。 たとえば、UniDic のパッケージには以下のような定数がありますが、これを Token.FeatureAt() に渡せばその素性が取れます。

    // CType represents  活用型 (e.g. 五段-カ行).
    CType = 4
    // CForm represents 活用形 (e.g. 連用形-促音便).
    CForm = 5
    // LForm represents 読み (e.g. コウエン).
    LForm = 6
    // Lemma represents 語彙素 (e.g. 公園, 行く).
    Lemma = 7
    // Orth represents 書字形出現形.
    Orth = 8
    // Pron represents 発音形出現形.
    Pron = 9
    // OrthBase represents 書字形基本型.
    OrthBase = 10
    // PronBase represents 発音形基本型.
    PronBase = 11
    // Goshu represents 語種.
    Goshu = 12
    // IType represents 語頭変化型.
    IType = 13
    // IForm represents 語頭変化形.
    IForm = 14
    // FType represents 語末変化型.
    FType = 15
    // FForm represents 語末変化形.
    FForm = 16

韓国語辞書対応

Korean Mecab に対応しました。

f:id:ikawaha:20200809174857p:plain

v1 から変わらない事

  • 辞書内包
  • 辞書はシングルトン
  • マルチスレッド (goroutine) 対応
  • ユーザー辞書対応
  • kagome コマンド (形態素解析/サーバ/デモ/Lattice表示)

TODO

  • トークンフィルタ
  • 1文が長すぎるときの適当な処理
  • その他の辞書対応

モチベーション

お待ちしています。ご要望も。

Happy hacking!

備忘:heroku で go.mod 環境のアプリをデプロイする

概要

heroku が go.mod 対応のアプリにも対応しているので、もう dep とかいらないや、とか思ってたらハマったのでメモ。

ハマりポイント

  • Go のバージョンをアプリのバージョンと合わせる事
  • install するアプリのターゲットが直下にないときはそれを指定しなければいけない事

解説

Go のバージョンについて

heroku がデフォルトで用いる Go のバージョンは 1.12.17 です。

Go の mod 環境は 1.11 から試験導入されて、1.13 からデフォルトになってます。過渡期の問題として、go.sum の計算方法が修正されたりして(GO 1.11.2, 1.11.4 あたり)多少の混乱があったわけですが、バージョン違いで特に問題になるという認識はありませんでした。ところが、heorku は Go 1.14 で作成した go.sum をうまく解決してくれませんでした。原因となるような現象を探せませんでしたが、ビルドする Go のバージョンをそろえたらうまくいったので多分これが原因だと思います。Go のバージョンをセットするには環境変数$GOVERSION=go.1.14 とすればいいです。実際には heroku 画面の Settings -> Config Vars で設定できます。

f:id:ikawaha:20200809094342p:plain

app.json では env に設定できます。

{
    "name": "kagome",
    "description": "Kagome Japanese Morphological Analyzer",
    "env": {
        "GOVERSION": "go1.14",
        "GO_INSTALL_PACKAGE_SPEC": "./cmd/kagome"
    },
    "buildpacks": [
        {
            "url": "https://github.com/heroku/heroku-buildpack-go.git"
        },
        {
            "url": "https://github.com/weibeld/heroku-buildpack-graphviz.git"
        }
    ],
    "stack": "heroku-18"
}

Install のターゲットについて

heroku はレポジトリ直下に main.go があることを想定しています。そうでない場合には GO_INSTALL_PACKAGE_SPEC でインストールするターゲットを指定してやる必要があります。

その他

  • Procfile は必要
  • app.json はアプリのデプロイ時には読んでくれない?
  • コンテナをデプロイした方がよいのでは?

Happy hacking!

Goa v3 でサービスメソッドに到達する前に発生するエラーをカスタマイズする

概要

エラーのレスポンス形式を制御したい、というのはよくある要望だと思うのですが、エラーのうちでも特にサービスメソッドに到達する前に(HTTPトランスポートで)発生するエラーをカスタマイズする方法を扱います。

たとえば、入力時のバリデーションで Bad Request が発生した場合や、なんらかのエラーが発生して起こる Internal Server Error に対してエラーの形式を制御します。

Goa での議論

github.com

ここで議論されて修正が入りました。

で、どうやるの?

やり方は以下です:

  1. カスタムエラーのフォーマッタを用意する
  2. 作成したフォーマッタをサービスの Server を New するときにセットする

1. カスタムエラーのフォーマッタを用意する

func(err error) goahttp.Statuser を満たす関数を用意します。ここで返値になっている Statuser

   // Statuser is implemented by error response object to provide the response
    // HTTP status code.
    Statuser interface {
        // StatusCode return the HTTP status code used to encode the response
        // when not defined in the design.
        StatusCode() int
    }

ステータスコードを返すインターフェースです。要するに、エラーを受け取って、ステータスコードを返すようなエラーの構造体を返せばよさそうです。

とはいえ、具体的によく分からないので、デフォルトで用意されるフォーマッタを見てみます。

Goa の http/error.go にコードがあります。

// NewErrorResponse creates a HTTP response from the given error.
func NewErrorResponse(err error) Statuser {
    if gerr, ok := err.(*goa.ServiceError); ok {
        return &ErrorResponse{
            Name:      gerr.Name,
            ID:        gerr.ID,
            Message:   gerr.Message,
            Timeout:   gerr.Timeout,
            Temporary: gerr.Temporary,
            Fault:     gerr.Fault,
        }
    }
    return NewErrorResponse(goa.Fault(err.Error()))
}

// StatusCode implements a heuristic that computes a HTTP response status code
// appropriate for the timeout, temporary and fault characteristics of the
// error. This method is used by the generated server code when the error is not
// described explicitly in the design.
func (resp *ErrorResponse) StatusCode() int {
    if resp.Fault {
        return http.StatusInternalServerError
    }
    if resp.Timeout {
        if resp.Temporary {
            return http.StatusGatewayTimeout
        }
        return http.StatusRequestTimeout
    }
    if resp.Temporary {
        return http.StatusServiceUnavailable
    }
    return http.StatusBadRequest
}

NewErrorResponse 関数にくるエラーは、基本的には goa.ServiceError のようです。そうでないときにはエラーをラップして、InternalServerError としているようです。

これを自分用に改造してやればよさそうです。

type MyErrorResponse struct {
    Code    int
    Message string
}

func NewMyErrorResponse(err error) goahttp.Statuser {
    if gerr, ok := err.(*goa.ServiceError); ok {
        return &MyErrorResponse{
            Code:    123,
            Message: gerr.Message,
        }
    }
    return &MyErrorResponse{
        Code:    666,
        Message: err.Error(),
    }
}

func (resp *MyErrorResponse) StatusCode() int {
    if resp.Code == 666 {
        return http.StatusInternalServerError
    }
    return http.StatusBadRequest
}

たとえば、こんな感じでカスタマイズ可能です。

2. どこにセットするか?

セットする場所は、cmd/<service>/http.go の中の Server を作成してる New の引数です。calc の例で説明すると、

// Wrap the endpoints with the transport specific layers. The generated
    // server packages contains code generated from the design which maps
    // the service input and output data structures to HTTP requests and
    // responses.
    var (
        calcServer *calcsvr.Server
    )
    {
        eh := errorHandler(logger)
        calcServer = calcsvr.New(calcEndpoints, mux, dec, enc, eh, nil) // ← ★ ここ!
        if debug {
            servers := goahttp.Servers{
                calcServer,
            }
            servers.Use(httpmdlwr.Debug(mux, os.Stdout))
        }
    }

修正後は、

       calcServer = calcsvr.New(calcEndpoints, mux, dec, enc, eh, NewMyErrorResponse) // ← ★ ここ!

実際動かしてみると?

calc で実験してみましょう。

修正前:

$ curl -X GET localhost:8000/add/a/b| jq .
{
  "name": "invalid_field_type",
  "id": "Em9NSWp6",
  "message": "invalid value \"a\" for \"a\", must be a integer; invalid value \"b\" for \"b\", must be a integer",
  "temporary": false,
  "timeout": false,
  "fault": false
}

修正後:

$ curl -X GET localhost:8000/add/a/b| jq .
{
  "Code": 123,
  "Message": "invalid value \"a\" for \"a\", must be a integer; invalid value \"b\" for \"b\", must be a integer"
}

確かにカスタマイズされてますね。

よく話題に出るんですが、やった事なかったのでいい勉強になりました。

Happy Hacking!

Goa v3 の入力と出力のバリデーション

概要

APIサーバで入力を受け付けるとき、入力の長さだったり、パターンだったりでバリデーションをかけたい事はよくあります。 また、レスポンスに対しても同じ事が出来れば自分で書くコードが減ってやりたい事に集中できそうです。

Goa v3 ではこのあたりをどのように扱えるかまとめてみました。

Goa で用意されている定型のバリデーション

Goa では Attribute へのバリデーションをデザインに明示できるようになっています。

たとえば、Payload で名前を8文字以上31文字以下で受け取りたい場合の Attribute は次のようにかけます。

Attribute("name", String, func() {
        MinLength(8)
        MaxLength(31)
})

こうすることで、8文字以上、31文字以下でない文字列であった場合はバリデーションではねる事が出来ます。

こうしたバリデーションはいくつか用意されていて、デザインで指定する事で条件を明示する事が出来ます。

バリデーション 対象 説明
MinLength / MaxLength 文字列 文字列長の最小/最大を指定できる
Minimum/Maximum 数値 最小値/最大値を指定できる
Enum 文字列/数値 値を列挙して指定できる
定型フォーマット 文字列 下に別記したフォーマットを指定できる

定型フォーマットで使えるもの

フォーマット 概要
FormatDate RFC3339 の日付形式 1977-06-28
FormatDateTime RFC3339 の日時形式 2014-06-10T07:31:34Z
FormatRFC1123 RFC1123 形式の日時 Sun, 30 Jan 1972 16:17:03 UTC
FormatUUID RFC4122 の UUID形式 76FB876C-96AC-91E7-BD21-B0C2988DDF65
FormatEmail RFC5322 のメールアドレス形式 john@example.com
FormatHostname RFC1035 のドメイン example.com
FormatIPv4, FormatIPv6, FormatIP RFC2373 の IPv4, IPv6 形式のアドレスもしくはそのいずれか 8.8.8.8, 2001:cafe:109f:34d2:d755:da28:b5dc:8f23
FormatURI RFC3986 の URI形式 github.com/goadesign/goa
FormatMAC IEEE 802 形式の MAC-48, EUI-48 or EUI-64 MACアドレス AD-85-EC-C3-95-9B
FormatCIDR RFC4632 もしくは RFC4291 の CIDR 記法の IP アドレス 192.168.100.14/24
FormatRegexp RE2 で定義される正規表現 (ab)|(cd)
FormatJSON JSON 形式のテキスト '{"name":"example","email":"mail@example.com"}'

定型のフォーマットはたとえば次のように指定します

Attribute("created_at", String, func() {
        Format(FormatDateTime)
})

入力時のバリデーション

入力時のバリデーションはたとえば次のように指定できます。

var _ = Service("calc", func() {
    Description("The calc service performs operations on numbers.")

    Method("login", func() {
        Payload(func() {
            Attribute("user", String, func() {
                Format(FormatEmail)
            })
            Attribute("password", String, func() {
                MinLength(8)
                MaxLenght(128)
            })
            Required("user", "password")
        })

        Result(LoginResult)

        HTTP(func() {
            GET("/login")
        })
    })
})

このデザインを生成すると、userpassword に関するバリデーションが生成され、エンドポイントにアクセスしたときに自動的にバリデーションがチェックされるようになります。バリデーションに失敗すると(HTTPトランスポートでは) BadRequest が返されます。

このバリデーションに関しては Payload がサービスメソッドに渡ってくるまでに実施されています。サービスメソッドで受け取った Payload はデザインで指定したバリデーションをチェック済みなので、ユーザーは(必要ならば)追加の細かなチェックをするだけで、ビジネスロジックに専念できます。

また、バリデーションは、Payload 内の Attribute だけでなく、パスパラメータにも同じように指定できます。例では HTTPトランスポートの話しかしていませんが、もし gRPC を利用していれば、gRPC のパラメータにも同じようにバリデーションが適用されます。

レスポンスへのバリデーション

入力にバリデーションが定義できるように、出力であるレスポンスにも同じようにバリデーションが定義できます。 たとえば、次のように定義できます。

var LoginResult = ResultType("application/vnd.login+jsonq", func() {
    Attributes(func() {
        Attribute("mail_address", String, func() {
            Format(FormatEmail)
        })
        Attribute("last_login", String, func() {
            Format(FormatDateTime)
        })
        Required("mail_address")  // mail_address は必ず返すけど last_login は View によって返したり返さなかったりする
    })
    View("default", func() {
        Attribute("mail_address")
    })
    View("extend", func() {
        Attribute("mail_address")
        Attribute("last_login")
    })
})

入力時のバリデーションと違うのは、これらのチェックをいつおこなって、バリデーションの結果に対してどのような処理をおこなうかはユーザーにゆだねられている点です。たとえばレスポンスにセットした mail_address がメールの形式でなかったとしましょう。このとき、APIは、エラーを返してもいいですし、mail_address を適当な文字列に置き換えてレスポンスしてもいいです。どのような処理をするかはユーザー次第です。

Goa はデザインで定義したチェックがおこなえるように、バリデーションの関数だけを提供してくれます。 上のデザインに対して提供されるバリデーションの関数は次です。(場所は gen/<service>/views に作られます)

// ValidateLogin runs the validations defined on the viewed result type Login.
func ValidateLogin(result *Login) (err error) {
    switch result.View {
    case "extend":
        err = ValidateLoginViewExtend(result.Projected)
    case "default", "":
        err = ValidateLoginView(result.Projected)
    default:
        err = goa.InvalidEnumValueError("view", result.View, []interface{}{"extend", "default"})
    }
    return
}

サービスメソッドで、レスポンスを用意してから、このバリデーションを当てれば、デザインで意図した形式になっているかをすぐにチェックできます。サンプルのデザインでは View を2つ用意しているので、それぞれの View でチェックを切り替えるようにコードが生成されています。

それぞれの中を覗いてみると、ちゃんと指定したバリデーションが実施されているのが分かります。

// ValidateLoginView runs the validations defined on LoginView using the
// "default" view.
func ValidateLoginView(result *LoginView) (err error) {
    if result.MailAddress == nil {
        err = goa.MergeErrors(err, goa.MissingFieldError("mail_address", "result"))
    }
    if result.MailAddress != nil {
        err = goa.MergeErrors(err, goa.ValidateFormat("result.mail_address", *result.MailAddress, goa.FormatEmail))
    }
    return
}

// ValidateLoginViewExtend runs the validations defined on LoginView using the
// "extend" view.
func ValidateLoginViewExtend(result *LoginView) (err error) {
    if result.MailAddress == nil {
        err = goa.MergeErrors(err, goa.MissingFieldError("mail_address", "result"))
    }
    if result.MailAddress != nil {
        err = goa.MergeErrors(err, goa.ValidateFormat("result.mail_address", *result.MailAddress, goa.FormatEmail))
    }
    if result.LastLogin != nil {
        err = goa.MergeErrors(err, goa.ValidateFormat("result.last_login", *result.LastLogin, goa.FormatDateTime))
    }
    return
}

って、わかったように書いてきましたが、実はレスポンスのバリデーションは使った事がありません。 自分でレスポンスをセットするので、バリデーションするまでもないことが殆どだからですかね。

サービスメソッドで、外部の API を叩いて、その値をレスポンスに詰めて返すんだけど、ときどき外部 API が意図通りに動いていない みたいなシチュエーションではこのようなチェックが役立ちそうですね・・・。

便利な使い方とか、こういうこと見落としてるよとかあったら教えて欲しいです。

Happy hacking!

Goa v3 における Trailing Slash 問題を整理する

概要

ここで取り上げる Trailing Slash というのは、HTTP REST API のエンドポイントの末尾につく '/' のことです。 末尾に '/' が ない エンドポイントと ある エンドポイントは基本的には別物です。 重箱の隅っこみたいな話で誰も読まない気がするんだけど、自分のためにと思ってメモ。

Goa のデザインではどう書いてどうなるか

HTTPのエンドポイントを指定する典型的なデザインは次のようになると思います。

var _ = Service("service", func() {
    HTTP(func() { 
        Path("foo")                        // (1) ベースパス
    })  
    Method("method", func() {
        HTTP(func() { 
            GET("/baa")                    // (2) パス
        })
    })
})

ベースパスの指定と、GET などのメソッドでしているパスの組み合わせで表現されます。 たとえば、上の例で、エンドポイントは /foo/baa となりますが、 GET("/baa/") と書けば、 エンドポイントは末尾スラッシュのある /foo/baa/ となります。

では、ベースパスに foo がセットされているときに GET("/") を指定したら末尾スラッシュはつくでしょうか? 答えは 「つきません」 です。生成されるエンドポイントは単に /foo になります。

ベースパスとパスの組み合わせで生成されるエンドポイントは次のようになります。

ベースパス パス 生成されるエンドポイント
なし /foo /foo
なし /foo/ /foo/
foo / /foo
foo /baa /foo/baa
foo /baa/ /foo/baa/
foo/ / /foo/
foo /./ /foo/ 特殊なケース

GETPUT などのメソッドに指定する "/" は末尾にスラッシュを付けません。 ただし、明示的にベースパスにスラッシュを付けたいときは "/./" を指定することが出来ます。

Trailing Slash の振る舞いは結局ルーターによる

さて、ここまでで Goa で生成されるエンドポイントの仕組みは分かりました。 いま /about というエンドポイントがあったときに、クライアントが /about/ とアクセスしてきたらどうなるでしょうか。 /about にリダイレクトしてくれるでしょうか?

実はこれを決めるのはルーターの仕事で、Goa の生成とは関係ありません。 Goa はデフォルトでは treemux を利用しているので、treemux のルーターの仕様にしたいがいます。 treemux の Trailing Slash の説明では次のように説明されています。

  • 末尾スラッシュがあるパターンが登録されていたら、末尾スラッシュがあるパターンが優先される
    • 末尾スラッシュのないパターンにマッチしても末尾スラッシュのあるパターンにリダイレクトされる
  • 末尾スラッシュがあるパターンが登録されていない場合、末尾スラッシュのあるアクセスは、末尾スラッシュのないパターンにリダイレクトされる

たとえば、ルーターGET : /about が登録されているとします。次に末尾スラッシュ付き GET : /posts/ と末尾スラッシュなし POST : /posts 登録されていたとしましょう。

router = httptreemux.New()
router.GET("/about", pageHandler)
router.GET("/posts/", postIndexHandler)
router.POST("/posts", postFormHandler)

このときに、アクセスは次のようになります:

  • GET /about は、登録されている /about にマッチ
  • GET /about/ は、リダイレクトされて /about にマッチ
  • GET /posts/ は、登録されている /posts/ にマッチ
  • GET /posts は、リダイレクトされて末尾スラッシュありの /posts/ にマッチ
  • POST /posts は、 リダイレクトされて /posts/ にマッチ。∵ GET メソッドで /posts/ が登録されているから

この辺の挙動は treemux のオプションで変更できるようです。 また、他のルーターを使うという選択肢もあります。

同名で Trailing Slash ありなしなエンドポイントを作ることはないと思いますが、リダイレクトの挙動については把握しておいた方がよさそうです。

Happy hacking!

Goa v3 でレスポンスをXMLにしたときトップレベルのタグを指定したい

概要

APIのレスポンスに XML を選択したとき、レスポンスの要素にはタグを付けたり、名前を変えたりは自由に出来ますが、レスポンスのXMLの一番外側のタグは、構造体の名前が自動的に設定されてしまいます。これを設定することは出来ないか?という issue が立っていました。

普段 ContentType を json で扱ってるので、XML についてまじめに調べたことなかったんですが、ちょっと気になったので調べてみました。

どういうときに困るのか?

issue にあったのは次のような例です。

package design

import (
    . "goa.design/goa/v3/dsl"
)

var _ = Service("calc", func() {
    Description("The calc service performs operations on numbers.")
    Method("add", func() {
        Payload(func() {
            Field(1, "a", Int, "Left operand")
            Field(2, "b", Int, "Right operand")
            Required("a", "b")
        })
        Result(CalcResult)
        HTTP(func() {
            GET("/add/{a}/{b}")
        })
    })
})

var CalcResult = ResultType("application/vnd.calc+xml", func() {
    TypeName("Foo")
    ContentType("application/xml")
    Attribute("Result", Int, func() {
        Description("result of calculation")
    })
    Required("Result")
})

このときのレスポンスが

<AddResponseBody>
  <Result>3</Result>
</AddResponseBody>

こんな感じになります。この AddResponseBody というのは、Goa が生成するレスポンス用の構造体の名前に由来します。

この AddResponseBody を任意の名前にしたい。というのが issue の趣旨でした。

Go では XML の Marshal どうしてるの?

Goa でレスポンスを XML に変換するときは、Go の Marshal を素直に使っています。ということは、Go でどうやってるかが分かれば解決の糸口が見つかりそうです。

golang.org

Go では XMLName xml.Name というフィールドを構造体にセットすることでトップレベルのタグを制御できそうです。知らなかった。

で、Goa でどうやるか?

XMLName xml.Name というフィールドをレスポンスの構造体にセットすればいいはずです。 でも、XMLName という名前のフィールドをAttributeとして追加することは出来ますが、こいつの型を xml.Name にしないと意味がありません。 実は Goa には "struct:field:type" というメタタグが用意されていて、これをセットした Attribute の型を指定したもので上書きできるという仕組みがあります。 すなわち、次のようにします。

var CalcResult = ResultType("application/vnd.calc+xml", func() {
    ContentType("application/xml")
    Attributes(func() {
        Attribute("XMLName", func() {
            Meta("struct:field:type", "xml.Name", "encoding/xml")
            Meta("struct:tag:xml", "Foo")
        })
        Required("XMLName")
        Attribute("Result", Int, func() {
            Description("result of calculation")
        })
        Required("Result")
    })

})

Meta("struct:field:type", "xml.Name", "encoding/xml") の最後の引数は、型に必要な import を指定できます。 さらに、Meta("struct:tag:xml", "Foo")xml:"Foo" というタグをセットできます。

すると、レスポンスは、

<Foo><Result>0</Result></Foo>

のようになります。

実は plugin 作って対応しようと思ってたけど、出来ずにいたら、機能が用意されていたというオチでした。 さすがです Goa。

Happy hacking!

Goa v3 のサービスメソッドを単体テストする

概要

Goa v3 には v1 にはあったテストヘルパーがありません。なので、テストを簡単にするために、テスト用のチェッカーを用意していました。

ikawaha.hateblo.jp

毎回このような E2E テストをするのではなくて、もっと単純に Go way なテストを出来ないかという質問が goa-jp slack であって、それに id:tchssk san がスマートな回答を寄せていたのでまとめておきます。

サービスメソッドを単体テストする

例によって例の如く calc サンプルで説明します。

examples/design.go at master · goadesign/examples · GitHub

このサンプルに対して以下のようなサービスメソッドを定義したとします。

// Add implements add.
func (s *calcSvc) Add(ctx context.Context, p *calcsvc.AddPayload) (int, error) {
    return p.A + p.B, nil
}

この Add は、 ab の値を足し算サービスメソッドです。

これに対する単体テスト以下のように書けます

package calc

import (
    "context"
    "log"
    "os"
    "testing"

    calcsvc "goa.design/examples/basic/gen/calc"
)

func TestCalcAdd(t *testing.T) {
    logger := log.New(os.Stdout, "", log.Ltime|log.Lshortfile)
    calcSvc := NewCalc(logger)

    cases := map[string]struct {
        a        int
        b        int
        expected int
    }{
        "1+1":   {a: 1, b: 1, expected: 2},
        "1+10":  {a: 1, b: 10, expected: 11},
        "1+100": {a: 1, b: 100, expected: 101},
    }
    for k, tc := range cases {
        t.Run(k, func(t *testing.T) {
            payload := &calcsvc.AddPayload{
                A: tc.a,
                B: tc.b,
            }
            actual, err := calcSvc.Add(context.Background(), payload)
            if err != nil {
                t.Errorf("got error %v, expected none", err)
            }
            if actual != tc.expected {
                t.Errorf("got %v, expected %v", actual, tc.expected)
            }
        })
    }
}

Table Driven テストでいかにも Go っぽいテストが書けました(書いたのは id:tchssk san です (^^ゞ )。余計なこともせず、スッキリしてていいですよね👍🏻。

ミドルウエアからむようなテストするときや、クライアントに返るレスポンスの形式を確かめておきたいとか、入力に対するバリデーションもチェックしたい場合など、そういったことをテストする場合には、テスト用チェッカーを用いて、そうでない場合はサービスメソッドの単体テストというように切り分けてテストするのがいいかもしれませんね。

ということで、id:tchssk san のアドバイスに感謝しつつ。 Happy hacking!

Goa v3 の HTTPトランスポートの Payload どうしてます問題

概要

Goa v3 の Payload は gRPC と HTTP の両方のトランスポートを考慮しているので v1 の HTTP だけを対象にした Payload だと思ってると、書いていてもしっくりこない感じがします。あと、引っかかりやすい問題もあってどうしていいのかよく分かってなかったんですが、 id:tchssk =サンがスッキリする方法を教えてくれたので備忘メモです。

Goa v3 における Payload

Goa v3 における Payload は gRPC と HTTP に共通して定義される、メソッドへの入力です。 入力の全てがここに記述されます。これらの入力に対するトランスポートごとの扱いは gRPC()HTTP() DSL 内で記述します。

HTTP トランスポートにおける3つの入力形式

Goa v1 では Payload といえば リクエストボディ の事でしたが、v3 では

  • リクエストヘッダ
  • パスパラメータ
  • リクエストボディ

の3つ(この分け方が適当なのかは???ですが)を合わせたもののことです。

なので、Payload の中には Basic 認証のためにヘッダで送信するパラメータとか、URL の一部として送信されるパスパラメータとか、json で送信されるようないわゆる(v1 で扱っていた) ペイロード としてのデータを全て記述します。

(僕が)HTTP を基準に考えているのでややこしいことがおこる

みなさん、gRPC で API を書いたりしてるんでしょうか? 僕は Goa v1 の頃からのユーザーなので、API を設計するときの考え方がどうしても HTTP を基準にしてしまいます。なので、

var UserInfoPayload = Type("UserInfoPayload", func() {
    Attribute("user_id", String)
    Attribute("password", String)
}

みたいなリクエストボディを考えて、

POST("/user/{id}/")

のようなエンドポイントを用意して、これに Basic 認証を足したりして・・・と考えます。

Basic 認証に必要な要素はこんな感じでしょうか。

var BasicAuthPayload = Type("BasicAuthPayload", func() {
    Username("username")
    Password("password")
})

Goa v3 では、リクエストボディの "user_id", "password" も、パスパラメータの "id" も、Basic認証で必要な "username" と "password" も全部 Payload なので、これらをまとめて記述する必要があります。

Type で記述しているまとまりをそのまま Payload() の中に入れられないので、これらを開いてから Payload に入れてやる必要があります。 Type を開くためには Extend が使えます。なので、Payload はこんな感じになります。

Payload(func() {
    Attribute("id", Int) // パスパラメータ
    Extend(BasicAuthPayload) // ヘッダ
    Extend(UserInfoPayload) // リクエストボディ
})

大体のケースではこれでうまくいくんですが、この例はうまくいかない場合を扱っています。 UserInfoPayload に "password" という Attribute があり、BasicAuthPayload の中にも "password" という同名の Attribute があって、うまくいきません。この例の場合では、Extend() は何も警告せずに "password" を1つにまとめてしまいます。(バグなのかも・・・)

どうしたらいいか?

上のようなケースの場合、どうしたらいいのでしょうか? それは、リクエストボディを指定する Body() DSL を利用することでうまくいきます。

Payload で同じ階層に展開されてしまうリクエストボディを Attribute を1つ導入して受けることで、うまい具合にリクエストボディを分離できます。こんな感じです。

Payload(func() {
    Attribute("id", Int) // パスパラメータ
    Extend(BasicAuthPayload) // ヘッダ
    Attribute("body", UserInfoPayload) // リクエストボディ
})

ただし、このままだと、UserInfoPayload の内容が "body" の下にネストされてしまうので、HTTP() DSL 内で、Body() DSL を用いて Attribute として定義した "body" を明示します。すなわち、以下のようにします。

HTTP(func() {
    POST("/user/{id}/")
    Body("body")
})

こうすることで、"body" でネストされずに、UserInfoPayload の内容が扱えます。 次のドキュメントが参考になります。 goa.design

note:

このとき、これはちょっとバグっぽいのですが、"body" を required にしておいてください。 生成されるコードがポインタのポインタみたいになってビルドできないみたいになってしまうようです。

こうしておくと、コントローラーに渡される Payload には、"body" の痕跡が残って、p.Body.Password のようにアクセスできるようになるので、コード上の見通しもよくなります。

gRPC のことは放っておいて、HTTP の事だけを話してきましたが、Paload が見通しよく片付くようになるいい方法だと思うので試してみてください。うまくいったとか、いかないとかコメントもらえると有難いです。

この方法を教えてくれた id:tchssk =サン に感謝です

Happy hacking!

GoLandでEmacsキーバインドにしているとTabが入力できない

概要

EmacsキーバインドにしているとTabを文字として入力できなくて難儀してた。 設定が分かったので備忘メモ。

設定

Emacs Tab なる機能に Tab が設定されているので、Add Keybord shortcut を選択して、これを Tab に移す。ただし、キーバーインド設定で Tab を押しても項目が映っちゃうので、横っちょの + を押して Tab を選ぶ。

f:id:ikawaha:20200319163505p:plain

追記:

emacs tab を上書きしてしまうと、コード整形でタブが入っちゃうので、Option+Tab くらいにしておくのが無難そう。

Happy hacking!

Rust の開発環境を整える

概要

作業メモ

参考

手順

Rust のインストール

$ curl https://sh.rustup.rs -sSf | sh

選択肢があるがデフォルトでやった。 実行後、パスを通しておく。

export PATH="$HOME/.cargo/bin:$PATH"

この辺は The book にまとまってるので詳細はそちらを参照。

VS Code の設定

IDE として VS Code を選択(IntelliJ とか CLion を使う手もあるらしい)。

拡張として次を入れる:

インストールしたら、Format On Save を有効にしておくと保存時に rustfmt がかかる。

f:id:ikawaha:20200313220011p:plain

拡張機能のビルドやデバッグなどは cargo で管理されたプロジェクト配下でないと効かないことに注意。

はまったこと

cargo install が認証でうまくいかずに難儀した。

$ cargo install ripgrep
    Updating crates.io index
error: failed to fetch `https://github.com/rust-lang/crates.io-index`

Caused by:
  failed to authenticate when downloading repository
attempted ssh-agent authentication, but none of the usernames `git` succeeded

Caused by:
  no authentication available

これは $HOME/.cargo/config

[net]
git-fetch-with-cli = true

と書いておけば解決する。詳細は以下:

github.com

Happy hacking!

Goa v3 で application/x-www-form-urlencoded を使う

はじめに

HTTPトランスポートの話です。忘れちゃいそうなのでメモ。

Goa v3 で application/x-www-form-urlencoded を使うにはカスタムデコーダーを用意する必要があります。 具体的には cmd 下で設定してるデコーダーに application/x-www-form-urlencoded が来たときの処理を追加してやればいいです。

参照: goa.design

デコード用のライブラリ

application/x-www-form-urlencoded を処理するライブラリはいくつかあると思うのですが、v1 では github.com/ajg/form を使ってるのでこれを使ってみます(デコードできればライブラリはなんでもいいです)。

カスタムデコーダ

デフォルトでは goahttp.RequestDecoder を使っています。これは application/x-www-form-urlencoded に対応していないので、application/x-www-form-urlencoded が来たときの処理を足して、そうでないときはデフォルトのデコーダーに投げます。

func RequestDecoder(r *http.Request) goahttp.Decoder {
    contentType := r.Header.Get("Content-Type")
    if contentType != "" {
        if mediaType, _, err := mime.ParseMediaType(contentType); err == nil {
            contentType = mediaType
        }
    }
    switch contentType {
    case "application/x-www-form-urlencoded":
        return form.NewDecoder(r.Body)
    default:
        return goahttp.RequestDecoder(r)
    }
}

設定する

cmd/<service>/http.goデコーダをセットしているところがあるので、これをカスタムデコーダーで置き換えれば ok です。

Happy hacking!

Goa v3 で Default は required な Attribute には効かない

TL;DR;

Goa v3 では Default DSL を required な Attribute に設定しても無視されます

詳細

Goa v1 では、required な Attribute に Default DSL でデフォルト値をつけて省略させることが出来ました。

v1 では、required にすると、対応する構造体のメンバがポインタでなくなって扱いやすいという事情があったので、省略可能な Attribute を作るために割と便利に使われていたと思います(少なくとも僕はそうでした・・・)。

でも、よく考えると、required な Attribute が省略可能というのも変な話ですよね(^^ゞ。

で、Goa v3 での話ですが、結論からいうと、required な Attribute に Default を設定しても無視されます。required でない Attribute に設定してください。

また、Goa v3 では、Default を設定すると、対応する構造体のメンバは ポインタではなくなります(扱いやすい!)。ここも v1 と挙動がちょっと違います。この辺の詳細は FAQ にも書いてあるので参照してください。

goa.design

Happy hacking!

追記:

v1 でも Default ついてるならポインタにならないとコメントいただいた。そうかもしれない。

( '-`).oO( v3 に移行しちゃったので v1 の環境作のめんどくて確かめられてない

お茶を濁して昔書いた v1 の記事を貼り付けておく

ikawaha.hateblo.jp

2019年買い物ふりかえり

概要

人のレビューが買い物するのに非常に助かるので、自分も今年買ったものを振り返り。

去年の振り返りはこちら

2018年の買い物振り返り - 押してダメならふて寝しろ

とてもよかった

炊飯器

炊飯器、価格帯の真ん中ぐらいのやつを10年ぐらい使ってたんですけど、ぼろぼろになっちゃったので買い換え。お米に全く興味が無く、ご飯好きじゃ無いと思ってたんですけど、炊飯器変えたら同じ米がメッチャおいしくて今年買い換えたものの中で一番よかった。

学びとしては、

  1. フラグシップな製品を買った方がいい
  2. 炊飯器は10万くらいで世の中に出て、世代交代を経て5万くらいに下がる

ということ。さんざん迷ったあげく6万ぐらいに下がってた型落ちのこれを買いました。高い買い物だが、はるかに上回る満足度があったので自分の中では納得している。

トースター

うっかりポイントで交換したツインバードのトースターが背が低くて、丸パン入らないし、すぐ焦げてイライラ Max で買い換えることにしました。

火力(W数)じゃなくて、庫内の温度が設定できるのがよいです。 パンがおいしく焼けて満足です。一緒に買ったお皿を使うと、パン2枚と目玉焼きが一緒に作れるので非常に助かっています。

ヘッドホン

店頭でノイズキャンセリングを試したら衝撃的でさんざん迷って買いました。音楽聴かなくても静かになるので通勤の時に使ったりしています。ただ夏は暑くてつけてられません・・・。

わりとよかった

フライパン

朝食作るように T-fal の小さいフライパンを買ってたんですが、わりとすぐ傷がついてダメになってしまってました。この手のものは消耗品かなと思ってたんですが、和平フレイズのフライパンがいいとのネット情報を鵜呑みにして買ってみたら、なるほど長持ちです。

電球

廊下の電球を LED にしてみたらめっちゃ明るかった。 明るすぎるぐらいだがはやく LED にしとけばよかった。

ふつう

近所の量販店に置いてあったので買ってみた。スースーする石けん、くらいの印象。 いいと思うけどネットで高いのを買って使うほどでもなさそう。 オッサン2.0のにおいになってるのかな?

ボドゲ

基準はこども(9歳)とできるゲーム。

TAGIRON

質問カードを交互に使って2人で数字を当て合うゲーム。こどもが気に入って割とよくやった。僕はかなりぐずぐずしたゲームをしちゃうんだけど、うまい人はササッと当てられるっぽい。

ラビリンス

ルールが単純なので手軽に遊べる。好評だったけど駆け引きよりは運の要素が大きい気がする。ちょっとした集まりにさっと出来てよいかもです。

ラビリンス (Labyrinth) ボードゲーム

ラビリンス (Labyrinth) ボードゲーム

  • メディア: おもちゃ&ホビー

バトルライン

これはクリスマスプレゼントでこどもに与えてまだ遊べてないけど期待大。

バトルライン (Battle Line) カードゲーム

バトルライン (Battle Line) カードゲーム

  • メディア: おもちゃ&ホビー

Recovery Middleware for Goa v3

概要

Goa v1 にはサーバ内で panic が起こったときにリカバーしてくれるミドルウエアがありました。v3 だとこれがないので移植しました。

Goa v3 からはミドルウエアは Go の標準の方法になっているので Goa 以外でも使えます。

github.com

使い方

handler = recovery.Recover()(handler)

何もオプションを指定しないと、500 Internal Server Error でレスポンスします。

オプション デフォルト値
ContentType application/json
ResponseStatus 500 (http.StatusInternalServerError)
StackSize MinimumStackSize (4KB)
Logger 標準 Log
ErrorHandler スタックトレースはログに出力し、レスポンスボディには何も出力しません

オプションを付けるときは

handler = recovery.Recover(
    ContentType("application/json"),
    ResponseStatus(http.StatusOK),
    ErrorHandler(func(c *Config, w http.ResponseWriter, msg string, stack []string) {
            w.Header().Set("Content-Type", c.ContentType)
            w.WriteHeader(c.ResponseStatus)
            obj := map[string]string{
                "error": fmt.Sprintf("%s\n%s", msg, strings.Join(stack, "\n")),
            }
            b, err := json.Marshal(obj)
            if err != nil {
                t.Fatal(err)
            }
            w.Write(b)
        }),
)(handler)

のようにします。ログに出すかどうか、レスポンスボディにエラーを含めるかなどは エラーハンドラーを自分で書くことで調整できます。

Happy hacking!