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

全てがJSONになる

TL;DR

JSON Schemaを使ってこういうことが実現可能になった。

  • ダミーAPIサーバの提供
  • ドキュメントの自動生成
  • APIクライアントの動的定義
  • APIサーバのバリデータの動的定義
  • APIサーバのレスポンスの自動テスト

JSON Schemaとは

JSON SchemaというのはあるJSONのデータ構造を記述するための方法および書式の仕様で、 JSON SchemaもJSONで記述される。 これを利用すれば、リソースベースの(=RESTfulライクな)APIの仕様が簡便に記述できる。 例えば、我々のAPIはレシピとユーザというリソースを扱っていて、 それぞれCRUDAPIを備えており、レシピはidとtitleとdescriptionという属性を持つ、 という旨をJSON Schemaで表現できる。

なんで最近ちょっと流行ってんの

Mobile First、 Service Oriented Architecture、 Microservicesみたいな便利な言葉が流行ってるから、 それにともなってか少しずつJSON Schemaも流行りつつある感じがする。 大きなアプリが細かいアプリに分かれてそれぞれ仕事をするようになるにつれ、 お互いのコミュニケーションの方法が問題になる。 この問題を解決させるための何かが必要で、その解決方法としてJSON Schemaや Hyper Application Language みたいな比較的理解しやすい道具がにわかに期待されてるんだと思う。

そこにわりと早い段階から着目して実用化していたのがHerokuで、 Heroku HTTP Toolchainという名前で幾つかのOSSをリリースしている。 HerokuのAPI関係のプロダクトについては前に APIクライアントを自動生成するやつ - ✘╹◡╹✘ で触れた。 この辺のHerokuのツール群は実際に検証して実装も読んでみたものの、 細かいところを見ていくと使い物にならん印象だったので、僕は別の実装をつくることにした。 わざわざ作り直さなくてもうまく空気読んで現実と折り合いあわせていけばいいものを、 我ながら馬鹿なことやってんなという感じがする。

JSON Schemaからドキュメントを生成する

手始めに、JSON SchemaからMarkdownで書かれたAPIドキュメントを生成する Jdoc というツールをつくった。 以下のようなコマンドで、こういうJSON Schemaから こういうAPIドキュメント が生成される。

$ jdoc spec/fixtures/schema.json > example-api-documentation.md

テストからドキュメントを生成する手法との比較

APIドキュメントの自動生成と言えば、 Autodocというライブラリも存在する。 これはRSpecで記述したAPIのテストをもとにAPIドキュメントを生成するというもの。 こういうAPIのテストから こういうAPIドキュメント が生成される。

Autodocが実装からドキュメントを生成するものであるのに対して、Jdocは仕様からドキュメントを生成する。 Autodocは既にテストが書いてあるのであればちょっとタグを追加するだけで簡単に成果物(=ドキュメント)を得られてコスパが良い。 実際にアプリが生成した内容からドキュメントを生成するため、実装とドキュメントの乖離が少なく抑えられる。 また、テストを書くことの見返りが増えるため開発者がテストを書くのを推進しやすい。 しかしながら、アプリの規模次第ではテストの実行にも時間が掛かるから、 大量のテストを抱えている場合にはドキュメント生成には数分掛かるようになる。 自動化しようとしても、ドキュメントをGitで管理する場合、 乱数や自動でインクリメントされるIDによって差分が大きくなりやすいという問題がある。 現実的には、レビューでDiffが多くて邪魔になりやすいなどの不満が出る。

Jdocは、記述できる仕様の情報量がAutodocより大きいことが多い。 Autodocはテスト実行時の結果から仕様を推測するために幾つかの情報が欠落するが、Jdocにはこれが無い。 またドキュメントの生成が単純で、ドキュメントをGitで管理する場合に差分が出ないので管理しやすいという利点もある。 ドキュメントの内容がアプリの実装から乖離しやすいという問題があるが、 後述する他のツールJSON Schemaを実装にも利用することでこの問題を防ぐことができる。

JSON Schemaをサーバの実装に利用する

Rack::JsonSchemaという、 JSON Schema x Rackで何かいろいろやるMiddlewareを集めたライブラリをつくった。 Rack::JsonSchema::RequestValidationというmiddlewareを装備すると、 受け取ったリクエストがJSON Schemaの定義に違反する場合にエラーを返すようになる。 例えばRailsの例だと「こういう形式のパラメータしか受け取りたくない」という処理を実現したいとき、 before_filterでゴニョゴニョやる代わりにJSON Schemaが使える。 こういう風に実装にもJSON Schemaの情報を活用していくことで、 前述した「ドキュメントの内容がアプリの実装から乖離しやすい」という問題をある程度はカバーできる。

Railsでは通常この役割はstrong_parametersやbefore_actionが担当することになる。 strong_parametersでは「このAPIではこういうパラメータの制約がある」というメタ情報がコード中にしか残らない。 そのため、もしAPIドキュメントを自動生成するときにこの情報を含めたくても参照できないという問題がある。 この問題に対して過去に講じた対策がr7kamura/weak_parametersで、 before_actionの便利wrapperを提供しつつ、定義したメタ情報をクラス内に保存して外部から参照可能にした。 例えばAutodocではこのメタ情報を参照していて、 weak_parametersとautodocを併用している場合はパラメータに関する情報を ドキュメント内に自動で追加してくれる

君だけの最強のアプリをつくろう

ところでRack::JsonSchema::RequestValidationはAPI自動納品系のライブラリと相性が良くて、 例えばr7kamura/rack-mongoid_adapter というRack middlewareと組み合わせて使うことができる。 これは「受け取ったリクエストをもとにMongoDBにクエリを投げて結果を返す」というREST APIを勝手に生やしてくれるもので、 そのままの状態だとクエリ投げ放題で脆弱なんだけど、 Rack::JsonSchema::RequestValidationを使えばリクエストの内容を検閲してJSON Schemaに則した値のみ入出力可能にしてくれる。 自分はこういうデッキ構築型対戦カードゲームのシナジー効果みたいなのが結構好きなのかもしれない。

ドキュメントと実装の乖離を無くす

Rack::JsonSchema::ResponseValidationというmiddlewareを装備すると、 アプリが返却しようとしているレスポンスがJSON Schemaで宣言した型と違反していると即座にエラーを返すようになる。 用途としては矯正ギプスみたいなもので、 テスト環境で装備して正常系のテストケースをCIなどで回しておけば、 JSON Schemaと実装との乖離、ひいてはドキュメントと実装の乖離を無くすことができる。

JSON SchemaからダミーAPIサーバを立てる

RESTful APIの慣習、すなわちGETリクエストには単一のドキュメントまたはその配列がステータスコード200で返る、 などを前提とすることで、JSON Schemaに定義された情報のみからダミーのレスポンスを返すことができる。 Rack::JsonSchemaにはspecupというコマンドを同梱してあり、 specup schema.jsonのように呼び出せば localhost:8080 にダミーのレスポンスを返すAPIサーバを立ててくれる。 勿論ポート番号などはオプションで指定可能で、 rackupと全く同じオプションを取るようになっているのでRackに詳しければ色々Hackして遊んだりできる。

$ specup schema.json
[2014-06-06 23:01:35] INFO  WEBrick 1.3.1
[2014-06-06 23:01:35] INFO  ruby 2.0.0 (2013-06-27) [x86_64-darwin12.5.0]
[2014-06-06 23:01:35] INFO  WEBrick::HTTPServer#start: pid=24303 port=8080

$ curl :8080/apps/1
[
  {
    "id": "01234567-89ab-cdef-0123-456789abcdef",
    "name": "example"
  }
]

$ curl :8080/apps/01234567-89ab-cdef-0123-456789abcdef
{
  "id": "01234567-89ab-cdef-0123-456789abcdef",
  "name": "example"
}

用途としては、specupコマンドとJSON Schemaさえあればどこでもサーバを立てられるので、 例えばクライアントライブラリのテストをする用途や、 サーバ実装が未完成の状態でも仕様を詰めたりある程度クライアント側の開発を進めたりなど、色々な用途に使えると思う。

テスト時のAPIのモックにJSON Schemaを利用する

RubyだとWebMockというライブラリがあって、 指定したAPIへのリクエストをstubして指定したレスポンスに置き換えてくれるという機能がある。 このWebMockに返却させるレスポンスにはRack applicationを指定することもできて、 あたかもリクエストがそのRack applicationに飛んだかのように処理される。 また、前述のspecupのダミーAPIサーバの機能はもともとRack middlewareとして実装されている (Rack::JsonSchema::Mock)。 これらの機能を組み合わせれば、 テスト内で発生するAPIリクエストを全てJSON Schemaから生成されたダミーレスポンスに差し替えることが可能になる。 実際にJsonismというHTTPクライアントでのテストでこの手法を利用した

APIドキュメントを表示する

specupで起動するサーバにはいろいろ面白機能が付いていて、 GET /docsにアクセスするとJdocで自動生成したAPIドキュメントの内容をそのまま返してくれるという機能がある。 HTMLに変換してCSSを施した綺麗なドキュメントページを表示したりというのも考えたんだけど、 正直text/plainでブラウザに表示されたMarkdown形式のAPIドキュメントもそんなに悪くなかったので今はそのままにした。 この機能実験的過ぎて正直何に使えるか分かってないんだけどきっと誰かがアート作品に昇華してくれる。 動的生成してブラウザ上で直接APIリクエストを発行できるコンソール画面を付けたり、 curlコマンドのサンプルを表示したり、という機能がついていくと便利かもしれない。

APIの仕様を返すAPI

specupで起動するサーバは、GET /schemaにアクセスすると JSON SchemaそのものをJSONで返してくれるという機能を持っている。 APIサーバがAPIの仕様を返却してくれるというのは結構良いアイデアだと思ってて、 例えばHerokuのAPIはこういう感じでJSON Schemaを取得できるようになっている (コピペで動く)。

$ curl https://api.heroku.com/schema  -H "Accept: application/vnd.heroku+json; version=3"

JSON Schemaが取得できるようになっていると何が良いかと言うと、 self-documentingでデザイン的に美しいということもあるんだけど、 後述のようにJSON Schemaをもとにクライアントの実装を自動生成できるという実用的な利点がある。

APIクライアントを自動生成する

APIサーバ、APIドキュメントと来たらAPIクライアントだ!ということで、 JSON SchemaからAPIクライアントを自動生成する Jsonismというライブラリをつくった。 使い方は非常に簡単で、JSON Schemaを引数に与えるとAPIを叩くためのメソッドが生えたクライアントが返ってくる。 またレスポンスは単純なHashではなくそれぞれのオブジェクト用のクラスのインスタンスになっていて、 ActiveRecordのようにそのオブジェクト自身に対して更新をかけることで更にAPIリクエストを送れるようになっている。 JSON RPCにちょっと似てる。httpieみたいなCLIツールも同梱できるとかっこいいかも。

schema = YAML.load_file("schema.yml")
client = Jsonism::Client.new(schema: schema)

# GET /apps
client.list_recipe

# POST /apps
response = client.create_recipe(title: "基本の寿司")
response.status  #=> 201
repsonse.headers #=> { ... }
repsonse.body    #=> #<Jsonism::Resources::Recipe>

# PUT /apps/:id
resource = response
resource.title = "☆基本の寿司☆"
resource.update

# DELETE /apps/:id
resource.delete

自動生成か 動的定義か

例えばHerokuのRubyAPIクライアント自動生成ツールHeroics は実行時にJSON Schemaを読み込むのではなく、 JSON SchemaをもとにAPIクライアント用のソースコードを(ERBで)自動生成するアプローチを取っている。 静的型付言語の場合はともかく、Rubyでは実行時にクラスやメソッドを動的に定義するというこが可能なので、 どちらのアプローチをとっても同じような機能は実現できる。 ただバージョン管理の方法であったり、生成されたクライアントを拡張する方法など、 主に運用面の使い勝手が変わってくると思う。 個人的には、動的定義で事足りる場合は常にそれを使うというのが良いように考え、 前述のAPIクライアントJsonismではそのようにした。 というのも、いちいちユーザにAPIクライアントのファイルをつくらせるのが手間だろうとも思ったし、 それに実行時にJSON SchemaのURLを与えれば自動的にJSON Schemaを取得してクライアントを動的定義できるとか、 更に言えばService Discovery APIから自動的に欲しいAPI用のJSON Schemaを見付けてきてクライアントが挙動を変えるとか、 そういう面白機能が実現できると良さそうだと考えたからこうした。

おわりに

JSON Schemaを使って週末にいろいろHackしてみた結果、次のようなことが可能になった。

  • ダミーAPIサーバの提供
  • ドキュメントの自動生成
  • APIクライアントの動的定義
  • APIサーバのバリデータの動的定義
  • APIサーバのレスポンスの自動テスト

実装の自動化についてはまだまだ可能性はありそうで、 例えば「このアプリはこの認証システムを使いながらこのルールに従った振る舞いをするAPIを提供する」 みたいな仕様の記述が可能になれば、 それを解釈して自動的に必要なRack middleware stackを積んだアプリをデプロイするサービスがつくれるかもしれない (API Server as a Serviceだ!) し、 お気に入りのJSON Schemaを登録できたり、 主要サービスのAPI用のJSON Schemaを皆で共同編集できるサイトをつくったりもできると思う。 JSON Schemaが最高かと言われると別に全然そんなことはなくてすごい書きづらいんだけど、 働いても働いても一向に仕事が無くならないのは何かがおかしい。どんどん自動化して職を奪っていきたい。 余暇でHackして富を生もう。

広告を非表示にする