goa API デザイン言語


goa API デザイン言語は Go で実装された DSL で、任意のマイクロサービス API を記述することができます。 デザイン言語は REST ベースの HTTP API を主な焦点にしていますが、他の方法論による API を記述するのにも十分柔軟です。 プラグインはコアDSLを拡張して、データベースモデル、サービスディスカバリ統合、障害ハンドラなどのマイクロサービスの他の側面を記述できるようにすることができます。

デザイン定義

デザイン言語の主要部は、定義 を記述するための関数の連鎖によって構成されています。 goa デザイン言語のルート定義は、API 定義です。DSLではこれを以下のように定義します:

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

var _ = API("My API", func() {        // "My API" is the name of the API used in docs
    Title("Documentation title")      // Documentation title
    Description("The next big thing") // Longer documentation description
    Host("goa.design")                // Host used by Swagger and clients
    Scheme("https")                   // HTTP scheme used by Swagger and clients
    BasePath("/api")                  // Base path to all API endpoints
    Consumes("application/json")      // Media types supported by the API
    Produces("application/json")      // Media types generated by the API
})

“dot import”をなぜ使うのかという質問がよく来るので以下を付け加えておきます: goa API デザイン言語はGoで実装されていますがGoではありません。 生成されるコードや実際に goa に含まれる Go のコードには”dot import”は使われていません。 このテクニックを DSL に使用するのは、はるかに洗練されたコードが得られるからです。 また、プラグインから来る DSL を透過的に混ぜることができます。

DSL ではさまざまな定義を再帰的に記述するために無名関数を頻繁に使用します。 上の例では、API 関数は、第1引数として関数の名前を受け取り、第2引数として無名関数を受け取ります。 ドキュメント内のこの無名関数もまたDSLであり、APIの追加のプロパティを定義しています。 この name+DSL のパターンは他の多くのDSL関数でも利用されています。

API 関数は API の一般的なプロパティを定義します:ドキュメント内のタイトルと説明、サービス利用規約(上の例では設定されていません)、ドキュメントやクライアントで使われるのデフォルトホスト、スキーマ、そしてすべてのエンドポイントのベースとなるパス(ベースパスをワイルドカードで定義して、キャプチャされたパラメータと対応させることもできます)

この関数は、API がサポートするメディアタイプも定義します。 Consumes 関数および Produces関数は、リクエスト(Consumes)およびレスポンス(Produces)のサポートメディアタイプを定義することもできます。 オプションで、生成されたコードがリクエスト・ペイロードを Unmarshal したりレスポンス・ボディを Marshal するように、使用するエンコーディングパッケージを指定することもできます。

追加のメタデータ(連絡先情報)から、セキュリティ定義、CORS ポリシー、レスポンステンプレートまで、API 関数で定義できるその他のプロパティがいくつかあります。 完全なリストについては、リファレンスを参照してください。

API エンドポイント

ルートAPI 定義とは別に、goa API 設計言語では、実際のエンドポイントをリクエストとレスポンスの詳細形式とともに記述することもできます。 Resource 関数は、関連する API エンドポイントのセットを定義します。 これは、API が RESTful である場合のリソースです。 実際の各エンドポイントは、Action 関数を使用して記述されます。 ここでは add アクションを(API エンドポイントとして)公開している単純な Operands リソースの例を示します。

var _ = Resource("Operands", func() {        // Defines the Operands resource
    Action("add", func() {                   // Defines the add action
        Routing(GET("/add/:left/:right"))    // The relative path to the add endpoint
        Description("add returns the sum of :left and :right in the response body")
        Params(func() {                      // Defines the request parameters
                                             // found in the URI (wildcards) and querystring
            Param("left", Integer, "Left operand")   // Defines left parameter as path segment
                                                     // captured by :left
            Param("right", Integer, "Right operand") // Define right parameter as path segment
                                                     // captured by :right
        })
        Response(OK, "plain/text")           // Defines response
    })
})

Resource 関数は任意の数のアクションを定義できます。 リソースは、共通ベースパス、共通パスパラメータ、およびすべてのアクションで共有されるその他のプロパティを定義できます。 アクションは、同じエンドポイントが異なるパスから要求されたり、異なる HTTP メソッドを使用したりする場合に備えて、複数のルートを定義できます(Routing 関数の引数は可変です)。

アクションパラメータを定義するために使用される DSL では、整数(integer)パラメータや数値(number)パラメータなら最小値と最大値、文字列パラメータでは正規表現で定義されたバリデーションを指定できます:

Param("left", Integer, "Left operand", func() {
    Minimum(0) // Do not allow for negative values.
})

パラメータを定義する構文は、下記のセクションで説明する Attribute DSL です。

ファイルサーバー

Files 関数を使用すると、リソース上にファイル・サーバーを定義することができます。 ファイルサーバーは、ルートの最後が*で始まるワイルドカードの場合、静的なファイルや指定されたファイルパスのすべてのファイルを提供します。 Files 関数は、オプションでセキュリティスキームを定義するための子 DSL を(無名関数を最後の引数として)受け入れます。 構文は、アクションのセキュリティスキームを定義するために使用される構文と同じです。

以下の例では、2つのファイルサーバーを持つ public リソースを定義しています。 ひとつは /swagger.json に送られたリクエストに対して public/swagger/swagger.json を返します。 もうひとつは、 js/*filepath に送られたリクエストに対して、public/js以下にあるすべてのファイルを返します。 ここで、 *filepath の値は public/js を基準としたファイルのパスに対応します。 swagger エンドポイントはまた、swagger 仕様を読み出す前に、クライアントに認証を要求するセキュリティスキームを定義しています。

var _ = Resource("public", func() {

    Origin("*", func() {        // CORS policy that applies to all actions and file servers
        Methods("GET")          // of "public" resource
    })

    Files("/swagger.json", "public/swagger/swagger.json", func() {
        Security("basic_auth")  // Security scheme implemented by /swagger.json endpoint
    })

    Files("/js/*filepath", "public/js/") // Serve all files under the public/js directory
})

データタイプ

goa API デザイン言語はリクエスト・ペイロードやレスポンス・メディアで定義される任意のデータタイプを記述することが可能です。 Type 関数は、Attribute 関数を利用して各フィールドを並べることでデータ構造を記述します。 またArrayOf 関数を利用して配列や配列のフィールドを定義することができます。 以下に例を示します。

// Operand describes a single operand with a name and an integer value.
var Operand = Type("Operand", func() {
    Attribute("name", String, "Operand name", func() { // Attribute name of type string
        Pattern("^x")                                  // with regex validation
    })
    Attribute("value", Integer, "Operand value")  // Attribute value of type integer
    Required("value")                             // only value is required
})

// Series represents an array of operands.
var Series = ArrayOf(Operand)

API 関数のように、Type 関数は2つの引数、名前とタイプのプロパティを記述する DSL、を受け取ります。 Type DSLは、次の3つの関数で構成されています。

  • Description はタイプの説明を指定します。
  • Attribute はタイプフィールドをひとつ定義します。
  • Required は必須のフィールドをリストします。フィールドは必ずそのタイプのインスタンスで存在しなければなりません。

Type はアクション・ペイロードを定義するのに利用できます:

Action("sum", func() {          // Defines the sum action
    Routing(POST("/sum"))       // The relative path to the add endpoint
    Description("sum returns the sum of all the operands in the response body")
    Payload(Series)             // Defines the action request body shape using the Series
                                // type defined above.
    Response(OK, "plain/text")  // Defines a response
})

Attribute

Attribute は、goa DSL において特別な役割を果たします。 これら Attribute は、データ構造を定義するために使用される基礎となります。 Attribute DSL は、基本的にデータ構造の定義を必要とする場所であれば、タイプ・フィールド、リクエスト・パラメータ、リクエスト・ペイロード、レスポンス・ヘッダ、リクエスト・ボディなどを記述するために使用されます。 Attribute を定義するための構文は非常に柔軟性があり、必要に応じて、少なくにも沢山にも指定できます。 完全な定義は次のとおりです:

Attribute(<name>, <type>, <description>, <dsl>)

最初の引数だけが必須で、他のすべての引数はオプションです。 デフォルトの Attribute タイプは String です。 Attribute で指定可能なタイプは以下です。

Name Go equivalent JSON equivalent
Boolean bool “true” or “false”
Integer int number
Number float number
String string string
DateTime time.Time RFC3339 string
UUID uuid.UUID RFC4122 string
Any interface{} ?

さらに、タイプ・フィールドは、ArrayOf または HashOf を使用するか、再帰的DSLを使用して定義できます。

var User = Type("user", func() {
    Description("A user of the API")
    Attribute("name")                 // Simple string attribute
    Attribute("address", func() {     // Nested definition, defines a struct in Go
        Attribute("number", Integer, "Street number")
        Attribute("street", String, "Street name")
        Attribute("city", String, "City")
        Required("city")              // The address must contain at least a city
    })
    Attribute("friends", ArrayOf("user"))
    Attribute("data", HashOf(String, String))
})

friends フィールドを定義するのに User という変数を参照する代わりに、"user" という名前を使っていることに注意してください。 どちらの構文も受理されます。 変数参照の代わりに名前を使用すると、再帰的定義を構築できます。 Github の examples リポジトリの types ディレクトリにはタイプの設計と生成されるコード間の対応を示す数多くの例があります。

レスポンス

レスポンス・メディアタイプ

次にレスポンスを見ると、goa デザイン言語の MediaType 関数は、レスポンス・ボディの形状を表すメディアタイプを記述します。 メディアタイプの定義はタイプの定義と似ています(メディアタイプはタイプの特殊な種類です)が、メディアタイプには2つの固有のプロパティがあります:

  • View は、同じメディアタイプの異なるレンダリングを記述することを可能にします。 多くの場合、API はリスト化されたリソースを返すリクエストでは「短い」表現を使用し、単一のリソースを返すリクエストではより詳細な表現を使用します。 View は、これらの異なる表現を定義する方法を提供することによって、ユースケースをカバーします。 メディアタイプ定義は、リソースをレンダリングするために使用されるデフォルトのビューを定義しなければなりません(適切に default と名前が付けられます)。

  • Link は、レスポンスに埋め込んでレンダリングされる関連するメディアタイプを表します。 リンクをレンダリングするために使われるビューは link です。これはリンクされるメディアタイプが、必ず link ビューを定義していなければならないということです。 リンクは、メディアタイプ定義において、Links 関数の配下にリストアップされます。 ビューは特殊な Links 関数を用いてすべてのリンクをレンダリングすることができます。

メディアタイプ定義の例を以下に示します:

// Results is the media type that defines the shape of the "add" action response.
var Results = MediaType("vnd.application/goa.results", func() {
    Description("The results of an operation")
    Attributes(func() {                              // Defines the media type attributes
        Attribute("value", Integer, "Results value") // Operation results attribute
        Attribute("requester", User)                 // Requester attribute
    })
    Links(func() {             // Defines the links embedded in the media type
        Link("requester")      // One link to the requester, will be rendered
                               // using the "link" view of User media type.
    })
    View("default", func() {   // Defines the default view
        Attribute("value")     // Includes the "value" field in the default view
        Links()                // And render links
    })
    View("extended", func() {  // Defines the extended view
        Attribute("value")     // Includes the value field
        Attribute("requester") // and the requester field
    })
})

// User is the media type used to render user resources.
var User = MediaType("vnd.application/goa.users", func() {
    Description("A user of the API")
    Attributes(func() {
        Attribute("id", UUID, "Unique identifier")
        Attribute("href", String, "User API href")
        Attribute("email", String, "User email", func() {
            Format("email")
        })
    })
    View("default", func() {
        Attribute("id")
        Attribute("href")
        Attribute("email")
    })
    View("link", func() { // The view used to render links to User media types.
        Attribute("href") // Here only the href attribute is rendered in links.
    })
})

レスポンス定義

Response 関数は、特定の潜在的なレスポンスを定義するためにアクション宣言内で利用されます。 レスポンスに本文とヘッダが含まれる場合には、レスポンス・ステータスコード、メディアタイプが記述されます。 それぞれのレスポンスは、他のほとんどの DSL 関数で名前が最初の引数となっているのと同様に、最初の引数にアクションのスコープ内で一意の名前を持つ必要があります。 以下の DSL は “NoContent” と名前づけられた HTTP ステータス 204 のレスポンスを定義しています:

Response("NoContent", func() {
    Description("This is the response returned in case of success")
    Status(204)
})

この例およびこのセクションの他のすべての例では、レスポンス・テンプレートを使用しないため、名前を含むレスポンスのすべてのプロパティを定義していることに注意してください。 実際には、ほとんどの場合、組み込みテンプレートの1つを使用してレスポンスが定義されます。 たとえば、上記のレスポンス(から Description を抜いたもの)を短縮して以下のようにすることができます:

Response(NoContent)

レスポンステンプレートについては、以下のセクションで詳しく説明しますが、レスポンステンプレートをカバーする前にまず Response の仕組みを理解する必要があります。

レスポンスにボディが含まれている場合、対応するメディアタイプはMedia 関数を使用して指定されます。 この関数は、メディアタイプ識別子または実際のメディアタイプの値を第1引数として受け取り、オプションでレスポンス・ボディのレンダリングに使用されるメディアタイプビューの名前を受け取ります。 ビューはオプションです。 たとえば、レスポンスのステータスなどに応じて、同じアクションでも異なるビューが返される場合があります。 ここではステータス・コード 200 で Results メディアタイプを持つ “OK” のレスポンス定義の例を示します:

Response("OK", func() {
    Description("This is the success response")
    Status(200)
    Media(Results)
})

便宜上、レスポンスのメディアタイプを Response 関数の第2引数として提供することができます(これは、以下の対応するセクションで説明する応答テンプレートを使用する場合に特に便利です)。 したがって、上記は次のようになります。

Response("OK", Results, func() {
    Description("This is the success response")
    Status(200)
})

Results の識別子を application/vnd.example.results とすると、上記は次と同等です:

Response("OK", "application/vnd.example.results", func() {
    Description("This is the success response")
    Status(200)
})

メディアタイプ識別子(上記の例では application/vnd.example.results)は、MediaType 関数を介して定義されたメディアタイプの識別子に対応しても、そうでなくてもよいことに注意してください。 生成されたコードは、メディアタイプ識別子がデザインで定義されたメディアタイプと一致しない場合には、Go の型 []byte を使用してレスポンス・ボディのタイプを定義します。

もし親アクションが常にデフォルト・ビューを返すなら、次のようにレスポンスを定義することができます:

Response("OK", func() {
    Description("This is the success response")
    Status(200)
    Media(Results, "default")
})

レスポンス・ヘッダーは、Headers 関数を用いて定義されます。 各ヘッダーを定義する構文は、Attribute を定義するのに使用される構文と同じです。

Response("OK", func() {
    Status(200)
    Media(Results, "default")
    Headers(func() {
        Header("Location", String, "Resource location", func() {
            Pattern("/results/[0-9]+")
        })
        Header("ETag") // assumes String type as with Attribute
    })
})

リソース vs. アクション・レスポンス

レスポンスは、Resource 定義の一部として、または Action 定義の一部として、2つの場所で定義できます。 Resource定義で定義されたレスポンスは、すべてのリソース・アクションに適用されます。

この例では、すべての Operands アクションはUnauthorized レスポンスを返します:

var _ = Resource("Operands", func() {
    Response("Unauthorized", func() {
        Description("Response sent for unauthorized requests")
        Status(401)
    })
    Action("add", func() {
        Routing(GET("/add/:left/:right"))
        Params(func() {
            Param("left", Integer, "Left operand")
            Param("right", Integer, "Right operand")
        })
        Response("OK", Results)
        // Response "Unauthorized" is implicit
    })
})

デフォルト・メディアタイプの活用

リソースはすべてのアクションに対して、デフォルトのメディアタイプを定義することができます。 デフォルトのメディアタイプを定義すると2つの効果があります:

  1. デフォルトのメディアタイプは、ステータスコード 200 を返しメディアタイプを定義しないすべてのレスポンスに使用されます。
  2. アクション・ペイロード、アクション・パラメータ、およびレスポンス・メディアタイプで定義された Attribute がデフォルト・メディアタイプで定義した Attrubite と名前が一致するものは、自動的にすべての Attribute のプロパティ(説明、タイプ、バリデーションなど)を継承します。

上記で定義した Results メディアタイプをデフォルトのメディアタイプとして使用し、add アクションの OK レスポンスの定義に活用されているのを、次のリソース定義で見てみましょう:

var _ = Resource("Operands", func() {
    DefaultMedia(Results)
    Action("add", func() {
        Routing(GET("/add/:left/:right"))
        Params(func() {
            Param("left", Integer, "Left operand")
            Param("right", Integer, "Right operand")
        })
        Response(OK)         // Uses the resource default media type
    })
})

今度は、 Results メディアタイプが合計を計算するために使用された最初に与えられた値を返したとしましょう:

var Results = MediaType("vnd.application/goa.results", func() {
    Description("The results of an operation")
    Attributes(func() {
        Attribute("left", Integer, "Left operand")
        Attribute("right", Integer, "Right operand")
        Attribute("value", Integer, "Results value")
        Attribute("requester", User)
    })
    View("default", func() {
        Attribute("left")
        Attribute("right")
        Attribute("value")
        Attribute("requester")
    })
})

add アクションの定義は、leftright のタイプとコメントを繰り返さずにすませることができます:

var _ = Resource("Operands", func() {
    DefaultMedia(Results)
    Action("add", func() {
        Routing(GET("/add/:left/:right"))
        Params(func() {
            Param("left")    // Inherits type and description from default media type
            Param("right")   // Inherits type and description from default media type
        })
        Response(OK)         // Uses the resource default media type
    })
})

これらの値の定義が変化しても、変更点はただ1箇所ですみます。

レスポンス・テンプレート

goa API デザイン言語では、すべてのアクションで活用できるようになレスポンス定義を API レベルで レスポンステンプレート として定義できるようになっています。 そのようなテンプレートは、レスポンス・プロパティのいずれかを定義するために任意の数の文字列引数を受け付けることができます。 テンプレートは、次の構文で定義されます:

var _ = API("My API", func() {
    ResponseTemplate("created", func(hrefPattern string) { // Defines the "created" template
        Description("Resource created")                    // that takes one argument "hrefPattern"
        Status(201)                     // using status code 201
        Header("Location", func() {     // and contains the "Location" header
            Pattern(hrefPattern)        // with a regex validation defined by the
                                        // value of the "hrefPattern" argument.
        })
    })
})

テンプレートは、レスポンスを定義するときに単に名前で参照するだけで使用されます:

Action("sum", func() {
    Routing(POST("/accounts"))
    Payload(AccountPayload)
    Response("created", "^/results/[0-9]+") // Response uses the "created" response template.
})

goa はすべての標準的な HTTP ステータス・コードに対するレスポンス・テンプレートを提供しているため、単純なケースのテンプレートを定義する必要はありません。ビルトイン・テンプレートの名前は、対応するHTTPステータスコードの名前と一致します。 例えば:

Action("show", func() {
    Routing(GET("/:id"))
    Response("ok", func() {
        Status(200)
        Media(AccountMedia)
    })
})

は次と同じです:

Action("show", func() {
    Routing(GET("/:id"))
    Response(OK, AccountMedia) // Uses the built-in "OK" response template that defines status 200
})

まとめ

デザイン言語にはさらに多くのものがありますが、この概要では、デザイン言語がどのように動作するかについてのひとつの感覚を与えているはずです。 言語が自然に感じられるので、設計をすばやく反復して洗練することが可能になります。 設計から生成された Swagger 仕様を、ステークホルダーと共有してフィードバックを収集し、反復することができます。 完成したら goagen は API スキャフォールディングやリクエスト・コンテキスト、バリデーション コードをデザインから生成し、それを実装に組み込みます。 デザインは、実装に関して常に最新の生きたドキュメントになります。