スキーマ定義言語 Protocol Buffers と protoc-gen-swagger を使って Web API のスキマを埋めよう

VOYAGE Lighthouse Studio の海老原 (@co3k) です。先日 30 歳になった記念としてタイトルはオヤジギャグです。

さて、普段は 神ゲー攻略 というゲーム攻略サイトを運営しているのですが、とある派生サービスを立ち上げるにあたり、 Web API スキーマ定義を gRPC に基づく形式の Protocol Buffers で書き、 protoc-gen-swagger プラグインを介して OpenAPI 定義ファイルとして生成する、というアプローチを採りました。

yugui さんの素晴らしい記事、「今さらProtocol Buffersと、手に馴染む道具の話」によってスキーマ定義言語としての Protocol Buffers がにわかに注目を浴びて以降、似たようなことをやりたいという方もいらっしゃるのではないでしょうか。

ところが、おそらく単体で protoc-gen-swagger プラグインを使う人はまだまだ限られているようで、

  • 機能が充分でなかったり、不安定であったりする
    • 複数スキーマのマージやエラーレスポンスの定義が割と最近サポートされた
    • 「デフォルト認証状態のリセット」が最近までできなかったうえに segfault で落ちていた
  • ドキュメントが不足している

という具合に、ちょっとまだ導入にあたってハードルが高めなのが正直なところです。

ただ、悪い話ばかりでもありません。ここでひとつ朗報なのですが、 2018/09/09 にリリースされた v1.5.0 では、前者の問題を解決するための変更が多く取り込まれています。

そのうちいくつかは、海老原がサービス開発中に必要となったものを修正し、取り込んでもらったものです。以下がその pull request です。

また、もちろん、他の方がおこなっておられる変更にも、 protoc-gen-swagger 単体で用いる際に有用となるものがあります。たとえば以下のようなものです。

しかしドキュメントは相変わらずありません!

そういったわけで、本エントリではこの選択をした理由のご紹介と、 v1.5.0 の新機能なども含む protoc-gen-swagger の使い方について簡単に説明していきます。同じようなアプローチを取る方の一助になれば幸いです。

TL;DR

読者の便宜のために、本エントリでご紹介する .proto ファイルと .swagger.json ファイルの例、および生成のための Makefile をまとめたリポジトリをご用意しました。ぜひご活用ください。 https://github.com/co3k/protobuf-swagger-example/

このアプローチを採った理由

  • JSON Hyper-Schema を書くのは (JSON を書かなかったとしても) 正直つらい
  • REST API をやっていくならエコシステム的に Swagger (と OpenAPI) 1 一強といった状況である
  • OpenAPI 仕様ではスキーマ部の定義に JSON Schema を記述することになるが、これも正直つらい
  • あっ、ちょうどいい感じのスキーマ定義言語として Protocol Buffers があるじゃん!
  • しかも protoc が思ったよりも強力じゃん! 知らなかった!
  • しかもしかも grpc-gateway をよく見たら protoc-gen-swagger なるプラグインがあるじゃん!

そもそも海老原は 4 年ほど前の LT、「JSON Schema で Web API のスキマを埋めよう」で触れているように (タイトルはダジャレです)、JSON Hyper-Schema による Web API スキーマ定義をこれまでずっと続けていました。これは JSON Hyper-Schema そのものにアドバンテージを感じていたというより、何でもいいので何らかのスキーマ定義を必要としていたことと、必要に迫られて contribute もした Heroku 製の prmd というドキュメント生成ツールを気に入っていたから、という側面が強いです。

正直 JSON Hyper-Schema や JSON Schema が気に入っていたかというとそんなことはなくて、かなり無理して書いていた感が強いです。もちろん JSON は human-writable ではないので YAML で書くわけですが、 JSON も YAML も汎用的なフォーマットであるがゆえに、どうしても持ってまわった書き方にならざるを得ないところがあります。

百聞は一見にしかずということで、 VOYAGE GROUP のバースペース AJITO の T シャツ裏に印字された JSON のスキーマ定義を考えてみましょう。印字された JSON は以下に 2 示す 3 とおり 4 です。 AJITO で過ごすことを ajiting と呼んでいるのですが、その ajiting とはどういったものか、というのを紹介するような内容となっています。

{ "#ajiting": {
    "description": "coding, discussion and other.",
    "url": "http://ajito.vg",
    "location": {
      "longitude": "35.6553195",
      "latitude": "139.6937795"
    },
    "beer": "free"
  }
}

この JSON 表現のスキーマを JSON Scheme によって表現してみると、たとえば以下のようになるでしょうか。

{
  "type": "object",
  "properties": {
    "#ajiting": {
      "type": "object",
      "properties" : {
        "description": {
          "type": "string"
        },
        "url": {
          "type": "string"
        }
        "location": {
          "type": "object",
          "properties": {
            "longitude": "string",
            "latitude": "string"
          }
        },
        "beer": "string"
      }
    }
  }
}

対して、 Protocol Buffers の場合はそもそもスキーマ定義を目的として作られたフォーマットであるため、簡潔な記述で済みます。

syntax = "proto3";

message Ajiting {
  message GeoCoordinate {
    string latitude = 1;
    string longitude = 2;
  }

  string description = 1;
  string url = 2;
  GeoCoordinate location = 3;
  string beer = 4;
}

message AjitingResponse {
  Ajiting ajiting_message = 1 [json_name = "#ajiting"];
}

さっそく Web API スキーマ定義を書いてみよう!

前置きはここまでとして、さっそく WebAPI のスキーマ定義を書いてみましょう。

ここで必要となってくるのが Protocol Buffers に関する以下のような知識です。

  • message の定義に関する知識
  • service の定義に関する知識
  • option に関する知識

これらについて理解しておけば、簡単な Web API 定義を書くには充分です。順番に見ていきましょう。

message の定義に関する知識

さて、リクエストやレスポンスなどを表現する message の記法については既に示したとおりです。詳しくは Language Guide を通読していただくとして、肝心なところだけ拾って説明していきます。

まずは = 1 などの謎の代入文についてですが、これは「タグ」と呼ばれるもので、フィールドを一意に識別するための番号です。が、最終的に JSON シリアライズするうえでは重要でない概念なので、とにかく 1 から順番に機械的につけていけばよい、と覚えておいてください。

また、 message は入れ子にすることができます。以下のような形です。

message Ajiting {
  message GeoCoordinate {
    string latitude = 1;
    string longitude = 2;
  }

  GeoCoordinate location = 3;
}

無名 message のようなものを定義したくなるところですがそれはできません。これでも JSON Schema に比べればまだシンプルなので、ここは名前付けの機会をもらえたと思ってグッとこらえましょう 5

それから忘れてはならないのは配列表現でしょうか。 AJITO T シャツに印字された JSON には以下のようなプロパティが存在します。

"beer": "free"

しかしこれは実に遠慮がちな表現で、 ajiting において free なのは beer だけではありません。せっかくなので Protocol Buffers における配列表現を用いつつ実態に合わせて修正してみましょう。

Protocol Buffers には repeated フィールドがあります。これは JSON シリアライズした場合に配列表現となります。

というわけで Ajiting の定義から beer を取り除き、文字列値の配列を表す以下の記述で置き換えます。

repeated string free = 5;

これによって、以下のような表現が合法となりました。よかったですね 6

"free": [
  "beer", "cocktail", "non-alcoholic cocktail", "juice",
  "talking", "coding", "playing instruments"
]

Web APi スキーマ定義用途であれば、 message について知っておくべきことはこれだけです。あとは 基本的な型 を確認しながら書いていきましょう。

service の定義に関する知識

続いて service の定義についてです。これは RPC 7 におけるメソッド定義の集合です……という説明より、実際にやってみたほうが多分早いですね。

ではさっそく何か service とメソッドを定義してみましょう。「ajiting とは心の所作」とはよく言ったもので、人にとって様々な ajiting があります。議論の場としての ajiting、作業スペースとしての ajiting、娯楽の場としての ajiting、 AJITO 以外での ajiting――ということで、全世界のいろいろな ajiting を一覧するメソッドがあれば便利そうですね。

まずは空の service を定義します。

service AjitingService {
}

次に、この service にメソッドを定義します。

service AjitingService {
  rpc ListAjiting(ListAjitingRequest) returns (ListAjitingResponse);
}

rpc に続く文字列がメソッド名です。続く括弧の中身がリクエストとなる message で、 returns に後続する括弧の中身がレスポンスとなる message です。もちろん ListAjitingRequest も ListAjitingResponse もまだ定義していないのでここで一緒に定義してしまいましょう。

まずリクエストについてですが、条件に合った ajiting の一覧が取得できたら便利そうではないでしょうか。ということで、検索クエリを指定できるような感じのメッセージを考えてみます。

message ListAjitingRequest {
  string query = 1;
}

レスポンスは Ajiting の配列と、あとは全件数でも返しておきましょうか。

message ListAjitingResponse {
  int32 num = 1;
  repeated Ajiting ajitings = 2;
}

基本的な service 定義についてはここまでの知識で充分です。

ちなみに service をどういう単位で作っていくか、というところですが、いろいろ試してみた感じ REST でいうところのリソース単位で細かく区切っていくと収まりが良さそうでした。

それからメソッドの命名については Google Cloud APIs の API Design Guide 内 Standard Methods の命名規則にとりあえず従ってつけています。この辺はお好みですが、いずれにせよある程度の一貫性があるといいですね。

option と google.api.http に関する知識

ここまでの知識だけではまだ Web API スキーマ定義を作ることはできません。ほとんどの場合、 message については特別な考慮をすることなく JSON にシリアライズ可能ですが、 Protocol Buffers でいうところの service と、 REST の文脈でいうところのリソースとメソッドという概念が結びついていません。

そうは言っても、もちろん、 service の概念を REST で表現したいというのは Protocol Buffers そのものがカバーする領域ではありません。こういうときに活躍するのが option です。これは端的にいうとファイル、 message やそのフィールド、 service やそのメソッドなどに対して任意のメタ情報を付加できる仕組みです。この仕組みの存在が、 Protocol Buffers をシンプルでありながらもパワフルな武器として成立させています。

protoc-gen-swagger (と、 grpc-gateway) は google.api.http 型のメッセージをメソッドに対する option として解釈できます。

まずはこのメッセージの定義をファイルの先頭あたりで import します。

import "google/api/annotations.proto";

そのうえで、たとえば先程の ListAjiting(ListAjitingRequest) に対して GET /v1/ajiting をマップするのであれば、以下のようにします。

rpc ListAjiting(ListAjitingRequest) returns (ListAjitingResponse) {
  option (google.api.http).get = "/v1/ajiting";
}

ListAjitingRequest には query というフィールドがありますが、デフォルトではすべてのフィールドはクエリパラメータとして扱われます。

もしパスパラメータとして扱いたい場合は、 (google.api.http).get の文字列に URI Template でお馴染みの形式で含んであげればよいです。パスパラメータに記述したフィールドは、クエリパラメータとして扱われなくなります。

rpc ListAjiting(ListAjitingRequest) returns (ListAjitingResponse) {
  option (google.api.http).get = "/v1/ajiting/{query}";
}

GET 以外のメソッドの場合も見てみましょう。既存の ajiting を PUT で編集する以下のメソッドを考えます。

rpc UpdateAjiting(UpdateAjitingRequest) returns (AjitingResponse);

UpdateAjitingRequest の定義は以下のようになるでしょうか。

message UpdateAjitingRequest {
  int32 id = 1;
  string description = 2;
  string url = 3;
  Ajiting.GeoCoordinate location = 4;
  repeated string free = 5;
}

では REST における PUT メソッドの定義を書いていきます。

rpc UpdateAjiting(UpdateAjitingRequest) returns (AjitingResponse) {
  option (google.api.http) = {
    put: "/v1/ajiting/{id}"
    body: "*"
  }
}

GET のときは違い、パスパラメータに使われなかったフィールドは、そのままではリクエストボディとして扱われません。そのため、残りのフィールドをすべてリクエストボディに含むよう、 body: "*" を指定しています。この指定がない場合はリクエストボディが空であるとみなされます。

ここで、 * 以外を指定することもできます。 UpdateAjitingRequest を以下のように変えて、 Ajiting を再利用するようにしてみましょう。

message UpdateAjitingRequest {
  int32 id = 1;
  Ajiting ajiting = 2;
}

こうしておけば、以下のように書けます。

rpc UpdateAjiting(UpdateAjitingRequest) returns (AjitingResponse) {
  option (google.api.http) = {
    put: "/v1/ajiting/{id}"
    body: "ajiting"
  };
}

OpenAPI 定義ファイルの生成

ここまでで Web API を表す Protocol Buffers 定義が出来上がりました。このファイルを ajiting.proto として保存しておきましょう。

syntax = "proto3";

import "google/api/annotations.proto";

message Ajiting {
  message GeoCoordinate {
    string latitude = 1;
    string longitude = 2;
  }

  string description = 1;
  string url = 2;
  GeoCoordinate location = 3;
  repeated string free = 5;
}

message AjitingResponse {
  Ajiting ajiting_message = 1;
}

message ListAjitingRequest {
  string query = 1;
}

message ListAjitingResponse {
  int32 num = 1;
  repeated Ajiting ajitings = 2;
}

message UpdateAjitingRequest {
  int32 id = 1;
  Ajiting ajiting = 2;
}

service AjitingService {
  rpc ListAjiting(ListAjitingRequest) returns (ListAjitingResponse) {
    option (google.api.http).get = "/v1/ajiting";
  }

  rpc UpdateAjiting(UpdateAjitingRequest) returns (AjitingResponse) {
    option (google.api.http) = {
      put: "/v1/ajiting/{id}"
      body: "ajiting"
    };
  }
}

ではさっそく protoc-gen-swagger で OpenAPI 定義ファイルを生成します。

まず Protocol Buffers (と protoc) をインストール したうえで、

$ go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger

によって protoc-gen-swagger を入手します。

あとは以下のコマンドを叩くだけ 8

$ protoc -I. -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/ --swagger_out=json_names_for_fields=true:. ajiting.proto

これで ajiting.swagger.json が生成されます。できあがったものは https://github.com/co3k/protobuf-swagger-example/blob/ce7a439ef0c692388549d3e18ab796bb3f46d5e7/01-ajiting.swagger.json に置いたので、 https://editor.swagger.io/ などでご確認ください。なかなかいい感じではないでしょうか?

ちなみに、 v1.4.1 から、複数の .proto ファイル定義を単一の .swagger.json ファイルにまとめられるようになりました。以下のように allow_merge パラメータに true を指定するだけです。便利!

$ protoc -I. -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/ --swagger_out=json_names_for_fields=true,allow_merge=true:. *.proto

OpenAPI 特有の設定をガッツリ書いていこう!

最小限のスキーマ定義はもうここまでで充分なわけですが、 Swagger のエコシステムをフル活用しようとすると、まだ物足りないところもあります。認証関連の設定であったり、ドキュメント生成やバリデーションなどですね。 protoc-gen-swagger はこのあたりもバッチリサポートしています。

そういった場合に必要な OpenAPI 特有の設定を Protocol Buffers 上で表現するのに、 option が大活躍するわけです。 protoc-gen-swagger が細かい設定類を定義するためのメッセージ群を用意してくれている ので、これを使っていきましょう 9

Swagger オブジェクト (ルートオブジェクト) の定義

一番外側のスコープで option を記述すると、ファイルレベルのメタ情報を付加することができます。

protoc-gen-swagger が提供する Swagger メッセージ (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) によって、 OpenAPI 仕様のルートオブジェクトである Swagger オブジェクトへの拡張をおこなうことができます。

まずは必要なファイルを import して、

import "protoc-gen-swagger/options/annotations.proto";

以下のように記述します。

option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = {
    swagger: "2.0";
    info: {
        title: "ajiting-api";
        description: "ajiting 用の Web API です。";
        version: "1.0";
    }
    host: "api.ajiting.example.com";
    schemes: HTTPS;
    security_definitions: {
        security: {
            key: "OAuth2";
            value: {
                type: TYPE_OAUTH2;
                flow: FLOW_ACCESS_CODE;
                authorization_url: "https://ajiting.example.com/oauth/authorize";
                token_url: "https://ajiting.example.com/oauth/token";
            }
        }
    }
    security: {
        security_requirement: {
            key: "OAuth2";
            value: {
            };
        }
    }
    responses: {
      key: "403";
      value: {
        description: "リソースへのアクセス権がない場合のレスポンスです。";
      }
    }
    responses: {
      key: "404";
      value: {
        description: "リソースが見つからなかったときのレスポンスです。";
      }
    }
};

はい、見てのとおり OpenAPI の Swagger オブジェクトをほぼそのまま Protocol Buffers として定義し直したような形になっているので、詳しいことは https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#swagger-object を見ながら設定していただければよろしいかと思います。

細かい注意点としては、

  • Protocol Buffers 側のキーはスネークケースで書く必要があります
  • 特定の値しか許容しない schemessecurity_definitionstype などは enum が定義されていて、その値を使っていく形になります。このあたりのドキュメントは存在しないので、 openapiv2.proto を見ながらどのような型を受け容れるのかを確認していく必要があります
  • reserverd キーワードで予約されているフィールド (Swagger オブジェクトであれば paths, definitions, tags) には未対応です

といったあたりでしょうか。

Operation オブジェクトの定義

OpenAPI における Operation は Protocol Buffers のメソッドに対応します。メソッドに対して grpc.gateway.protoc_gen_swagger.options.openapiv2_operation の option を設定することで、この Operation を拡張できます。以下は先程定義した AjitingService の ListAjiting の認証設定を上書き (ファイルレベルでは OAuth2 を強制するが、 ListAjiting は認証なしでアクセスできるようにする) してみましょう。

service AjitingService {
  // みんなの #ajiting を一覧する
  //
  // みんながそれぞれに思う #ajiting を一覧します。
  // query が指定されている場合はその条件に従った #ajiting を絞り込みます。
  rpc ListAjiting(ListAjitingRequest) returns (ListAjitingResponse) {
    option (google.api.http).get = "/v1/ajiting";
    option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = {
      security: {};  // ここでデフォルトの認証設定を上書きする
    };
  }
}

OpenAPI において、 Operation の security は Security Requirement Object の配列です。ファイルレベルで定義した認証設定とは別な認証設定を渡すことができるわけですが、ここで空配列を指定することで、認証設定を取り除く、つまり認証なしでのアクセスが許可されるようになります。

また、メソッドに対するコメントは、一行目が OpenAPI における Operation の summary として、空行を挟んでそれ以降の文字列が description として扱われます。最終的に OpenAPI 定義からドキュメント等を生成したい場合などに有用でしょう。

Schema オブジェクトの定義

OpenAPI における Schema は Protocol Buffers の message に対応します。こちらも grpc.gateway.protoc_gen_swagger.options.openapiv2_schema によって拡張可能です。

// 緯度経度情報
//
// 世界測地系 (WGS84) における緯度経度情報を表します。
// 日本測地系 2000 や 日本測地系 2011 などの他の測地系の値は受け容れませんので、
// 事前に変換をおこなっておく必要があります。
message GeoCoordinate {
  option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = {
    json_schema: {
      required: ["latitude", "longitude"]
    }
  };

  string latitude = 1;
  string longitude = 2;
}

ここでは、 json_schema によって JSON Schema を定義できます。なんだか本末転倒な気もしますが、いまのところ細かいバリデーションや required については JSON Schema 経由で定義していくしかありません。

message に対するコメントは、メソッドに対するコメントと同様に、 OpenAPI 定義における Schema の title ないし description として扱われます。

フィールドに対する JSON Schema Validation 定義を追加する

OpenAPI 定義上で利用できる JSON Schema Validation は、たとえば文字列のパターンを制限したい場合や、文字数制限をおこなう場合などに有用でした。

これを Protocol Buffers 上でも定義しましょう。フィールドに対するオプション grpc.gateway.protoc_gen_swagger.options.openapiv2_field を用いることで、以下のように表現できます。

string latitude = 1 [(grpc.gateway.protoc_gen_swagger.options.openapiv2_field) = {pattern: "^[\\-]?[0-9]{0,2}\\.[0-9]+$"}];
string longitude = 2 [(grpc.gateway.protoc_gen_swagger.options.openapiv2_field) = {pattern: "^[\\-]?[0-9]{0,3}\\.[0-9]+$"}];

特定のステータスコードのレスポンスを定義する

Swagger オブジェクトや Operation オブジェクトは responses を受け容れます。これによって特定のステータスコードのレスポンスを定義することができます。 Swagger オブジェクトに対する例を再掲します。

option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = {
    responses: {
      key: "403";
      value: {
        description: "リソースへのアクセス権がない場合のレスポンスです。";
      }
    }
    responses: {
      key: "404";
      value: {
        description: "リソースが見つからなかったときのレスポンスです。";
      }
    }
};

この例は説明を付加しただけでレスポンス自体の定義はおこなっていません。 RFC7807 に従ったエラーレスポンスを以下のように Protocol Buffers で定義します。

message ErrorResponse {
  string type = 1;
  int32 status = 2;
  string title = 3;
  string detail = 4;
  string instance = 5;
}

JSON Schema からはこれを以下のように参照できます。

responses: {
  key: "403";
  value: {
    description: "リソースへのアクセス権がない場合のレスポンスです。";
    schema: {
      json_schema: {
        ref: ".ErrorResponse";
      }
    }
  }
}

ガッツリ書いた結果を生成してみよう!

ということで、最終的な .proto ファイルは以下のような形になりました。実際には Swagger オブジェクトの定義や ErrorResponse などは独立した .proto に分けたいところですね。

syntax = "proto3";

import "google/api/annotations.proto";
import "protoc-gen-swagger/options/annotations.proto";

option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = {
    info: {
        title: "ajiting-api";
        description: "ajiting 用の Web API です。";
        version: "1.0";
    }
    host: "api.ajiting.example.com";
    schemes: HTTPS;
    security_definitions: {
        security: {
            key: "OAuth2";
            value: {
                type: TYPE_OAUTH2;
                flow: FLOW_ACCESS_CODE;
                authorization_url: "https://ajiting.example.com/oauth/authorize";
                token_url: "https://ajiting.example.com/oauth/token";
            }
        }
    }
    security: {
        security_requirement: {
            key: "OAuth2";
            value: {
            };
        }
    }
    responses: {
      key: "403";
      value: {
        description: "リソースへのアクセス権がない場合のレスポンスです。";
        schema: {
          json_schema: {
            ref: ".ErrorResponse";
          }
        }
      }
    }
    responses: {
      key: "404";
      value: {
        description: "リソースが見つからなかったときのレスポンスです。";
        schema: {
          json_schema: {
            ref: ".ErrorResponse";
          }
        }
      }
    }
};

// エラーレスポンスオブジェクト
//
// [RFC7807](https://tools.ietf.org/html/rfc7807) に従ってエラーの内容を表します。
message ErrorResponse {
  string type = 1;
  int32 status = 2;
  string title = 3;
  string detail = 4;
  string instance = 5;
}

// ajiting を表現するオブジェクト
//
// ajiting の緯度経度情報や、どのような ajiting がおこなわれるか、何が free なのかを表します。
message Ajiting {
  // 緯度経度情報
  //
  // 世界測地系 (WGS84) における緯度経度情報を表します。
  // 日本測地系 2000 や 日本測地系 2011 などの他の測地系の値は受け容れませんので、
  // 事前に変換をおこなっておく必要があります。
  message GeoCoordinate {
    option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = {
      json_schema: {
        required: ["latitude", "longitude"]
      }
    };

    // 緯度
    //
    // 世界測地系 (WGS84) における緯度情報を表します。
    string latitude = 1 [(grpc.gateway.protoc_gen_swagger.options.openapiv2_field) = {pattern: "^[\\-]?[0-9]{0,2}\\.[0-9]+$"}];

    // 経度
    //
    // 世界測地系 (WGS84) における経度情報を表します。
    string longitude = 2 [(grpc.gateway.protoc_gen_swagger.options.openapiv2_field) = {pattern: "^[\\-]?[0-9]{0,3}\\.[0-9]+$"}];
  }

  // ajiting の説明
  string description = 1;

  // ajiting の URL
  string url = 2;

  // ajiting がおこなわれる場所
  GeoCoordinate location = 3;

  // 何が free な ajiting か
  repeated string free = 5;
}

// ajiting を返すためのレスポンス
//
// T シャツの裏に印字されているように #ajiting をキーとする `Ajiting` を返します
message AjitingResponse {
  Ajiting ajiting_message = 1 [json_name = "#ajiting"];
}

// ajiting 一覧取得用リクエスト
message ListAjitingRequest {
  // 希望する ajiting の条件を指定するための検索クエリ
  string query = 1;
}

// ajiting 一覧取得用レスポンス
message ListAjitingResponse {
  // 該当する ajiting の件数
  int32 num = 1;

  // 該当する ajiting の一覧
  repeated AjitingResponse ajitings = 2;
}

// ajiting 更新要リクエスト
message UpdateAjitingRequest {
  // 更新する ajiting の ID
  int32 id = 1;

  // 更新する ajiting オブジェクトの内容
  Ajiting ajiting = 2;
}

service AjitingService {
  // みんなの #ajiting を一覧する
  //
  // みんながそれぞれに思う #ajiting を一覧します。
  // query が指定されている場合はその条件に従った #ajiting を絞り込みます。
  rpc ListAjiting(ListAjitingRequest) returns (ListAjitingResponse) {
    option (google.api.http).get = "/v1/ajiting";
    option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = {
      security: {};
      responses: {
        key: "404";
        value: {
          description: "指定した条件に当てはまる ajiting がなかった場合に返すレスポンスです。";
        }
      };
    };
  }

  // #ajiting を更新する
  //
  // 指定した内容によって #ajiting を更新します。
  rpc UpdateAjiting(UpdateAjitingRequest) returns (AjitingResponse) {
    option (google.api.http) = {
      put: "/v1/ajiting/{id}"
      body: "ajiting"
    };
  }
}

ではこれで ajiting.swagger.json を生成してみましょう。もちろんできあがったものはこちらにございます! https://github.com/co3k/protobuf-swagger-example/blob/ce7a439ef0c692388549d3e18ab796bb3f46d5e7/02-ajiting.swagger.json

まとめ

  • grpc-gateway 付属の protoc プラグイン、 protoc-gen-swagger を用いて Protocol Buffers ファイルから OpenAPI 定義ファイルを生成する方法をご紹介しました
  • そのうえで必要となる基本的な Protocol Buffers の書き方と、 OpenAPI 定義に踏み込んだ発展的な option の利用方法をご紹介しました

弊チームでは生成した OpenAPI 定義を基に、クライアント (TypeScript) 側スクリプトを AutoRest で、サーバ (Go) 側スクリプトを go-swagger によって生成することでかなり楽をできている実感があります。みなさんにもぜひお試しいただければ幸いです。


  1. 本エントリでは、ツール等を含めた Swagger のエコシステムを含めて Swagger と呼び、 OpenAPI v2 仕様そのものへの言及については OpenAPI と呼んで区別することにします。なお、 protoc-gen-swagger はいまのところ OpenAPI v2 準拠なので、 OpenAPI v3 は本エントリのスコープ外です。

  2. 現在 VOYAGE GROUP は url フィールドに記載された http://ajito.vg を所有していませんのでご注意ください。

  3. 既報のとおり VOYAGE GROUP は 2019 年にオフィス移転を予定しており、 AJITO も生まれ変わります。 ajiting の際にはこの location フィールドの座標をアテにせず、 Web サイトに掲載された最新のアクセス情報 をご参照ください。

  4. この free は「言論の自由 (free speech)」ではなく「ビール飲み放題 (free beer)」の方の free です。

  5. 一応このように定義しておけば、外から Ajiting.GeoCoordinate として参照できるというメリットもあります。

  6. この free は「言論の自由 (free speech)」の free でもあり「ビール飲み放題 (free beer)」の free でもあります。

  7. もちろん gRPC も含みますがこれに限定されません。

  8. JSON スキーマ上で #ajiting というキー名を利用可能にするために json_names_for_fields オプションを指定しています。通常はあまり気にすることはないと思いますが、覚えておいて損はないオプションかもしれません。ちなみにこのオプションは v1.5.0 にて最近追加されたものです。

  9. なお、ここから説明することは本当にドキュメントがないので心して読んでください。

VOYAGE GROUPエンジニアインターンシップ Treasure2018 を開催しました #voyage_intern

こんにちは、@saxsir です。今年の7月からエンジニア => 人事になりました。

それはさておき、VOYAGE GROUPでは学生向けエンジニアインターンシップTreasureを毎年開催しています。

主に来年就活をするであろう学生エンジニアのみなさんに向けてまとめを書いておこうと思ったのですが、今年は参加してくれた学生がたくさんブログを書いてくれました!

参加してくれた学生の生の声を読んだ方が伝わるかなと思うので、みんなが書いてくれたブログを紹介する形にしたいと思います。

目次

Treasureとは

VOYAGE GROUPが2006年から毎年夏に開催しているエンジニア学生向けのインターンシップです。(今年で13回目の開催になります) 期間は3週間、前半は講義(手を動かすものが多い)中心のインプット期間、後半はチーム開発期間になっています。

今年は前半の講師 + 後半チーム開発のサポーター*1を合わせると24人、人事も合わせると32人がインターンに関わっており、なんと参加学生よりクルー*2の方が多い!

これだけ手厚いインターンをやっているのはVOYAGE GROUPくらいなのではないでしょうか。笑

内容については私が説明するよりも参加者の生の声を見るのが一番だと思うので、それをこれから紹介します。

参加者の感想ブログ(みんなたくさん書いてくれてありがとう!

たくさんあって全部いいブログなのですが、この記事を読みに来た人用に簡単にラベリングしてみました。

内容や環境が分かる

Treasureの説明, 講義の内容・レベル感について書いてくれています。 私が書くより綺麗にまとまってるのでこれを読めばいいかもしれません。 dragon-taro.com

1日の簡単なタイムテーブルが書いてあって、よりイメージがつきやすいかなと思います。 講義の感想も3行で書いてあってより雰囲気がわかりやすい。 monpoke1.hatenablog.com

毎日日報をブログに書いてくれていました。これを読めば内容もいろいろ伝わる気がします。 最終成果物の画像も載ってますね! yoshikawataiki.net

「それぞれにいいところがある、素敵なチームだった。」 素敵な一言ですね。 hatsunem.hatenablog.com

DBの講義がとてもよかった、と具体的に書いてくれています。 (私も勉強になりました) polyomino.hatenablog.jp

+ 応募したきっかけや選考に関して参考になる

内容についても詳しく書いてくれていますが、応募したきっかけや動機についても書いてくれています。 来年来る人は参考になるかも?? koukyo1213.hatenablog.com

guri-blog.hatenablog.com

fcimsb55yn23.hatenablog.com

+ いかに最高だったか、感想がわかりやすい

「筋トレがしたくなり、そろそろジムに行く時間なので、」、からの内容が長くてちょっとツンデレ感がありますね。 shimohiroaki.hatenablog.com

おまけ

開催中のTwitterの様子は下記にまとまっています。

https://togetter.com/li/1263149

風景

  • 講義期間
  • チーム開発期間
  • 最終発表

のイメージがなんとなく見えそうな写真をいくつか。

講義

前半はインプット期間。

朝会の様子。気合いの入る一言でスタートします。 f:id:saxsir256:20181003155357j:plain

講義の様子。インプットして f:id:saxsir256:20181003155336j:plain

手を動かして

f:id:saxsir256:20181003161624j:plain

分からないことは補足があったり f:id:saxsir256:20181003155339j:plain

ペアプロしたり f:id:saxsir256:20181003161142j:plain

TAが教えてくれたり

f:id:saxsir256:20181003161145j:plain

チーム開発

後半はアウトプット。チームでつくるものを考えて形にします。

議論をしたり f:id:saxsir256:20181003155400j:plain

コードを書いたり f:id:saxsir256:20181003162407j:plain

デバッグしたり f:id:saxsir256:20181003162111j:plain

最終発表後

前でデモをしたり f:id:saxsir256:20181003162859j:plain

最終発表中は講師陣から質問が飛んできたり f:id:saxsir256:20181003155346j:plain

全体での最終発表後はブース形式で講師や学生同士でお互いの制作物を見たり f:id:saxsir256:20181003155349j:plain

最後に

私自身は2014年に学生としてTreasureに参加、2015年には内定者としてTAで関わり、入社後はチームごとにつく技術サポーターとして2016年〜、今年は人事という立場でもありつつ技術サポーター(と講義を一部やったり)としてTreasureに関わりました。

だいたい年明けて2月頃から準備が始まり、準備~開催まで延べ50人以上のクルーが携わる一大イベント。

毎年参加して思うのは、とにかくクルーが全力コミット。準備期間も開催中も全力コミットだし、なによりクルーも全力で楽しんでます。

これだけ多くの人が関わってくれて、学生はみんな優秀で、毎年終わった後はふりかえりをして改善して...普通にスゴい。 「360°スゴイ」インターンだと思います。

と、中の人が書いてもほんとかよ?ってなると思うのでぜひぜひ参加してくれた学生のみんなのブログを読んでみてください。

気になった人は来年待ってるよ!

f:id:saxsir256:20181005101425j:plain

*1:後半のチーム開発で各チームにつく現場のエンジニア

*2:VOYAGE GROUPでは社員のことをクルーと呼びます

BIT VALLEY 2018スポンサー告知!

こんにちはシステム本部 三浦@hironomiu です。

VOYAGE GROUPでは勉強会からエンジニアイベント、カンファレンスなど様々な機会で共感できるイベントに関してスポンサーを行っています。 今回は2018年9月10日、渋谷区文化総合センター大和田にて開催されるテックカンファレンス「BIT VALLEY 2018」にスポンサーの告知エントリーしたいと思います。

BIT VALLEY 2018

公式サイトはこちらになります。 bit-valley.jp

なぜスポンサーとなったのか

BIT VALLEY 2018はconnpassにて参加エントリーができます。この中に「学生支援プログラム」と言うリンクがあります。 sbv.connpass.com

学生支援プログラム

supporterz.jp

テックカンファレンスとして共感できるだけでなく地方の学生さんに対し「最新の技術・多様な働き方を知ることで、これからのキャリアイメージを描く きっかけ作り」を提供することは、とても共感できました。

終わりに

「BIT VALLEY 2018」テックカンファレンススポンサーの宣伝でした。セッションの合間には弊社の1分ムービーも流れますので参加された方は是非観てみてください!

builderscon tokyo 2018で #ajitofm の公開収録します!

こんにちはシステム本部 三浦@hironomiu です。

VOYAGE GROUPでは勉強会からエンジニアイベント、カンファレンスなど様々な機会で共感できるイベントに関してスポンサーを行っています。 今回は2018年9月6日(木)から8日(土)の3日間開催される「builderscon tokyo 2018」にスポンサーとなり9日(土) 12時20分からランチセッションを開催することをご紹介したいと思います。

builderscon tokyo 2018

buildersconは、 「知らなかった、を聞く」 をテーマとした技術を愛する 全てのギーク達のお祭りです

公式サイトより引用

テーマに興味を持たれた方は是非参加してみてください!

builderscon.io

去年の様子

去年の「builderscon tokyo 2017」からスポンサーとして支援させていただいています。去年の様子は公式の画像と振り返りのブログエントリーからご覧ください。

公式

www.instagram.com

振り返りブログエントリー

techlog.voyagegroup.com

ランチセッション

VOYAGE GROUPのランチセッションは9日(土) 12時20分開始予定です。このランチセッションでは去年と同じく公開ajitofmと言う形で行う予定です。

Lunch Session (VOYAGE GROUP) - builderscon tokyo 2018

ajitofm

ajitofmは弊社エンジニアに留まらず社外のエンジニアの方も招き多岐にわたるテーマで語るpodcastです。このエントリー時点で30話まで収録されています。

ajito.fm

終わりに

「builderscon tokyo 2018」カンファレンススポンサーの宣伝でした。弊社エンジニア陣が様々なテーマで語りますので是非ご参加ください!

社内勉強会の紹介「Reactハンズオン編」

こんにちはシステム本部 三浦@hironomiu です。

社内勉強会していますか?

VOYAGE GROUPでは業務時間内に各人の開発などに支障がない限り、特に制限なく社内勉強会が開催されています。 今回はそんな社内勉強会の雰囲気を伝えたく先日開催された「Reactハンズオン」をダイジェストでエントリーしたいと思います。

Reactハンズオン?

この勉強会はサービスのフロントでReactを導入すると言う経緯でエンジニア有志が立ち上げました。講師は週に1回業務開発に携わって頂いているフリーランスの @mizchi さんに行っていただきました。

勉強会風景

「Reactハンズオン」では参加エンジニアが多そうということもあり社内バーAJITOで開催されました。ハンズオンと言うことで通常1時間の勉強会が多いのですが今回は2時間かけて行われました。 f:id:hironomiu:20180815150422j:plain

ライブコーディング

「Reactハンズオン」の講師役の @mizchiさん より適時、ライブコーディングなどによる解説も織り交ぜて今回のハンズオンは進みました。 f:id:hironomiu:20180815151331j:plain

Slack

Slackではハンズオンの内容を貼り付けてお互いに確認やReactについていろいろな議論が展開されました。

f:id:hironomiu:20180828100031p:plain

ハンズオン資料

今回のハンズオン資料は@mizchiさんのGitHubリポジトリで公開されています。READMEを頭から進めることでゴールに到達できますので是非挑戦してみてください!

github.com

終わりに

社内勉強会の紹介「Reactハンズオン編」でした。社内勉強会は必要な技術に関しては気軽に行われているだけでなく、最近は前回エントリーしたSQLアンチパターンの勉強会と同様に社内エンジニアだけでなく社外の優秀なエンジニアを巻き込んでの勉強会なども活発に行われています。より良いサービスを展開するために自発的な技術習得は欠かせないと思いますので今後も自学自習し自走できる人材育成の視点からも勉強会カルチャーを大事にしていきたいと思います。

「SQLアンチパターン勉強会2018」は社内アンチパターンを持ち寄っての開催と進化しました!

こんにちはシステム本部 三浦@hironomiu です。

SQL書いていますか?

VOYAGE GROUPでは主に2年目の若手向けに2014年から2017年までSQLアンチパターン勉強会がゆるく続いていました。(2014年当時のエントリーはこちら)

2018年は?

今年2018年は監訳者の@t_wadaさんが弊社の子会社などに週2回エンジニアリングコーチとしてきて頂いている縁もあり、コーチングの時間内で2年目に縛らず中堅、シニアも含め開催しようと言う流れになりました。

f:id:hironomiu:20180815143100p:plain

なんと更に贅沢にもt_wadaさんを進行役のメンターとしてお招きでき、2年目の若手に限らずシニアメンバーも含め、各章を見た後に社内のDB、テーブル、SQLを持ち寄ったた社内SQLアンチパターンあるある話まで広げた勉強会としてグレードアップして開催することになりました。

f:id:hironomiu:20180815143017p:plain

勉強会スケジュール

今回は7月4日(水)から毎週水曜の定期開催、各会1時間、当日は1章〜3章ぐらいのペースで進めていきます。現時点で第4回まで開催され毎週1章ペースで進んでいます。過去のSQLアンチパターン勉強会に比べますとt_wadaさんの章紹介が手厚く、更に社内のアンチパターン事例を持ち寄りt_wadaさんのファシリテートで場が盛り上がるため各会1章と濃密なペースも妥当かなと思っています。

事前準備

基本読書会ですので事前に該当章を読むのは当然ですが、それなりに時間の経った書籍と言うこともあり、インターネット上にある優良な章紹介などもt_wadaさんから当日、または事前準備として共有してもらえるのもありがたいところです。

f:id:hironomiu:20180815133315p:plain

第1章 ジェイウォーク

初回7月4日は1章ジェイウォークを開催しました。この章では天然もののアンチパターンをarataが持ち寄りt_wadaさんが1章紹介後に、t_wadaさんがファシリテートの形でいろいろと議論が盛り上がりました。

f:id:hironomiu:20180815142834p:plain

チラ見せ

第2章、3章、4章の弊社Slackの各章開幕の様子をチラ見せします。

2章

f:id:hironomiu:20180815142734p:plain

3章

f:id:hironomiu:20180820090920p:plain

4章

f:id:hironomiu:20180815142628p:plain

終わりに

2018年のSQLアンチパターン勉強会の紹介でした。毎回20名前後のエンジニアクルーが集まりAJITO、Slackのチャンネル双方でアンチパターンネタで大盛り上がりと盛況です。今年もSQLアンチパターン勉強会を開催してみて三浦が感じたのは一般的な技術書は技術の進歩などにより書籍の寿命は短めな傾向にありますが、長い期間生き残ったSQL(とRDBMS)所以に日本語版が発売されてから長く価値のある書籍として活躍してるなと改めて思いました。各章が独立していて途中参加の敷居が低く、Webアプリケーション開発では使う頻度の高い技術ですので社内勉強会向けにオススメの書籍だと思います。

非同期コミュニケーションにおけるドキュメント

VOYAGE MARKETINGの @katzchum です。

自分が携わったプロジェクトで、以下の2点を課題として、プロジェクトの進め方について検討しました。

  • プロジェクト立ち上げの流動的な場面でもリモートのメンバーに対しても如何に早く共通理解を促進させることが出来るか?
  • 開発のインプットとして、どうドキュメントを位置づけてチームへの情報の伝達手段として用いるか?

検討した結果、以下の取組を行ないました。

  • ドキュメントをDSLでコード化
  • DSLのレビューを通じて情報共有&ディスカッション
  • デッドコード化させない取組(短期・長期ドキュメント)

取組のプロセスとして軽量だったわりには良い体験や気付きが得られた様に思い、プロジェクトの背景・初期の悩みから、どうアプローチして取り組んで行ったのか、振り返りも交えてお話したいと思います。

ドキュメントの悩み

新規のプロジェクト立ち上げ時にドキュメントどうすんの?と聞かれると悩みますよね。

プロジェクトのスタートは大抵、以下のようになるかと思います。

  • プロダクトとして何をどうしたいのか?の青写真を作成
  • プロダクトオーナーと開発者との間で、青写真の図解を眺めてプロジェクトの進め方の検討やタスクのブレークをする
  • キックオフMTGを開催して青写真をベースにプロジェクトの概要を共有する

以降、青写真を起点に様々なコミュニケーションがされていきます。

ドメイン知識を持ち合わせた一箇所に固定化されたチームであれば、青写真をホワイトボードに書き殴ってディスカッションするなどできますが

  • プロジェクト自体がスモールスタート
  • 段階的にメンバーが増える(メンバーが固定化されない)
  • ドメイン知識が形成されていない(不確実性も高く・情報が全て出揃っていない)
  • リモート開発も組み入れる

上記の制約や要素が組み合わさると直接会話だけのコミュニケーションだけでは成り立ちません。
体制も含めてプロジェクトの成熟度が低い状況下ではそれなりの数のドキュメントの作成が必要となります。 ドキュメントがない若しくは適切に管理・共有されていないと、悩み事や困りごとが増えてきます。

  • 人が増えるたびに繰り返し説明するコストが嵩む
    毎回同じ図を書いて説明するのはめんどくさい
  • プロジェクトが進んでいく中で図も少しづつ変化していく
    何がどう変わったのか?どういう議論があってそうなっていったのか?
    ホワイトボードで書いた図に対してdiscussionが行われて出てきた指摘をどうやってリモートのメンバーに共有していくのか?
  • そもそもリモートのメンバーにホワイトボード使って説明ができない
    ホワイトボードの写真をとって共有したりもするけれども、結構情報が欠落するしミスリードもしやすい
    図の書き方は人それぞれだったりするし、複雑な構成だと言葉で補う必要がある
  • リモートメンバーにも図のレビューや疑問を挙げてもらいたいけど難しい
    書いた図に対して具体的にピンポイントでここ!というのを文章で指し示すのは大変です
    図に赤書きしたものをやりとりするのも、最新の図がどんどん変わっていくなかではもっと大変です
  • 共通の言葉が定まってないとやりづらい
    プロジェクトの初期では抽象的な概念がまだ言語化されていないので、非同期なコミュニケーションでは正確性を欠きます
    プロジェクトメンバーがそれぞれ同じ様な図だけど微妙にバラバラものを書いて、違うキーワードで説明されるケースが出てきそうです

またプロジェクトの立ち上げ当初では

  • 抽象的な概念が言語化されきたら、それを元の図解とのマッピングが出来るか?

というのも重要かと思います。
最初はふわふわっとした内容をイメージ化するのは雰囲気で出来ます。
そのアウトプットされたイメージを通してコミュニケーションすることでだんだん事柄や概念が言語化されていきます。
最初から全てを言語化するのは難しく、図を使って説明を補い共通理解を促進させるのが図解のメリットとなります。
図解は共通理解されている言語とセットで扱うことで価値が出てきます。
あくまでも図は補足説明の為のアウトプットであって、それ自体で全てを表現できるわけではありません。
エンジニアも非エンジニアも同じ言葉(ユビキタス言語)を使って今後やり取りしていくことになります。
逆にここら辺が出来ないとコミュニケーションが迷子になってしまいます。

以下の2点を課題として、プロジェクトの進め方について検討しました。

  • プロジェクト立ち上げの流動的な場面でもリモートのメンバーに対しても如何に早く共通理解を促進させることが出来るか?
  • 開発のインプットとして、どうドキュメントを位置づけてチームへの情報の伝達手段として用いるか?

検討した結果、冒頭で説明した以下の取組を行ないました。

  • ドキュメントをDSLでコード化
  • DSLのレビューを通じて情報共有&ディスカッション
  • デッドコード化させない取組(短期・長期ドキュメント)

非同期コミュニケーションにおけるドキュメントのやりとり

ドキュメントをDSLでコード化

なぜDSLなのか?どうしてコード化する必要があったのか?まず理由を説明したいと思います。

まずDSLですが、

  • 統一した表記内容にすることができる
  • コードとして取り扱うことができる

がメリットとしてあります。
フリーフォーマットでドキュメントを書くことは表現力に幅がありますが書き手のセンスが問われます。
表記内容に統一性がないので、読解力を求められたり違った解釈をされる可能性があります。
文字や表だけで表現するには冗長的になりすぎる場合は図解したい時も、フリーフォーマットでは書き方や扱うツールも千差万別です。

次にコード化する目的としての主眼は

  • Github上でレビューを可能にする

です。 通常のプログラムのコードと同様にドキュメントもGitHub上でのレビュー対象にします。
ここで言う所のドキュメントとは開発のインプットとなるもので

  • 各種UMLが扱うダイアグラム(図解)
  • データモデリングの叩き
  • 画面ラフ
  • 連携I/F(API、ファイル)の叩き etc..

などを組み合わせして他の文章(主にIssue)を補足する為のものになります。
図などはどんなツール(なんならホワイトボードでも)でも書けますが、その多くが独自フォーマットのバイナリでGithub上のレビューではうまく扱えません。これらの図や表をDSLを用いてコード(テキスト)化することでレビューを可能にします。
また対面レビューでなくてもリモートのメンバーも含めて大勢の人たちにレビューされてコメントがログとして残るようになります。

又、コード化の副次的なメリットとして以下があります。

  • 再利用が可能になる 説明をする度に毎回ホワイトボードに書く手間を省くことができる
    再現性もあり、編集・加工(コピペで一部分のみ変更とか)も楽
  • 複数ドキュメント間の関連が想像しやすい 記述するDSLが異なっても、同じ語句を用いることで複数のドキュメントに関連の意味づけをすることが出来ます
    コンテキストが異なる複数の独立した図で、イメージ上で例えば○と□とで違う表現方法がされたとしても同じ語句を使っていれば同じものとして認識できる
  • 変更の履歴をトレースできる バージョン管理の対象にすることで、履歴を追うことができる
    図解であったとしても、どの部分が修正されたか?又、コミットログなどでIssueと紐付けることでなぜ変更されたか?がわかる
  • ピンポイントでの引用・参照ができる 特にレビューの際に有効で、紙に印刷して赤書きを入れなくてもピンポイントで行又はブロック単位での指摘ができる
  • 情報の分断がなくなる コードと同様に扱うことによって情報が分散せずに済む
    一箇所に情報が集約されるので検索もしやすくなる(テキスト化されているので中身も検索対象となる)

どういうDSLを用いたか?

これから私が実際にプロジェクトで利用したいくつかDSLの紹介をしたいと思いますが、実際にどう書くのか?というのはここでは説明しません。
取り扱うDSLの記法が多数存在するので、ここで紹介するより、リンク先のドキュメントを見て頂いたほうが早いのでそちらを参考にしてみてください。

PlantUML

plantuml.com 各種UMLのダイアグラムを扱う際に利用しています
一通りのUMLをDSLで直感的に記載できます
UML以外の図も簡単な画面ラフ(ワイヤーフレーム)も扱うことができます

GraphQL

http://facebook.github.io/graphql/October2016/facebook.github.io データモデリングの叩きを作成する際に、ざっくりした型の定義(type)を行ない、データ取得のパターンの洗い出し(Query)とデータの操作のパターンの洗い出し(Mutation)を雰囲気で書きます
実装は行ないません。あくまでも雰囲気です
GraphQL自体はただのクエリ言語ですのでシステムを外部から見た際のデータにフォーカスしたインターフェース(振る舞い)をゆるく言語化する際に利用します
UMLのクラス図でも同じ目的で扱うことは出来ますが、内部実装までフォーカスしてしまうのでより抽象度の高いGrapQLを用います

OpenAPI(Swagger) Specification

swagger.io 最終的なAPI仕様書として記述させることもできます
連携I/Fの叩きとして先に作成します。レスポンス例も記載できるので、振る舞いのイメージを伝えるのにも便利です

CSV Schema

http://digital-preservation.github.io/csv-schema/csv-schema-1.2.htmldigital-preservation.github.io CSVやTSVのファイルフォーマットの仕様書を扱うことができます

DSLのレビューを通じて情報共有&ディスカッション

DSLで作成したドキュメントはGithub上でPRを作成してレビューを行ないます。
これらを作成・レビューする意図としては以下になります。

  • お互いの認識の共通理解の促進と確認
  • 開発のインプットとしての質を向上させる

文字だけで説明するのではなくDSLで書いた図等で補足するのですが、説明する側される側のどちらがDSLを書いても良いです。
作成されたDSLをレビューすることで、イメージどおりであればお互いの認識がとれて先に進められます。
逆に既に共通認識が取れていて文章のみ(多くはIssueのコメント)で伝わっている(疑問がない)のであれば、DSLを書いて補完させる必要はありません。
ただDSLでコード化するメリットで記載した同じ語句を用いる手法を取り入れて敢えてDSLで書き起こすというのもあります。
こうすることで例えばコード内で使うクラス名とDSLないの語句がマッチングでき、内部設計のレビューにもなります。

文章のみの説明だけでは伝わりづらいので、PlantUMLのDSLを使った例で説明したいと思います。
養殖な例題となってしまいますが、挨拶をするというユースケースがありそれを実装する場合で説明します。

”自分から相手に対して挨拶をする” という要件だった時にどの様に解釈しますでしょうか?
一番単純に書くとすると以下のようになります。

gist.github.com

f:id:katzumi:20180516194754p:plain

わたしとあなたがいて”こんにちは”というメッセージを渡しているだけですが、要件の文字を書き起こしただけでお絵かきしたに過ぎません。
これではメリットがありません。DSLなのでドメインを意識してコードをリファクタリングしてみます。

gist.github.com

f:id:katzumi:20180516194801p:plain

まず、わたしとあなたというのがアクターとして定義されました。
Author as “わたし” としているのが、利用する語句をあわせる意味があります。
他のドキュメント内でもAuthorという語句があったらこちらの図と同じ意味となります。
コンテキストが違って筆者と別名をつけていても同じものと理解できます。
単純に わたし としていたものが実装する際のAuthorというアクター。。Authorクラスが必要になるのかな?とか想像できる様になります。
アクターとしての概念が言語化されて明示されたことになります。このコードを見た人によっては”サンプルだからってブログを見ている人との関係性だったの?Personの方がもっと汎用的でいいんじゃない?”とツッコミを入れたくなると思います。
そういうフィードバックを貰うことで、お互いの共通認識の確認になり、ディスカッションもされることで開発のインプットとしての質も向上します。

デッドコード化させない取組(短期・長期ドキュメント)

ドキュメントはGithub上で管理されることになりコードと同じ様にチーム内で共同所有されます。
実際、ドキュメントはDSLで書かれたテキストなので、ソースコードを検索している際に関連するドキュメントも結果に含まれることになります*1のでよりソースコードと同列という意識で扱われます。
その共同所有しているという意識の中でチーム内で自然発生的に2つのルール的なものが出来てきました。

  • 短期なものは定期的にクリーニングする
  • 残す文章については参照がしやすいように改善する

作成したドキュメントを短期的なものと長期的になものに分けて、それぞれがデッドコード(DSLがプロジェクト的に参照されない状態)にならない様な取組となりました。

短期的なものでは、WIPのPR上でディスカッションのみに利用してmergeせずに捨てたりしました。
最初はプロジェクトの進行と共にコードと併せてドキュメントをブラッシュアップさせていた物が、開発が安定してきてコードも変更が少なく関連するドキュメントの参照・更新が少なくなったものは役割が終えたとして削除PRを作成して消しました。
ドキュメントの生成(merge)と破棄のプロセス自体がPRを通して行われます。

長期的なドキュメントについては、それぞれのドキュメントが独立して存在して初見者にはどうやって読み進めたら良いか分かりづらいので各DSLをAsciidoc内に埋め込んでインデックス化させました。
Asciidoc自体はマークダウンで書くことができPlantUML、Github形式のMarkdownやソースコード(コードハイライト対応)を埋め込むことができ、目次(Table of Contents)までつけれます。
AsciidocはGithub上でそのまま表示することが出来ますが基本的な表現のみしかサポートされていません。
UML等の図の埋め込みが展開されないのとAsciidoc独自の表現が反映されないのでPDF化して参照出来るようにしました。
PDF化すると大変綺麗に出力(仕様大全っぽく)できるのですが、出力するのが手間(環境構築も含め)なのでJenkinsで定期的に自動生成するようにしました。

取り組んだ結果どうだったのか?

まず個人的な感想となりますが、主にDSLを書く側だったのではあるのですがいくつかまとめると

  • オープンなフォーマット且つ軽量だったので書く安心感があった
    誰が書いてもほぼ均一な表現になるし読める(平準化される)
    言語の読み方についての説明は不要だった
    いつでも捨てられる安心感
  • ストックが溜まっていく感じが良かった(語彙力が増す)
    同じ様な図がコピペで使い回しができる
    変更点を説明するのに、前の図が使えて差分がわかる
    使い続けるとどんどんすらすら書けるようになる
  • イメージを文章化、更にコードとマッピングするフローがうまくつなげれた気がする
    表現が難しいのですが、頭の中を整理する道具として思考のフローとマッチした感がありました
    抽象的な概念(イメージ)の中から共通理解できる言語に落とし込むイメージです

    • 初期の思考フロー
      f:id:katzumi:20180516193624p:plain
    • 実装時の思考フロー
      f:id:katzumi:20180516193631p:plain

DSLが中間言語的な振る舞いをして開発からみるとより理解しやすい言語で語られる様になったかなと思います

  • 同じ表現(文章)が複数出現した時に正規化・共通化の気づきを得られた
  • 図だけで全て表現・説明しようとする悪いクセが直った
    作成した図を使って説明する時に、わかりやすいように吹き出しをいっぱい付ける癖がありました
    DSL自体はシンプルに書き、説明をPRのレビューコメントで残す様にしました
    そうすることで図がごちゃごちゃせずに、わかりやすくメンテナンスしやすいものに出来ました
    副次的な効果で、コメントに積極的にレビュー観点を残せるようになり、フィードバックを得やすくなりました
    設計上の不安も共有することで、図だけ見てもふ~んで済まされない感じにはもっていけました。
  • DSLとコードに乖離があると良い意味で気持ち悪さを感じるようになった
    作成したDSLをインプットにコーディングを進めて乖離がでてくるケースはまあ普通にあります
    その際にコードのレビューとセットでDSLの修正も併せて行うと、あーこうしたのね!とレビューする側の理解が早いです
    特にある程度のボリュームの新規機能で設計や実装に不安がある時にDSLがあると、初見のコードから実装時の思いを紐解くよりも共通理解されている言語で修正した内容と目的がわかる(コミットログなどから)とお互いに納得感が得られて進めます
    初期の理解で作成したDSLに対してコーディングを進めてより理解が進んだ状態で見直すと、もっとこうしたら良かったのでは?という気付きが得られるケースもありました

他のメンバーからのフィードバックとその考察をまとめると

  • 図は簡単に書けたが、UML自体の書き方が難しかった
    雰囲気でUMLは書けますが、ちゃんと理解して伝わるように書くのには苦労していました
    ただ初期だけで一度出来上がったものに対して変更を行うのは問題なく出来ていました
  • 画面ラフの共有はさくさくできてよかった
    モデルと画面項目のマッピングや項目位置など、基本的な要件を伝えるのはそれほど手間にもならず手戻りも少なくできた
    ラフから実際の画面(bootstrap)を作成するのは別途UIのガイドラインがないと辛い
  • ドキュメントの更新を忘れる
    プロジェクトの初期でドキュメントの扱いPRのフローが定まっていなかった為、どうしても更新が漏れるケースがありました
    メンバーには完全に同期しなくても良い旨を共有していましたが、次の改修もありそうなものはなるべくDSLの修正を行うように依頼をしました
    ここら辺はドキュメントを作りすぎない、短期的なものと長期的なものを分けてうまく扱う様に布教していくしかない様に思います
  • ユースケース・ステートマシンを書く時にレイアウトが大変
    自動で良しなに配置してくれるけど、迷子にならないように配置しようとするとコツがいる
  • 情報が一箇所に集約されGithubだけに集中できる
    リモートからだと他のツールやリポジトリを参照する際にVPNが必要なケースが多かったのですが、それが不要になったのが大きいです
    ローカルにcheckoutして参照できるので、ネットワークがつながらない時に作業中断しなくてすみました

総評すると取組のプロセスとして軽量だったわりには良い体験や気付きが得られた様に思います。
最初はドキュメントをツールやフォーマットの側面からの困りごとを改善したいとの思いからスタートでしたが、振り返ってみると非同期なコミュニケーションで情報のロスを少なく・逆に密にする為に手法とそれにマッチするフォーマットとをそれぞれ変える必要があったのだと思いました。

*1:前述のソースコードとDSLとで同じ語句を用いて意図的にそうなることを目指しています