皆様、WebAPIのドキュメントはどのように書かれているでしょうか?
WikiとMarkdownでしょうか?Excelでしょうか?それとも、OpenAPIだったりするのでしょうか??
どの方法であれ、正しく運用されていれば問題はありませんね。
問題ないとはいえ、出来れば余分なコストはかけずにドキュメンテーションは行いたいものです。
OpenAPI(Swagger)
WebAPIのドキュメントといえば真っ先に思いつくのがOpenAPI(Swagger)ではないでしょうか?
OpenAPIの仕様に基づいてWebAPIのドキュメントを書くことで、様々なツールを利用した恩恵を受けることができるようになります。
例えば以下のような恩恵が受け取れます。
これはOpenAPIで書きたくなりますね!
- 綺麗なレイアウトのHTMLドキュメント
- WebAPIモック
- AWSなどIaaSとの連携
既存システムへの導入の辛さ
OpenAPIの仕様に基づいてドキュメントを書くと受け取れる恩恵が大きいことは先に書きました。
この恩恵を受け取りたいと考え、既存システムのWebAPIドキュメントもSwaggerで書き直したいと思うのが人情でしょう。
ですが、これは以下のような理由から中々うまくいくものではありません。
- 既存システムの場合、WebAPIの仕様書が更新されておらずOpenAPIの仕様でドキュメンテーションするにも実装を都度確認になければならないことが多々ある
- 既存システムの場合、その歴史の長さから多数のWebAPIが存在し、それらをドキュメンテーションしていく中で人間のモチベーションが朽ち果てる
- OpenAPIはRESTfulAPIを記述することを前提にしているため、RESTfulで設計されていないWebAPIをOpenAPIの仕様に合わせて書いていくにはちょっとした工夫が必要になる
一部、既存システムのWebAPIドキュメントをOpenAPI化出来たという事例も聞きますが、
これは完全に特殊ケースだと考えており、一般的な企業のエンジニアリソース量であったり、一般的な人間のモチベーションの量ではOpenAPI化を完遂するのは困難だろうと考えています。
Presentation層
唐突にPresentation層のお話を書きますが、しばしお付き合いください。
多くのMVCを採用するWebアプリケーションは、View(というかテンプレート)がModelと直接やり取りをすることはなく、
ControllerがModelとやり取りし、その結果をViewに渡し、Viewのヘルパーやテンプレートエンジンの機能を利用してデータを出力していることと思います。
ここで言うPresentation層1とは、ControllerがModelとやり取りするのを肩代わりし、Viewが行うヘルパーやテンプレートエンジンで行う複雑な機能を肩代わりする層のことです。
と書いても分かりにくいかもしれないので、簡易的な実装を見てみましょう。
class UserProfileController
def index
user = User.find(1)
@presenter = UserPresenter.new(user)
end
end
class UserPresenter
def initializer(user)
@user = user
end
def name
@user.name
end
def age
return "ヒ・ミ・ツ" unless @user.is_show_age
@user.age
end
end
<dl>
<dd><%= presenter.name %></dd>
<dd><%= presenter.age %></dd>
</dl>
このような感じで、UserモデルがViewに現れるのを防いだり、表示に関するロジックを集約したりすることに役立ってくれるのがPresentation層です。
このスタイルはWebAPIのレスポンススタイルにも適用可能で、
例えばテンプレートエンジンでjsonをレンダリングする場合は以下のようになるでしょう。
{
name: <%= presenter.name %>,
age: <%= presenter.age %>
}
クラス図としてみれば以下のようになり、
+----------------+
| UserPresenter |
+----------------+
| + name: String |
| + age: String |
+----------------+
JSONのフォーマットとしては以下のように表現できます。
{
"name": "string",
"age": "string"
}
つまり、表示するためのデータをPresentationクラスに集約しさえすれば、
Viewへの参照可能なデータを定義したIFと見なすことが出来、
出力がJSONである時、このIFはWebAPI出力部分の仕様と同等の情報を持つことが出来るのです。
PresenterからWebAPIドキュメントへ
さて、PresenterクラスがWebAPIのIFとなり得るIFを持つことはご理解頂けたのではないかと思います。
ここからは、このクラスからWebAPIドキュメントを生成する方法について考えてみましょう。
DocCommentを利用する
余程古来から存在するプログラミング言語でなければDocCommentからドキュメントを生成するようなツールが付属、あるいはライブラリとして提供されています。
このツールの中間結果を利用することでWebAPIドキュメントを生成してみます。
(今回はRubyのyardというツールを使っています)
以下のようなPresenterクラスを用意してDocCommentを書いていきます。
# User API
# @api { method: :post }
class UserPresenter
def initializer(user)
@user = user
end
# User name
# @return [String] user name
def name
@user.name
end
# User age
# @return [Fixnum] user age
def age
@user.age
end
end
このように書いておくことで、ソースコードのドキュメントを出力出来るようになります。
この時、利用するツール内ではクラスに対するDocCommentの情報、メソッドに対するDocCommentの情報が扱いやすい形で内部に存在しているのでこれを利用してWebAPIのドキュメントに必要な情報に変換していくことにします。
Swaggerスタイルの出力を行う
今回はこのDocCommentのツールが提供する情報を変換してSwaggerドキュメントへと変換を行います。
以下のような簡単なコードを用意するだけでOKです。
require "yard"
require "json"
swagger = {
swagger: "2.0",
info: {
version: "1.0.0",
title: "WebAPI Document",
},
paths: {},
definitions: {},
}
type_map = {
String: { type: :string },
Fixnum: { type: :integer, format: :int64 },
}
file = "sample/user_presenter.rb"
YARD.parse(file)
YARD::Registry.all(:class).each do |c|
name = c.name.to_s.gsub("Presenter", "")
path = "/#{name.downcase}"
swagger[:paths][path] = {}
api_info = eval(c.tag(:api).text)
swagger[:paths][path][api_info[:method]] = {
description: c.docstring.to_s,
responses: {
"200": {
description: "successful",
schema: { "$ref": "#/definitions/#{name}" }
}
}
}
swagger[:definitions][name] = {
type: :object,
properties: {},
}
c.meths.each do |m|
next if m.name == :initializer
tag = m.tag(:return)
type_info = type_map[tag.types[0].to_sym]
swagger[:definitions][name][:properties][m.name] = {}
swagger[:definitions][name][:properties][m.name][:type] = type_info[:type]
swagger[:definitions][name][:properties][m.name][:format] = type_info[:format] if type_info[:format]
swagger[:definitions][name][:properties][m.name][:description] = tag.text
end
end
puts swagger.to_json
この結果は以下のようになります。
{
"swagger": "2.0",
"info": {
"version": "1.0.0",
"title": "WebAPI Document"
},
"paths": {
"/user": {
"post": {
"description": "User API",
"responses": {
"200": {
"description": "successful",
"schema": {
"$ref": "#/definitions/User"
}
}
}
}
}
},
"definitions": {
"User": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "user name"
},
"age": {
"type": "integer",
"format": "int64",
"description": "user age"
}
}
}
}
}
あとはこれをOpenAPIの仕様に基づいてドキュメントを生成してくれるツールに投入すればWebAPIドキュメントの出来上がりです。
まとめ
Presenter層の導入からWebAPIドキュメント(Swaggerの定義)の出力までを行いました。
この方法は、
- 既存ソースのリファクタリング
- DocCommentの導入によるソースコード仕様の明確化
- WebAPIドキュメントの自動生成
と一石三鳥のメリットがあり、自前でWebAPIドキュメントを書いていくより非常に大きなメリットをもたらすことが出来ます。
DocCommentのツールから状態を取得するというステップがややコストがかかるものの、WebAPIのドキュメント生成にあっかるトータルの作業コストを考慮すれば十分にペイできるものなのでWebAPIドキュメントを手書きで頑張ろうとする前に一考してみてはいかがでしょうか?
-
OSI参照モデルの第6層のことではなく、MVC2のPresenterのことを指しています ↩