ぺい

渋谷系アドテクエンジニアの落書き

GolangのgoaでAPIをデザインしよう(バリデーション編)

goaはいいぞ!

f:id:tikasan0804:20170505212036p:plain

Golangのgoaの勉強に役立つ情報まとめ - ぺい
goaの情報をもっと見たい方は、上のリンクから確認してください

わかっちゃいるけど、さぼりたいバリデーション

APIは単純なJSONやText返したり、用途は色々あると思います。ですけど、実際返す処理の前にクライアントから飛んでくるリクエストが間違っていないかバリデーションが必要になりますよね。これがだるい!
しかも、プログラム側で実装するだけでも大変なのに、それをドキュメントにどういう条件でOKとするかとか書き出すと、結構細かく記述する必要があり、正直面倒でしかないと思います。そして、仕様変更があれば楽しい修正が始まります。無駄無駄無駄無駄無駄無駄無駄!!!!!!

goaでやったら秒で終わるよ

そこで、今回はgoaでバリデーションすると、幸せになれる一例を示したいと思います。サンプルコードと説明を貼っていくので、この楽さを実感してほしいです。

goaはコード生成する時に少し長めのコマンドが必要になるので、Makefileを作っておくと良いと思います。 ちなみに、コマンドやフォルダ構成は毎回同じものを使うので、テンプレートを作成しました。よろしければ利用してください。
もし、もっと便利なフォーマットがあればPRをください。

$ go get github.com/tikasan/goa-stater
または
$ ghq get github.com/tikasan/goa-stater

デザイン定義

以下のような感じで、用意されているメソッドを使えば大体なんとかなります。
ちなみにFormay関数には、email以外にもいくつかあります。

  • “date-time”: RFC3339 date time
  • “email”: RFC5322 email address
  • “hostname”: RFC1035 internet host name
  • ipv4”, “ipv6”, “ip”: RFC2373 IPv4, IPv6 address or either
  • uri”: RFC3986 URI
  • mac”: IEEE 802 MAC-48, EUI-48 or EUI-64 MAC address
  • “cidr”: RFC4632 or RFC4291 CIDR notation IP address
  • regexp”: RE2 regular expression
var _ = Resource("validation", func() {
    BasePath("/validation")
    Action("validation", func() {
        Description("Validation")
        Routing(
            GET("/"),
        )
        Params(func() {
            // Integer型
            Param("id", Integer, "id", func() {
                Example(1)
            })
            // Integer型かつ1〜10以下
            Param("integerType", Integer, "数字(1〜10)", func() {
                Minimum(0)
                Maximum(10)
                Example(2)
            })
            // String型かつ1〜10文字以下
            Param("stringType", String, "文字(1~10文字)", func() {
                MinLength(1)
                MaxLength(10)
                Example("あいうえお")
            })
            // String型かつemailフォーマット
            Param("email", String, "メールアドレス", func() {
                Format("email")
                Example("example@gmail.com")
            })
            // String型でEnumで指定されたいずれかの文字列
            Param("enumType", String, "列挙型", func() {
                Enum("A", "B", "C")
                Example("A")
            })
            // String型で何も指定が無ければ”でふぉ”という文字列が自動でセットされる
            Param("defaultType", String, "デフォルト値", func() {
                Default("でふぉ")
                Example("でふぉ")
            })
            // String型で正規表現で指定したパターンの文字列
            Param("reg", String, "正規表現", func() {
                Pattern("^[a-z0-9]{5}$")
                Example("12abc")
            })

            // 全て必須パラメーター
            Required("id", "integerType", "stringType", "email", "enumType", "defaultType", "reg")
        })
        Response(OK, ValidationMedia)
        Response(BadRequest, ErrorMedia)
    })
})

MediaTypeの定義

とりあえず、返すだけ。

var ValidationMedia = MediaType("application/vnd.validation+json", func() {
    Description("example")
    Attributes(func() {
        Attribute("id", Integer, "id", func() {
            Example(1)
        })
        Attribute("integerType", Integer, "数字(1〜10)", func() {
            Example(5)
        })
        Attribute("stringType", String, "文字(1~10文字)", func() {
            Example("あいうえお")
        })
        Attribute("email", String, "メールアドレス", func() {
            Example("example@gmail.com")
        })
        Attribute("enumType", String, "列挙型", func() {
            Example("A")
        })
        Attribute("defaultType", String, "デフォルト値", func() {
            Example("でふぉ")
        })
        Attribute("reg", String, "デフォルト値", func() {
            Example("12abc")
        })
    })
    Required("id", "integerType", "stringType", "email", "enumType", "defaultType", "reg")
    View("default", func() {
        Attribute("id")
        Attribute("integerType")
        Attribute("stringType")
        Attribute("email")
        Attribute("enumType")
        Attribute("defaultType")
        Attribute("reg")
    })
})

コントローラー定義

コントローラーは、バリデーションが完了した値が来た時だけを想定した処理をすればおkです。デザイン定義が完了したら、デザインを元にコードを生成しましょう。

$ goagen bootstrap -d github.com/tikasan/goa-stater/design
package main

import (
    "github.com/goadesign/goa"
    "github.com/tikasan/goa-stater/app"
)

// ValidationController implements the validation resource.
type ValidationController struct {
    *goa.Controller
}

// NewValidationController creates a validation controller.
func NewValidationController(service *goa.Service) *ValidationController {
    return &ValidationController{Controller: service.NewController("ValidationController")}
}

// Validation runs the validation action.
func (c *ValidationController) Validation(ctx *app.ValidationValidationContext) error {
    // ValidationController_Validation: start_implement

    // Put your logic here

    // ValidationController_Validation: end_implement
    res := &app.Validation{}
    res.ID = ctx.ID
    res.IntegerType = ctx.IntegerType
    res.StringType = ctx.StringType
    res.Email = ctx.Email
    res.EnumType = ctx.EnumType
    res.DefaultType = ctx.DefaultType
    res.Reg = ctx.Reg
    return ctx.OK(res)
}

実際に動かしてみよう

$ go run main.go
$ curl -v 'http://localhost:8080/api/v1/validation?id=1&defaultType=&email=satak%40gmail.com&enumType=A&integerType=10&stringType=foo&reg=12abc'
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET /api/v1/validation?id=1&defaultType=&email=satak%40gmail.com&enumType=A&integerType=10&stringType=foo&reg=12abc HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Content-Type: application/vnd.validation+json
< Date: Sat, 06 May 2017 01:47:26 GMT
< Content-Length: 117
< 
{"id":1,"defaultType":"","email":"satak@gmail.com","enumType":"A","integerType":10,"reg":"12abc","stringType":"foo"}
* Connection #0 to host localhost left intact

goa紹介で使うソース達は、以下のリポジトリに随時更新していきます。
github.com