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

goa の controller を実装する

概要

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

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

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

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

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

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

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

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

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

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

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

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

コントローラのコード

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

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

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

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

package main

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

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

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

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

        // Put your logic here

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

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

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

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

        // Put your logic here

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

レスポンス

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

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

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

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

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

InternalServerErrorなレスポンス:

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

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

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

レスポンス

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

package mymiddleware

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

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

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

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

コントローラに紐づける

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

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

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

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

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

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

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

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

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

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