Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F114820
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
8 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
R22 open_api_spex
Attached
Detach File
Event Timeline
Log In to Comment