从您的 Phoenix/Elixir 代码轻松生成 API 文档 – 第1部分

  2020年10月8日

本文介绍了一些技术,这些技术让您可以从您的 Phoenix 代码中高效、轻松地生成精美的 API 文档。Phoenix 是一个基于 Elixir 构建的高效 Web 框架。Elixir 是一种动态、函数式语言,旨在构建可扩展且可维护的应用程序,它利用了 Erlang VM。我知道这有很多新术语,但别担心。最终我们将与 控制器测试 一起工作,我们都了解这两个术语的含义。

简要概述我们将要做的事情:我们将使用 phoenix_swagger 库直接从我们的控制器生成 swagger 文件。然后我们将使用名为 bureaucrat 的库,它会消费该 swagger 文件,运行您的控制器测试,并生成一个包含两者(宏 + 测试)信息的 markdown 文件。最后,我们将使用 slate,它是一个静态 API 文档渲染器,我们将把生成的 markdown 文件输入给它,并从中生成精美的 HTML 文档。

PhoenixSwagger

PhoenixSwagger 是一个库,它为 phoenix web 框架提供了 swagger 集成。该库目前支持 OpenAPI 规范 2.0 版 (OAS)。OAS 的 3.0 版 尚不支持。要在 phoenix 应用程序中使用 PhoenixSwagger,只需将其添加到 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 使用 Jason 作为 JSON 库。我强烈建议这样做,因为 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 规范文件 (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
	

这个例子展示了一个非常简单的装饰示例。对于更复杂的宏,请查阅 phoenix_swagger 文档。还要注意,在 response 宏中,我没有使用数字 HTTP 代码,而是使用了一个 code(:ok) 函数,将 :ok atom 转换为数字 200。这是我个人的约定。我更喜欢阅读具有明确含义的 atom,而不是 HTTP 数字代码,因为后者我总是需要思考一段时间才能知道它代表什么。

不幸的是,当使用 PhoenixSwagger 宏装饰我的控制器时,我遇到了两个问题。

1.) 共享通用 Schema

PhoenixSwagger 没有惯用的解决方案来共享通用 Schema。Schema 是端点返回的数据结构的声明性描述。为了定义通用 Schema 的实际含义

通用 Schema 是在多个控制器中使用的 Schema。

这非常重要。如果您想保持您的 Schema DRY(不要重复自己),您必须想办法实现在控制器之间共享您的 Schema。出于必要,我不得不提出一个解决方案。最初我在 PhoenixSwagger 仓库上创建了 issue,然后我提交了一个 拉取请求 (PR),其中包含提案/解决方案的完整文档。我们仍在拉取请求中与库的作者协作,以提出最佳解决方案,但以下是提案的现状

以下是您通常为控制器定义 Schema 的方式

	
  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
	

以下是您通过通用 Schema 支持为控制器定义 Schema 的方式:

	
  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 函数,该函数返回作为唯一参数提供给函数的 Schema,并与通用 Schema 合并。有关其工作原理的更多信息,请查看我上面提到的 PR。

注意:实际解决方案在拉取请求合并之前可能会发生变化

2.) 不支持嵌套的 Phoenix 资源

这对我来说确实是一个大麻烦。Phoenix 框架支持 嵌套资源。创建嵌套资源时,您的控制器模块中会出现同名但签名不同的操作。

路由模块

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

用户控制器模块

	
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。不幸的是,我不能等着别人给我提供一个解决方案,所以我把今晚所有可用的脑细胞都调动起来,自己想出了多个解决方案。其中一个看起来很有希望,所以我继续在此基础上构建,最终它变成了一个可行理论。当然,我不得不妥协。我必须放弃 Router 模块中的嵌套资源,但我不需要动我的控制器文件,这是一个巨大的胜利。魔法就在于 defdelegate Elixir 宏

路由模块

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

用户控制器模块

	
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 就认为它有两个不同的控制器操作名称。正如我之前提到的,您不能再使用嵌套资源了,但这种嵌套是我愿意接受的代价。有关嵌套 Phoenix 资源与 PhoenixSwagger、替代解决方案或任何其他重要信息的更多信息,请关注 GitHub issue

目前所有问题都已解决,让我们再次运行以下命令,您新生成的 swagger 规范文件将包含控制器宏中定义的所有信息,并转换为 JSON 表示形式。

	       
$ mix phx.swagger.generate
	

现在 PhoenixSwagger 库中还有一个巧妙的技巧。该库包含一个 plug,其中包含了从您的 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 允许我们对 API 进行实际的 HTTP 请求,但这还不够。这正是 bureaucrat 发挥作用的地方。它可以轻松集成到我们现有的解决方案中。但那是另一个故事,也是本系列的第 2 部分。

提示 1

PhoenixSwagger 支持 x-nullable 以允许 Schema 属性或请求参数可为空

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

提示 2

避免使用实际类型和 :null atom 对来表示 Schema 属性或请求参数可为空。请改用提示 1 中提到的 x-nullable。在 swagger editor 中验证 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 文档
© . All rights reserved.