Page MenuHomePhorge

No OneTemporary

Size
8 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/open_api_spex/controller.ex b/lib/open_api_spex/controller.ex
index ada14a6..ceb2133 100644
--- a/lib/open_api_spex/controller.ex
+++ b/lib/open_api_spex/controller.ex
@@ -1,180 +1,184 @@
defmodule OpenApiSpex.Controller do
@moduledoc ~S'''
Generation of OpenAPI documentation via ExDoc documentation and tags.
## Supported OpenAPI fields
- Attribute `operationId` is automatically provided by the implementation
- and cannot be changed in any way. It is constructed as `Module.Name.function_name`
- in the same way as function references in backtraces.
-
### `description` and `summary`
Description of endpoint will be filled with documentation string in the same
manner as ExDocs, so first line will be used as a `summary` and whole
documentation will be used as `description` field.
+ ### `operation_id`
+ The action's `operation_id` can be set explicitly using a `@doc` tag.
+ If no `operation_id` is specified, it will default to the action's module path: `Module.Name.function_name`
+
### `parameters`
Parameters of the endpoint are defined by `:parameters` tag which should be
map or keyword list that is formed as:
```
[
param_name: definition
]
```
Where `definition` is `OpenApiSpex.Parameter.t()` structure or map or keyword
list that accepts the same arguments.
### `responses`
Responses are controlled by `:responses` tag. Responses must be defined as
a map or keyword list in form of:
```
%{
200 => {"Response name", "application/json", schema},
:not_found => {"Response name", "application/json", schema}
}
```
Where atoms are the same as `Plug.Conn.Status.code/1` values.
### `requestBody`
Controlled by `:request_body` parameter and is defined as a tuple in form
`{description, mime, schema}` or `{description, mime, schema, opts}` that
matches the arguments of `OpenApiSpex.Operation.request_body/3` or
`OpenApiSpex.Operation.request_body/4`, respectively.
```
@doc request_body: {
"CartUpdateRequest",
"application/vnd.api+json",
CartUpdateRequest,
required: true
}
```
### `tags`
Tags are controlled by `:tags` attribute. In contrast to other attributes, this
one will also inherit all tags defined as a module documentation attributes.
## Example
```
defmodule FooController do
use MyAppWeb, :controller
use #{inspect(__MODULE__)}
@moduledoc tags: ["Foos"]
@doc """
Endpoint summary
Endpoint description...
"""
@doc parameters: [
id: [in: :path, type: :string, required: true]
],
request_body: {"Request body to update Foo", "application/json", FooUpdateBody, required: true},
responses: [
ok: {"Foo document", "application/json", FooSchema}
]
def update(conn, %{id: id}) do
foo_params = conn.body_params
# …
end
end
```
'''
alias OpenApiSpex.Operation
defmacro __using__(_opts) do
quote do
@doc false
@spec open_api_operation(atom()) :: OpenApiSpex.Operation.t()
def open_api_operation(name),
do: unquote(__MODULE__).__api_operation__(__MODULE__, name)
defoverridable open_api_operation: 1
end
end
@doc false
@spec __api_operation__(module(), atom()) :: Operation.t() | nil
def __api_operation__(mod, name) do
with {:ok, {mod_meta, summary, docs, meta}} <- get_docs(mod, name) do
%Operation{
description: docs,
- operationId: "#{inspect(mod)}.#{name}",
+ operationId: build_operation_id(meta, mod, name),
parameters: build_parameters(meta),
requestBody: build_request_body(meta),
responses: build_responses(meta),
summary: summary,
tags: Map.get(mod_meta, :tags, []) ++ Map.get(meta, :tags, [])
}
else
_ -> nil
end
end
defp get_docs(module, name) do
{:docs_v1, _anno, _lang, _format, _module_doc, mod_meta, mod_docs} = Code.fetch_docs(module)
{_, _, _, docs, meta} =
Enum.find(mod_docs, fn
{{:function, ^name, _}, _, _, _, _} -> true
_ -> false
end)
if docs == :none do
:error
else
docs = Map.get(docs, "en", "")
[summary | _] = String.split(docs, ~r/\n\s*\n/, parts: 2)
{:ok, {mod_meta, summary, docs, meta}}
end
end
+ defp build_operation_id(meta, mod, name) do
+ Map.get(meta, :operation_id, "#{inspect(mod)}.#{name}")
+ end
+
defp build_parameters(%{parameters: params}) do
for {name, options} <- params do
{location, options} = Keyword.pop(options, :in, :query)
{type, options} = Keyword.pop(options, :type, :string)
{description, options} = Keyword.pop(options, :description, :string)
Operation.parameter(name, location, type, description, options)
end
end
defp build_parameters(_), do: []
defp build_responses(%{responses: responses}) do
for {status, {description, mime, schema}} <- responses, into: %{} do
{Plug.Conn.Status.code(status), Operation.response(description, mime, schema)}
end
end
defp build_responses(_), do: []
defp build_request_body(%{body: {name, mime, schema}}) do
IO.warn("Using :body key for requestBody is deprecated. Please use :request_body instead.")
Operation.request_body(name, mime, schema)
end
defp build_request_body(%{request_body: {name, mime, schema}}) do
Operation.request_body(name, mime, schema)
end
defp build_request_body(%{request_body: {name, mime, schema, opts}}) do
Operation.request_body(name, mime, schema, opts)
end
defp build_request_body(_), do: nil
end
diff --git a/test/controller_test.exs b/test/controller_test.exs
index 9864872..1e2f6ad 100644
--- a/test/controller_test.exs
+++ b/test/controller_test.exs
@@ -1,44 +1,49 @@
defmodule OpenApiSpex.ControllerTest do
use ExUnit.Case, async: true
alias OpenApiSpex.Controller, as: Subject
doctest Subject
@controller OpenApiSpexTest.UserControllerAnnotated
describe "Example module" do
test "exports open_api_operation/1" do
assert function_exported?(@controller, :open_api_operation, 1)
end
test "has defined OpenApiSpex.Operation for show action" do
assert %OpenApiSpex.Operation{} = @controller.open_api_operation(:update)
end
test "summary matches 'Endpoint summary'" do
op = @controller.open_api_operation(:update)
assert op.summary == "Update a user"
assert op.description == "Update a user\n\nFull description for this endpoint...\n"
end
test "has response for HTTP 200" do
assert %{responses: %{200 => _}} = @controller.open_api_operation(:update)
end
test "has parameter `:id`" do
assert %{parameters: [param]} = @controller.open_api_operation(:update)
assert param.name == :id
assert param.required
end
test "has a requestBody" do
op = @controller.open_api_operation(:update)
assert %OpenApiSpex.RequestBody{} = op.requestBody
assert op.requestBody.description == "Request body to update a User"
assert op.requestBody.required == true
assert %OpenApiSpex.MediaType{schema: schema} = op.requestBody.content["application/json"]
assert schema == OpenApiSpexTest.Schemas.User
end
+
+ test "sets the operation_id" do
+ op = @controller.open_api_operation(:show)
+ assert op.operationId == "show_user"
+ end
end
end
diff --git a/test/support/user_controller_annotated.ex b/test/support/user_controller_annotated.ex
index 85b5ca8..59f6e1c 100644
--- a/test/support/user_controller_annotated.ex
+++ b/test/support/user_controller_annotated.ex
@@ -1,20 +1,34 @@
defmodule OpenApiSpexTest.UserControllerAnnotated do
use OpenApiSpex.Controller
alias OpenApiSpexTest.Schemas.User
@moduledoc tags: ["User"]
@doc """
Update a user
Full description for this endpoint...
"""
@doc parameters: [
id: [in: :path, type: :string, required: true]
]
@doc request_body: {"Request body to update a User", "application/json", User, required: true}
@doc responses: [
ok: {"User response", "application/json", User}
]
def update(_conn, _params), do: :ok
+
+ @doc """
+ Show a user
+
+ Fuller description for this endpoint...
+ """
+ @doc operation_id: "show_user"
+ @doc parameters: [
+ id: [in: :path, type: :string, required: true]
+ ]
+ @doc responses: [
+ ok: {"User response", "application/json", User}
+ ]
+ def show(_conn, _params), do: :ok
end

File Metadata

Mime Type
text/x-diff
Expires
Tue, Nov 26, 4:35 PM (1 d, 9 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40427
Default Alt Text
(8 KB)

Event Timeline