Phoenix/ElixirコードからAPIドキュメントを簡単に生成 – パート1

  2020年10月8日

この記事は、Phoenixコードから直接、美しく、効果的かつ簡単にAPIドキュメントを生成できる事実とテクノロジーについて書かれています。PhoenixはElixirで構築された生産性の高いWebフレームワークです。Elixirは、Erlang VMを活用してスケーラブルで保守性の高いアプリケーションを構築するために設計された、動的で関数型言語です。多くの新しい用語が出てきましたが、心配はいりません。最終的にはコントローラテストを扱っていくことになりますが、これらの用語の意味は皆さんもご存知でしょう。

私たちがこれから何をするのか、簡単に概要を説明します。まず、phoenix_swaggerライブラリを使って、コントローラから直接Swaggerファイルを生成します。次に、そのSwaggerファイルを消費し、コントローラテストを実行して、両方(マクロ+テスト)からの情報を含むマークダウンファイルを生成するbureaucratというライブラリを使用します。最後に、静的なAPIドキュメントレンダラーであるslateを使い、生成されたマークダウンファイルをフィードして、そこから美しいHTMLドキュメントを生成します。

PhoenixSwagger

PhoenixSwaggerは、Phoenix WebフレームワークにSwagger統合を提供するライブラリです。このライブラリは現在、OpenAPI Specificationバージョン2.0 (OAS) をサポートしています。OASのバージョン3.0はまだサポートされていません。PhoenixSwaggerをPhoenixアプリケーションで使用するには、mix.exsファイル内の依存関係リストに追加するだけです。


def deps do
  [
    {:phoenix_swagger, "~> 0.8"},
    {:ex_json_schema, "~> 0.5"} # optional
  ]
end

ex_json_schemaはPhoenixSwaggerのオプションの依存関係ですが、後で必要になるので、これもインストールしてください。次に、mix deps.getを実行します。

次に、Phoenixアプリケーションに設定エントリを追加し、Swaggerファイルの生成に使用される出力ファイル名、ルーター、エンドポイントモジュールを指定します。


config :my_app, :phoenix_swagger,
  swagger_files: %{
    "priv/static/swagger.json" => [
      router: MyAppWeb.Router, 
      endpoint: MyAppWeb.Endpoint
    ]
  }


PhoenixSwaggerをJSONライブラリとしてJasonを使用するように設定することもできます。JasonはPhoenixのデフォルトJSONライブラリであり、主要なJSON ElixirライブラリとしてPoisonよりも採用が進んでいるように見えるため、これを行うことを強くお勧めします。


config :phoenix_swagger, json_library: Jason

次に、Swaggerドキュメントのアウトラインを作成する必要があります。このアウトラインは、APIに関する一般情報、名前、バージョン、利用規約、セキュリティ定義などを想像してください。アウトラインは、Routerモジュールで定義された関数 swagger_info/0 から返されるマップとして実装されます。


defmodule MyApp.Router do
  use MyApp.Web, :router


  pipeline :api do 
   plug :accepts, ["json"]
  end

  scope "/api", MyApp do
    pipe_through :api
    resources "/users", UserController
  end 

  def swagger_info do
    %{
      schemes: ["http", "https", "ws", "wss"],
      info: %{
        version: "1.0",
        title: "MyAPI",
        description: "API Documentation for MyAPI v1",
        termsOfService: "Open for public",
        contact: %{
          name: "Vladimir Gorej",
          email: "[email protected]"
        }
      },
      securityDefinitions: %{
        Bearer: %{
          type: "apiKey",
          name: "Authorization",
          description:
          "API Token must be provided via `Authorization: Bearer ` header",
      in: "header"
        }
      },
      consumes: ["application/json"],
      produces: ["application/json"],
      tags: [
        %{name: "Users", description: "User resources"},
      ]
    }
  end
end    


その他の含めることができる情報の詳細については、Swaggerオブジェクト仕様を参照してください。次に、以下のコマンドを実行すると、swagger specファイル (swagger.json) が ./priv/static/ ディレクトリに生成されます。

	
$ mix phx.swagger.generate
	

もちろん、この時点では、生成されたSwagger仕様ファイルには、Routerモジュール内の`swagger_info/0`関数によって提供された情報しか含まれていません。実際に価値を与えるには、コントローラのアクションをPhoenixSwaggerマクロで装飾する必要があります。ここではその方法については詳しく説明しません。phoenix_swaggerドキュメントは、これらのマクロを適切に作成するための十分なガイダンスを提供しています。

	
import Plug.Conn.Status, only: [code: 1]
use PhoenixSwagger

swagger_path :index do
  get("/users")
  description("List of users")
  response(code(:ok), "Success")
end

def index(conn, _params) do
  users = Repo.all(User)
  render(conn, :index, users: users)
end
	

この例は、非常にシンプルな装飾の例を示しています。より複雑なマクロについては、PhoenixSwaggerのドキュメントを参照してください。また、responseマクロで数値HTTPコードの代わりに、`:ok`アトムを数値の200に変換する`code(:ok)`関数を使用していることにも注目してください。これは私の個人的な慣習です。HTTP数値コードは常にそのコードが何を表すのかしばらく考える必要があるため、明示的な意味を持つアトムを読む方が好きです。

残念ながら、PhoenixSwaggerマクロでコントローラを装飾する際に2つの問題に遭遇しました。

1.) 共通スキーマの共有

PhoenixSwaggerには、共通のスキーマを共有するためのイディオム的な解決策がありません。スキーマとは、エンドポイントが返すデータ構造の宣言的な記述です。共通スキーマとは何かを定義するために

共通スキーマとは、複数のコントローラで使用されるスキーマのことです。

これは覚えておくことが非常に重要です。スキーマをDRYに保ちたい場合は、コントローラ間でスキーマを共有する方法を考案する必要があります。必要に迫られ、私は解決策を考案しました。最初はPhoenixSwaggerリポジトリ課題を作成し、その後、提案/解決策の完全なドキュメントを含むプルリクエスト(PR)を提供しました。最善の解決策を見つけるために、プルリクエスト内でライブラリの作者とまだ共同作業していますが、以下の提案の現状です。

コントローラにスキーマを従来の方法で定義する方法は次のとおりです。

	
  def swagger_definitions do
  %{
    User:
      swagger_schema do
        title("User")
        description("A user of the application")

        properties do
          name(:string, "Users name", required: true)
        end
      end
  }
end
	

これは、共通スキーマをサポートするコントローラにスキーマを定義する方法です。:

	
  def swagger_definitions do
  create_swagger_definitions(%{
    User:
      swagger_schema do
        title("User")
        description("A user of the application")

        properties do
          name(:string, "Users name", required: true)
        end
      end
  })
end
	

その違いは微妙ですが、非常に重要です。マップを返す代わりに、create_swagger_definitions/1関数を呼び出します。この関数は、唯一の引数として関数に提供されたスキーマと共通スキーマをマージして返します。この仕組みの詳細については、前述のPRを参照してください。

注:プルリクエストが実際にマージされる前に、実際の解決策は変更される可能性があります

2.) ネストされたPhoenixリソースの非サポート

これは私にとって本当に大きな痛手でした。Phoenixフレームワークはネストされたリソースをサポートしています。ネストされたリソースを作成すると、コントローラモジュール内に同じ名前だが異なるシグネチャを持つアクションを持つことになります。

ルーターモジュール

	
resources "/groups", GroupController, only: 
[:show] do
resources "/users", UserController, only: 
[:index]
end
	

UserControllerモジュール

	
swagger_path :index do
  get("/groups/{group_id}/users")
  description("List of users specific to group")
  response(code(:ok), "Success")
end

def index(conn, %{"group_id" => group_id}) do
  ...controller body...
end

# vs

swagger_path :index do
  get("/users")
  description("List of users")
  response(code(:ok), "Success")
end

def index(%Plug.Conn{} = conn, params) do
  ...controller body...
end
	

ご覧のとおり、パターンマッチングを使用して、適切なコントローラーアクションをルーターマッピングに一致させます。ここに問題があります。PhoenixSwaggerは、同じ名前(:index)のコントローラーアクションに対してマクロを作成できません。最初のマクロは常に:indexアクションに一致します。


この問題が起こりうると知ったとき、最初に私がしたのは、PhoenixSwaggerリポジトリにissueを作成することでした。残念ながら、誰かが回避策を提供してくれるのを待つことができなかったので、その夜に残っていた脳細胞をすべて使い、複数の回避策を自分で考案しました。ある回避策が有望に思えたので、それを基に構築を続け、最終的には実用的な理論になりました。もちろん、妥協しなければなりませんでした。ルーターモジュールからネストされたリソースを削除する必要がありましたが、コントローラーファイルを変更する必要がなかったのは大きな勝利でした。この魔法はdefdelegate Elixirマクロにあります。

ルーターモジュール

	
resources "/groups", GroupController, only: [:show]
get "/groups/:group_id/users", UserController, :index_by_group
	

UserControllerモジュール

	
defdelegate index_by_group(conn, params),
  to: UserController,
  as: :index

swagger_path :index_by_group do
  get("/groups/{group_id}/users")
  description("List of users specific to group")
  response(code(:ok), "Success")
end

def index(conn, %{"group_id" => group_id}) do
  ...controller body...
end

# vs

swagger_path :index do
  get("/users")
  description("List of users")
  response(code(:ok), "Success")
end


def index(%Plug.Conn{} = conn, params) do
   ...controller body...
end
	

defdelegateを使うことで、:index関数のいずれかにローカルエイリアスを作成でき、PhoenixSwaggerマクロはこの委譲を消費します。そうすることで、PhoenixSwaggerは2つの異なるコントローラアクション名を持っていると認識します。前述したように、ネストされたリソースはもう使用できませんが、ネストは私が受け入れる覚悟のある代償でした。ネストされたPhoenixリソースとPhoenixSwagger、代替ソリューション、その他の重要な情報については、GitHubのissueを参照してください。

これで全ての問題が解決しました。もう一度以下のコマンドを実行すると、新しく生成されたswagger specファイルには、コントローラーマクロで定義されたすべての情報がJSON形式に変換されて含まれます。

	       
$ mix phx.swagger.generate
	

PhoenixSwaggerライブラリの袖には、最後の巧みなトリックがあります。このライブラリには、PhoenixアプリケーションからSwaggerUIをホストするために必要なすべての静的アセットを含むプラグが含まれています。ルーターにswaggerスコープを追加し、すべてのリクエストをSwaggerUIに転送します。

	
scope "/api/swagger" do
  forward "/", PhoenixSwagger.Plug.SwaggerUI,
    otp_app: :my_app,
    swagger_file: "swagger.json"
end
	

mix phx.server でサーバーを起動し、 localhost:4000/api/swagger にアクセスしてください。SwaggerUIが表示され、Swagger仕様ファイルがロードされます。

SwaggerUIを使用せずにSwagger仕様ファイルにアクセスするには、次のURLにアクセスしてください。localhost:4000/api/swagger/swagger.json。

これで、PhoenixSwaggerライブラリの主な機能と制限が理解できたでしょう。ここまで読み進めていれば、コードから直接APIドキュメントを利用し、生成する方法はすでに知っているはずです。個人的には、PhoenixSwaggerマクロは、コントローラーコードを実際に読むことなく、コントローラーマクロを見るだけで、コントローラーが何を行い、どのようなステータスコードを返すかを素早く理解する必要があるときに役立ちます。

現在欠けている唯一の点は、マクロで装飾したPhoenixコントローラのアクションごとに、生成されたSwagger仕様ファイルにAPI呼び出しの例(リクエスト/レスポンスのペア)がないことです。確かに、SwaggerUIを使えば実際のHTTPリクエストをAPIに対して実行できますが、それでは不十分です。そして、まさにこの点でbureaucratが役立ちます。既存のソリューションに手間なく統合できます。しかし、それは別の機会、このシリーズのパート2でのお話です。

ヒント 1

PhoenixSwagger は、Schema のプロパティやリクエストパラメータを nullable にするために、x-nullable をサポートしています。

	
last_modified(:string, "Datetime of video file last modification",
  format: "date-time",
  "x-nullable": true
)
	

ヒント2

スキーマプロパティまたはリクエストパラメータがnullableであることを表現するために、実際の型と:nullアトムのペアを使用することは避けてください。代わりに、ヒント1で述べたx-nullableを使用してください。このように定義されたフィールドは、SwaggerエディターでSwagger仕様ファイルを検証する際にエラーを生成します。

	
      
# Don't do this
last_modified([:string, :null], "Datetime of video file last modification",
  format: "date-time"
)
	

この記事が気に入ったら、Twitterでフォローしてください:@vladimirgorej

 

Phoenix/ElixirコードからAPIドキュメントを簡単に生成するシリーズ

  • パート1:Phoenix Swagger
  • パート2:Bureaucrat - テストからのAPIドキュメント
  • パート3:Slate - 美しい静的HTMLドキュメント