Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Alternative operation specs API #265

Merged
merged 17 commits into from
Sep 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@
transport: 2,
action_fallback: 1,
socket: 2,
render: 2
render: 2,
operation: 2,
tags: 1,
security: 1
],
export: [
locals_without_parens: [
operation: 2,
tags: 1,
security: 1
]
]
]
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ For each plug (controller) that will handle API requests, operations need
to be defined that the plug/controller will handle. The operations can
be defined using moduledoc attributes that are supported in Elixir 1.7 and higher.

Note: For projects using Elixir releases, [#242](there is an issue) that
potentially breaks OpenApiSpex's integration with your application. See the next section
for work-arounds to this issue.

```elixir
defmodule MyAppWeb.UserController do
@moduledoc tags: ["users"]
Expand Down Expand Up @@ -164,13 +168,24 @@ The definitions data is cached, so it does not actually extract the definitions
Note that in the ExDoc-based definitions, the names of the OpenAPI fields follow `snake_case` naming convention instead of
OpenAPI's (and JSON Schema's) `camelCase` convention.

### Alternatives to ExDoc-Based Operation Specs

#### %Operation{}

If the ExDoc-based operation specs don't provide the flexibiliy you need, the `%Operation{}` struct
and related structs can be used instead. See the
[example user controller that uses `%Operation{}` structs]([example web app](https://github.com/open-api-spex/open_api_spex/blob/master/examples/phoenix_app/lib/phoenix_app_web/controllers/user_controller_with_struct_specs.ex).)

For examples of other action operations, see the
[example web app](https://github.com/open-api-spex/open_api_spex/blob/master/examples/phoenix_app/lib/phoenix_app_web/controllers/user_controller.ex).

#### Experimental API

There is a new, experimental Operation spec API that has the same lightweight syntax
as the ExDoc-based API, but without the potentially breaking issue described in
[issue #242](https://github.com/open-api-spex/open_api_spex/issues/242).
This new API is described in the `OpenApiSpex.ControllerSpecs` docs.

### Schemas

Next, declare JSON schema modules for the request and response bodies.
Expand Down
128 changes: 7 additions & 121 deletions lib/open_api_spex/controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ defmodule OpenApiSpex.Controller do
```
'''

alias OpenApiSpex.{Operation, Parameter, Response, Reference}
alias OpenApiSpex.{Operation, OperationBuilder}

defmacro __using__(_opts) do
quote do
Expand All @@ -177,12 +177,12 @@ defmodule OpenApiSpex.Controller do
%Operation{
summary: summary,
description: description,
operationId: build_operation_id(meta, mod, name),
parameters: build_parameters(meta),
requestBody: build_request_body(meta),
responses: build_responses(meta),
security: build_security(meta),
tags: Map.get(mod_meta, :tags, []) ++ Map.get(meta, :tags, [])
operationId: OperationBuilder.build_operation_id(meta, mod, name),
parameters: OperationBuilder.build_parameters(meta),
requestBody: OperationBuilder.build_request_body(meta),
responses: OperationBuilder.build_responses(meta),
security: OperationBuilder.build_security(meta, mod_meta),
tags: OperationBuilder.build_tags(meta, mod_meta)
}
else
_ -> nil
Expand Down Expand Up @@ -230,118 +230,4 @@ defmodule OpenApiSpex.Controller do
nil
end
end

defp ensure_type_and_schema_exclusive!(name, type, schema) do
if type != nil && schema != nil do
raise ArgumentError,
message: """
Both :type and :schema options were specified for #{inspect(name)}. Please specify only one.
:type is a shortcut for base data types https://swagger.io/docs/specification/data-models/data-types/
which at the end imports as `%Schema{type: type}`. For more control over schemas please
use @doc parameters: [
id: [in: :path, schema: MyCustomSchema]
]
"""
end
end

defp build_operation_id(meta, mod, name) do
Map.get(meta, :operation_id, "#{inspect(mod)}.#{name}")
end

defp build_parameters(%{parameters: params}) when is_list(params) or is_map(params) do
params
|> Enum.reduce([], fn
parameter = %Parameter{}, acc ->
[parameter | acc]

ref = %Reference{}, acc ->
[ref | acc]

{:"$ref", ref = "#/components/parameters/" <> _name}, acc ->
[%Reference{"$ref": ref} | acc]

{name, options}, acc ->
{location, options} = Keyword.pop(options, :in, :query)
{type, options} = Keyword.pop(options, :type, nil)
{schema, options} = Keyword.pop(options, :schema, nil)
{description, options} = Keyword.pop(options, :description, "")

ensure_type_and_schema_exclusive!(name, type, schema)

schema = type || schema || :string

[Operation.parameter(name, location, schema, description, options) | acc]

unsupported, acc ->
IO.warn("Invalid parameters declaration found: " <> inspect(unsupported))

acc
end)
|> Enum.reverse()
end

defp build_parameters(%{parameters: _params}) do
IO.warn("""
parameters tag should be map or list, for example:
@doc parameters: [
id: [in: :path, schema: MyCustomSchema]
]
""")

[]
end

defp build_parameters(_), do: []

defp build_responses(%{responses: responses}) when is_list(responses) or is_map(responses) do
Map.new(responses, fn
{status, {description, mime, schema}} ->
{Plug.Conn.Status.code(status), Operation.response(description, mime, schema)}

{status, {description, mime, schema, opts}} ->
{Plug.Conn.Status.code(status), Operation.response(description, mime, schema, opts)}

{status, %Response{} = response} ->
{Plug.Conn.Status.code(status), response}

{status, description} when is_binary(description) ->
{Plug.Conn.Status.code(status), %Response{description: description}}
end)
end

defp build_responses(%{responses: _responses}) do
IO.warn("""
responses tag should be map or keyword list, for example:
@doc responses: %{
200 => {"Response name", "application/json", schema},
:not_found => {"Response name", "application/json", schema}
}
""")

[]
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

defp build_security(%{security: security}) do
security
end

defp build_security(_), do: nil
end
Loading