7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

DiverseAdvent Calendar 2019

Day 4

手書きのWebAPI仕様書とPresentationLayerとWebAPI

Last updated at Posted at 2019-12-04

皆様、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を書いていきます。

UserPresenter.rb
# 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ドキュメントを手書きで頑張ろうとする前に一考してみてはいかがでしょうか?

  1. OSI参照モデルの第6層のことではなく、MVC2のPresenterのことを指しています

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?