ぺい

大阪の専門学生の落書き。主にエンジニア寄りの話。

GolangのgoaでAPIをデザインしよう(エンドポイント編)

goaはいいぞ!

f:id:tikasan0804:20170505212036p:plain

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

エンドポイントにも色々ある

同じエンドポイントの指定でも、DSLの書き方で変わります。若干癖があったりするので、今回はそれについてまとめたいと思います。

例:ユーザーのフォロー操作。
同じ名前だけど、HTTPメソッドが違うので、操作の中身が違う。

PUT /users/follow // フォローする
DELETE /users/follow // フォローを外す

例:なんかのリスト
/listというリソースに対してエンドポイントで操作を表現している。(途中まで操作同じだけど、若干違う)

GET /list/new  // 新しいリスト
GET /list/topic // 注目されてるリスト
GET /list // リスト(全件)

例:ちょっと特殊ケース

GET /api/v1/method/users/:id/follow/:type

以上のエンドポイントをDSLでどうやって表現するかを紹介します。

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

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

APIをデザインする

順を追って記事を読んでいればすんなりソースも読めると思います。(つまりコメントがなし)

package design

import (
    . "github.com/goadesign/goa/design"
    . "github.com/goadesign/goa/design/apidsl"
)

var _ = Resource("method", func() {
    BasePath("/method")
    Action("method", func() {
        Description("HTTPメソッド")
        Routing(
            GET("/get"),
            POST("/post"),
            DELETE("/delete"),
            PUT("/put"),
        )
        Response(OK, MessageMedia)
        Response(BadRequest, ErrorMedia)
    })
    Action("list", func() {
        Description("リストを返す")
        Routing(
            GET("/list"),
            GET("/list/new"),
            GET("/list/topic"),
        )
        Response(OK, CollectionOf(UserMedia))
        Response(BadRequest, ErrorMedia)
    })
    Action("follow", func() {
        Description("フォロー操作")
        Routing(
            PUT("/users/follow"),
            DELETE("/users/follow"),
        )
        Response(OK, MessageMedia)
        Response(BadRequest, ErrorMedia)
    })
    Action("etc", func() {
        Routing(GET("/users/:id/follow/:type"))
        Description("ちょっと特殊ケース")
        Params(func() {
            Param("id", Integer, "id")
            Param("type", Integer, "タイプ", func() {
                Enum(1, 2, 3)
            })
        })
        Response(OK, "plain/text")
        Response(BadRequest, ErrorMedia)
    })
})

MediaTypeを定義する

以前記事で書いたコードです。

var UserMedia = MediaType("application/vnd.user+json", func() {
    Description("example")
    Attributes(func() {
        Attribute("id", Integer, "id", func() {
            Example(1)
        })
        Attribute("name", String, "名前", func() {
            Example("hoge")
        })
        Attribute("email", String, "メールアドレス", func() {
            Example("satak47cpc@gmail.com")
        })
        Required("id", "name", "email")
    })
    // 特別な指定がない場合はdefaultのMediaType
    View("default", func() {
        Attribute("id")
        Attribute("name")
        Attribute("email")
    })
    // tinyという名前の場合は、簡潔なレスポンスフォーマットにすることが出来る
    View("tiny", func() {
        Attribute("id")
        Attribute("name")
    })
})

コントローラー定義

デザインを元にコードを生成しましょう。コントローラーは、値を返すだけの単純なものを作成します。

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

import (
    "fmt"

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

// MethodController implements the method resource.
type MethodController struct {
    *goa.Controller
}

// NewMethodController creates a method controller.
func NewMethodController(service *goa.Service) *MethodController {
    return &MethodController{Controller: service.NewController("MethodController")}
}

// Etc runs the etc action.
func (c *MethodController) Etc(ctx *app.EtcMethodContext) error {
    // MethodController_Etc: start_implement

    // Put your logic here

    // MethodController_Etc: end_implement
    return ctx.OK([]byte(fmt.Sprintf("ID: %d, Type %d", ctx.ID, ctx.Type)))
}

// Follow runs the follow action.
func (c *MethodController) Follow(ctx *app.FollowMethodContext) error {
    // MethodController_Follow: start_implement

    // Put your logic here
    var message string
    // 何かのフォロー操作
    if "PUT" == ctx.Request.Method {
        message = "フォローした"
    } else if "DELETE" == ctx.Request.Method {
        message = "フォロー外した"
    }
    // MethodController_Follow: end_implement
    res := &app.Message{}
    res.Message = message
    return ctx.OK(res)
}

// List runs the list action.
func (c *MethodController) List(ctx *app.ListMethodContext) error {
    // MethodController_List: start_implement

    // Put your logic here
    var listType string
    switch ctx.RequestURI {
    //case client.ListMethodPath(): <---これでもいけるけど・・・って感じ 何かいい方法。
    case "/api/v1/method/list":
        listType = "ただの"
    case "/api/v1/method/list/new":
        listType = "新しい"
    case "/api/v1/method/list/topic":
        listType = "注目されている"
    }
    // MethodController_List: end_implement
    res := make(app.UserTinyCollection, 2)
    u1 := &app.UserTiny{
        ID:   1,
        Name: listType + "ユーザー1",
    }
    u2 := &app.UserTiny{
        ID:   2,
        Name: listType + "ユーザー2",
    }
    res[0] = u1
    res[1] = u2
    return ctx.OKTiny(res)
}

// Method runs the method action.
func (c *MethodController) Method(ctx *app.MethodMethodContext) error {
    // MethodController_Method: start_implement

    // Put your logic here
    message := ctx.RequestURI

    // MethodController_Method: end_implement
    res := &app.Message{}
    res.Message = message
    return ctx.OK(res)
}

実際に動かしてみよう

$ go run main.go
$ curl -v -X PUT 'http://localhost:8080/api/v1/method/users/follow'
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> PUT /api/v1/method/users/follow HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Content-Type: application/vnd.message+json
< Date: Sat, 06 May 2017 06:34:09 GMT
< Content-Length: 33
< 
{"message":"フォローした"}
* Connection #0 to host localhost left intact
$ curl -v -X DELETE 'http://localhost:8080/api/v1/method/users/follow'
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> DELETE /api/v1/method/users/follow HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Content-Type: application/vnd.message+json
< Date: Sat, 06 May 2017 06:35:05 GMT
< Content-Length: 36
< 
{"message":"フォロー外した"}
* Connection #0 to host localhost left intact
$ curl -v 'http://localhost:8080/api/v1/method/users/1/follow/3'
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET /api/v1/method/users/1/follow/3 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Content-Type: plain/text
< Date: Sat, 06 May 2017 06:35:35 GMT
< Content-Length: 13
< 
* Connection #0 to host localhost left intact
ID: 1, Type 3% 

これで出来上がりです。エンドポイントのURIによって処理を変えるところが少し不細工なんで、誰か良い案あれば教えて欲しいです。

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