スキップしてメイン コンテンツに移動

goa request parameters

Request

この章では、APIリクエストのデザインについて説明していきます。
リクエストには、主に3種類、データの渡し方があります。

  • URLのPathに含まれる、リソースを識別するものとしてのパラメータ
  • Queryパラメータ(GETで良く使われる、?に続く文字列ですね)
  • リクエストボディ

他にもHTTPリクエストという点では、HTTPヘッダも存在しますが、
HTTPヘッダはリクエストパラメータというより、
リクエストの形式や認証などに使われる、メタデータ的な意味合いがあるため
この章では扱いません。

それでは、それぞれの詳細を見ていきましょう。

Pathパラメータ

RESTFulなAPIを扱うとき、良く出てくる

/user/:id -> /user/100
/product/:category/:product_id -> /product/book/300

のような、URLに含まれるパラメータですね。
正式名称は何というのでしょうか。

このパラメータは、Resourceを識別するために使われます。
Queryパラメータで識別しても良いのですが、Queryパラメータはどちらかというと、
APIの振る舞い・挙動を変える意味合いがあります。

この辺の考えは、RESTfulというアーキテクチャの話になってくるため、ここでは割愛します。

それでは実装してみましょう。
先ほどまでのソースコードを一旦、コミットします。

$ git add .
$ git commit -m "first commit"

そして、デザインファイルにResourceを追加します。

var _ = Resource("products", func() {
  Action("show", func() {
    Routing(GET("products/:category_id/:product_id"))
    Params(func() {
      Param("category_id", Integer, "カテゴリID")
      Param("product_id", Integer, "プロダクトID")
    })
    Description("商品情報を取得します")
    Response(OK, "text/plain")
  })
})

product_id が一意ならcategory_idは不要にも思えるかも知れませんが、
そこは設計の話になり本章から逸脱するため、ご容赦ください。

コード生成、ビルド、起動します。

$ goagen bootstrap -d goasample/design
$ go build
$ ./goasample

そして、cliツールからアクセスしてみましょう。

$ go run tool/goasample-cli/main.go show products --category_id 5 --product_id 10
2017/05/30 08:37:59 [INFO] started id=BwvPY5MH GET=http://localhost:8080/products/5/10
2017/05/30 08:37:59 [INFO] completed id=BwvPY5MH status=404 time=16.410688ms
error: 404: {"id":"ybV8FQXs","code":"not_found","status":404,"detail":"/products/5/10"}
exit status 4

おや? HTTP status codeが404で返ってきてますね。
404とはResource Not Found
なぜでしょう?

実は、**リソースを追加した場合は main.go を手動で修正する必要があります。
以下のコードを追加します。

// Mount "Products" controller
app.MountProductsController(service, NewProductsController(service))

差分を見ましょう。

        c := NewHelloworldController(service)
        app.MountHelloworldController(service, c)

+       // Mount "Products" controller
+       app.MountProductsController(service, NewProductsController(service))
+
        // Start service
        if err := service.ListenAndServe(":8080"); err != nil {

何をやっているかというと、アプリケーション(goasample)に、コントローラをMountしています。

初回のコード生成では特に意識しませんでしたが、2回目からはプログラマが行う必要があります。
それはmain.goが編集可能なファイルであるため、自動生成してしまうとプログラマの修正が無かったことになってしまうからでしょう。

そしてまた、goasampleを再起動し、cliツールでアクセスして見ましょう。

$ go run tool/goasample-cli/main.go show products --category_id 5 --product_id=10
status=200で返ってきました。

※cliツールでパラメータを指定するとき、
--category_id 5
--product_id=10
イコールで指定するか、スペースを空けるかの2パターンが使えます。

ロジックの追加

自動生成されたapp配下のファイルを見ていると、Pathパラメータ(category_id、product_id)がコンテキストに追加され、コントローラのアクションに渡ってくるようになっています。

products.goにロジックを追加してみましょう。

$ git add .
$ vim products.go

fmtパッケージをimportして、Pathパラメータを文字列にして返すようなロジックを実装します。

// Show runs the show action.
func (c *ProductsController) Show(ctx *app.ShowProductsContext) error {
  // ProductsController_Show: start_implement

  // Put your logic here

  // ProductsController_Show: end_implement
  return ctx.OK([]byte(fmt.Sprintf("カテゴリIDは%d 商品IDは %d です\n", ctx.CategoryID, ctx.ProductID)))
}

$ git diff products.go
diff --git a/products.go b/products.go
index 47f2d56..34d1610 100644
--- a/products.go
+++ b/products.go
@@ -1,6 +1,7 @@
 package main

 import (
+       "fmt"
        "github.com/goadesign/goa"
        "goasample/app"
 )
@@ -22,5 +23,5 @@ func (c *ProductsController) Show(ctx *app.ShowProductsContext) error {
        // Put your logic here

        // ProductsController_Show: end_implement
-       return nil
+       return ctx.OK([]byte(fmt.Sprintf("カテゴリIDは%d 商品IDは %d です\n", ctx.CategoryID, ctx.ProductID)))
 }

また再起動し、cliでアクセスすると

$ go run tool/goasample-cli/main.go show products --category_id 5 --product_id=10
2017/05/30 08:57:36 [INFO] started id=4oA7CGOu GET=http://localhost:8080/products/5/10
2017/05/30 08:57:36 [INFO] completed id=4oA7CGOu status=200 time=5.013642ms
カテゴリID5 商品ID10 です

ちゃんと、レスポンスが表示されました!

Queryパラメータ

次は、URLの http://xxxx?hogehoge=123 の、 ? 以降のパラメータ( queryパラメータ )を定義していきましょう。

git add .
vim design/design.go

サンプルとして、 verbose というパラメータを追加してみましょうか。
ロジックとしては、商品の詳細情報も
queryパラメータは Params() というDSLを追加するだけです。

design/design.go

                Params(func() {
                        Param("category_id", Integer, "カテゴリID")
                        Param("product_id", Integer, "プロダクトID")
+                       Param("verbose_mode", Boolean, "詳細情報も取得")
                })

products.rb

$ git diff products.go
diff --git a/products.go b/products.go
index 34d1610..8cd8dfc 100644
--- a/products.go
+++ b/products.go
@@ -23,5 +23,9 @@ func (c *ProductsController) Show(ctx *app.ShowProductsContext) error {
        // Put your logic here

        // ProductsController_Show: end_implement
+       if ctx.VerboseMode != nil && *ctx.VerboseMode {
+               return ctx.OK([]byte(fmt.Sprintf("カテゴリIDは%d 商品IDは %d です。価格は1,000円です\n", ctx.CategoryID, ctx.ProductID)))
+       }
+
        return ctx.OK([]byte(fmt.Sprintf("カテゴリIDは%d 商品IDは %d です\n", ctx.CategoryID, ctx.ProductID)))
}

APIをコールしてみましょう。

$ go run tool/goasample-cli/main.go show products --category_id 15 --product_id=101
2017/06/09 09:50:38 [INFO] started id=EEC+2YdD GET=http://localhost:8080/products/15/101
2017/06/09 09:50:38 [INFO] completed id=EEC+2YdD status=200 time=20.93987ms
カテゴリID15 商品ID101 です

verbose_modeをtrueにしてみます。

$ go run tool/goasample-cli/main.go show products --category_id 15 --product_id=101 --verbose_mode=true
2017/06/09 09:51:00 [INFO] started id=gUijOoNn GET=http://localhost:8080/products/15/101?verbose_mode=true
2017/06/09 09:51:00 [INFO] completed id=gUijOoNn status=200 time=6.468188ms
カテゴリID15 商品ID101 です。価格は1,000円です

ctx.verboseModeというパラメーがコンテキストに追加されて、アプリケーション上で使えるようになりました。

nilチェックをしているのは、verbose_modeが必須パラメータではないため、パラメータがポインタ型となっているためです。
パラメータが指定されていない場合はnilとなります。

パラメータを必須にしたい場合、 *Required*DSLを使用します。

$ git diff design/
diff --git a/design/design.go b/design/design.go
index 2d47446..4df4510 100644
--- a/design/design.go
+++ b/design/design.go
@@ -27,6 +27,7 @@ var _ = Resource("products", func() {
                        Param("category_id", Integer, "カテゴリID")
                        Param("product_id", Integer, "プロダクトID")
                        Param("verbose_mode", Boolean, "詳細情報も取得")
+                       Required("verbose_mode")
                })
                Description("商品情報を取得します")
                Response(OK, "text/plain")

これで、パラメータを指定しないでAPIをコールすると、

$ go run tool/goasample-cli/main.go show products --category_id 15 --product_id=101
2017/06/09 10:01:05 [EROR] required flag is missing flag=--verbose_mode
Error: required flag verbose_mode is missing

エラーとなります。

Payoad

最後はpayloadと呼ばれる、リクエストボディのパラメータ定義の説明をします。
POST/PUT/PATCHメソッドなどで使うパラメータですね。

design/desgin.go

$ git diff design/design.go
diff --git a/design/design.go b/design/design.go
index 4df4510..3093937 100644
--- a/design/design.go
+++ b/design/design.go
@@ -32,4 +32,23 @@ var _ = Resource("products", func() {
                Description("商品情報を取得します")
                Response(OK, "text/plain")
        })
+       Action("create", func() {
+               Routing(POST("products"))
+               Payload(CreateProductPayload)
+               Description("商品を作成します")
+               Response(OK, "text/plain")
+       })
+})
+
+var CreateProductPayload = Type("CreateProductPayload", func() {
+       Member("category_id", Integer, "カテゴリID", func() {
+               Example(3)
+       })
+       Member("product_id", Integer, "商品ID", func() {
+               Example(10)
+       })
+       Member("price", Integer, "価格", func() {
+               Example(1000)
+       })
+       Required("category_id", "product_id", "price")
 })

Typeという、ユーザー定義DSLを使用してPayloadを定義しています。
各パラメータはMemberで定義されているのかわかりますね。
そして、定義されたPayloadを、showというアクションと紐づけています。

products.go

コントローラには、以下の処理を追加実装します。

func (c *ProductsController) Create(ctx *app.CreateProductsContext) error {
       return ctx.OK([]byte(fmt.Sprintf("カテゴリID %d 商品ID %d 価格 %d で商品を作成しました\n", ctx.Payload.CategoryID, ctx.Payload.ProductID, ctx.Payload.Price)))
}

これで、cliツールからアクセスしてみると

$ go run tool/goasample-cli/main.go create products --payload '{"category_id": 5, "product_id": 10, "price": 1000}'
2017/06/09 10:22:09 [INFO] started id=sU8zFKMo POST=http://localhost:8080/products
2017/06/09 10:22:09 [INFO] completed id=sU8zFKMo status=200 time=7.963341ms
カテゴリID 5 商品ID 10 価格 1000 で商品を作成しました

--payloadパラメータで、JSONを指定しています。

APIサーバといったら通常、JSONでのやりとりが普通になっていますね。
なので、レスポンスもJSON形式で返すのが理想です。

リクエストについては、基本的な設定を説明できたかと思います。

次章では、レスポンスの返し方について説明していきます。

コメント

このブログの人気の投稿

自動生成されるファイル

自動生成されるファイル 先ほどのサンプルアプリケーションの作業ディレクトリには、
以下のファイル・ディレクトリがあります。
design/ app/ client/ swagger/ tool/ helloworld.go main.go
一つ一つ見ていきましょう。
生成物一覧
design/design.go
これは、最初に実装したデザインファイルになります。
アプリケーションの枠組みを決める設計図のようなものです。

goa独自のDSL(domain-specific language)を使って、APIの仕様を定義していきます。

app/
ここには、デザインされたアプリケーションの、実際に動くコードが生成されていきます。
基本的に、このディレクトリに格納されたファイルは編集不可(できるけど、しちゃだめ)と考えて良いです。

プログラマが追加実装しても、次にまたコード生成した場合、プログラマの追加実装は上書きされて消されてしまいます。

app/contexts.go
コンテキストが実装されています。
goaでは基本的に、API一つ一つに対して対応するコンテキストが実装されていきます。

コンテキストとは、HTTPリクエストを受け付けてからレスポンスを返すまで、
一連の流れが終わるまでの状態を表します。

2つのHTTPリクエストがあった場合、コンテキストは別々のオブジェクトとなります。
どのHTTPリクエストなのかを識別できるような、リクエスト毎の固有情報ですね。

コンテキストは3つのプロパティを持ち、
typeShowHelloworldContext struct { context.Context *goa.ResponseData *goa.RequestData}
Go標準のContextと、リクエスト・レスポンスを持っています。
app/controllers.go
コントローラのinterfaceが定義されています。
コントローラとは、Webアプリケーションでよく使われるデザインパターンMVC(Model View Controller)のControllerです。

コントローラはResource単位で生成され、今回のサンプルで言うと
HelloworldControllerというinterfaceが定義されています。

app/hrefs.go

goa -Golang Web Application Framework-

Webアプリケーションフレームワーク goa
goaとは、GolangでWebアプリケーションを構築していくフレームワークです。

今流行りのマイクロサービスやAPIサーバを早急に構築できます。(HTMLを返すWebサーバももちろん、実装可能です)

早急といった言葉を使ったのは、Webアプリケーションを構築する際の

ルーティングリクエストパラメータの定義レスポンスパラメータの定義パラメータのバリデーションコントローラの定義
といった、ビジネスロジック以外の土台部分を自動で生成してくれて、
ビジネスロジックの実装に専念できるからです。

自動生成といっても、思い描いたものが勝手にできる訳ではなく、
goa独自のDSLを記述してアプリケーションの仕様を定義する(デザインする)必要があります。

goaではアプリケーションの枠組みを、design.go(デザインファイル)というAPIの定義書を作って生成していきます。

DSLを覚える学習コストはありますが、それに見合った以上の働きをしてくれます。

Webアプリケーションのフレームワークとして有名なRuby on Railsも、
お作法を知らないと、レールから外れた実装をする事になり複雑な実装になってしまいます。

が、Railsは世界中で使われてる有名なフレームワークとなっています。
その学習コスト以上に見合った以上の働きがあるからでしょう。

goaは、ビジネスロジックに没頭できる点において、Railsに匹敵できるほどポテンシャルを持っていると思います。
もちろん、RubyではなくGo言語なので、言語仕様上の違いがあり単純に比較はできません。

このブログでは、自分の学習結果のアウトプットとして、goaについての紹介を連載していきたいと思います。