Page MenuHomePhorge

No OneTemporary

Size
27 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/open_api_spex/controller.ex b/lib/open_api_spex/controller.ex
new file mode 100644
index 0000000..bb18d32
--- /dev/null
+++ b/lib/open_api_spex/controller.ex
@@ -0,0 +1,158 @@
+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.
+
+ ### `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 `:body` parameter and is defined as a tuple in form
+ `{description, mime, schema}`.
+
+ ### `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 #{inspect(__MODULE__)}
+
+ @moduledoc tags: ["Foos"]
+
+ @doc """
+ Endpoint summary
+
+ More docs
+ """
+ @doc [
+ parameters: [
+ id: [in: :path, type: :string, required: true]
+ ],
+ responses: [
+ ok: {"Foo document", "application/json", FooSchema}
+ ]
+ ]
+ def show(conn, %{id: id}) do
+ # …
+ 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}",
+ 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_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: Operation.request_body(name, mime, schema)
+
+ defp build_request_body(_), do: nil
+end
diff --git a/lib/open_api_spex/operation.ex b/lib/open_api_spex/operation.ex
index 9009701..e1f8d69 100644
--- a/lib/open_api_spex/operation.ex
+++ b/lib/open_api_spex/operation.ex
@@ -1,233 +1,233 @@
defmodule OpenApiSpex.Operation do
@moduledoc """
Defines the `OpenApiSpex.Operation.t` type.
"""
alias OpenApiSpex.{
Callback,
ExternalDocumentation,
MediaType,
Operation,
Parameter,
Reference,
RequestBody,
Response,
Responses,
Schema,
SecurityRequirement,
Server,
}
@enforce_keys :responses
defstruct tags: [],
summary: nil,
description: nil,
externalDocs: nil,
operationId: nil,
parameters: [],
requestBody: nil,
responses: nil,
callbacks: %{},
deprecated: false,
security: nil,
servers: nil
@typedoc """
[Operation Object](https://swagger.io/specification/#operationObject)
Describes a single API operation on a path.
"""
@type t :: %__MODULE__{
tags: [String.t()],
summary: String.t() | nil,
description: String.t() | nil,
externalDocs: ExternalDocumentation.t() | nil,
operationId: String.t() | nil,
parameters: [Parameter.t() | Reference.t()],
requestBody: RequestBody.t() | Reference.t() | nil,
responses: Responses.t(),
callbacks: %{String.t() => Callback.t() | Reference.t()},
deprecated: boolean,
security: [SecurityRequirement.t()] | nil,
servers: [Server.t()] | nil
}
@doc """
Constructs an Operation struct from the plug and opts specified in the given route
"""
- @spec from_route(PathItem.route) :: t
+ @spec from_route(PathItem.route()) :: t | nil
def from_route(route)
def from_route(route = %_{}) do
route
|> Map.from_struct()
|> from_route()
end
def from_route(%{plug: plug, plug_opts: opts}) do
from_plug(plug, opts)
end
def from_route(%{plug: plug, opts: opts}) do
from_plug(plug, opts)
end
@doc """
Constructs an Operation struct from plug module and opts
"""
- @spec from_plug(module, any) :: t
+ @spec from_plug(module, any) :: t | nil
def from_plug(plug, opts) do
plug.open_api_operation(opts)
end
@doc """
Shorthand for constructing a Parameter name, location, type, description and optional examples
"""
@spec parameter(atom, Parameter.location, Reference.t | Schema.t | atom, String.t, keyword) :: Parameter.t
def parameter(name, location, type, description, opts \\ []) do
params =
[name: name, in: location, description: description, required: location == :path]
|> Keyword.merge(opts)
Parameter
|> struct(params)
|> Parameter.put_schema(type)
end
@doc """
Shorthand for constructing a RequestBody with description, media_type, schema and optional examples
"""
@spec request_body(String.t, String.t, (Schema.t | Reference.t | module), keyword) :: RequestBody.t
def request_body(description, media_type, schema_ref, opts \\ []) do
%RequestBody{
description: description,
content: %{
media_type => %MediaType{
schema: schema_ref,
example: opts[:example],
examples: opts[:examples]
}
},
required: opts[:required] || false
}
end
@doc """
Shorthand for constructing a Response with description, media_type, schema and optional examples
"""
@spec response(String.t, String.t, (Schema.t | Reference.t | module), keyword) :: Response.t
def response(description, media_type, schema_ref, opts \\ []) do
%Response{
description: description,
content: %{
media_type => %MediaType {
schema: schema_ref,
example: opts[:example],
examples: opts[:examples]
}
}
}
end
@doc """
Cast params to the types defined by the schemas of the operation parameters and requestBody
"""
@spec cast(Operation.t, Conn.t, String.t | nil, %{String.t => Schema.t}) :: {:ok, Plug.Conn.t} | {:error, String.t}
def cast(operation = %Operation{}, conn = %Plug.Conn{}, content_type, schemas) do
parameters = Enum.filter(operation.parameters || [], fn p -> Map.has_key?(conn.params, Atom.to_string(p.name)) end)
with :ok <- check_query_params_defined(conn, operation.parameters),
{:ok, parameter_values} <- cast_parameters(parameters, conn.params, schemas),
{:ok, body} <- cast_request_body(operation.requestBody, conn.body_params, content_type, schemas) do
{:ok, %{conn | params: parameter_values, body_params: body}}
end
end
@spec check_query_params_defined(Conn.t, list | nil) :: :ok | {:error, String.t}
defp check_query_params_defined(%Plug.Conn{} = conn, defined_params) when is_nil(defined_params) do
case conn.query_params do
%{} -> :ok
_ -> {:error, "No query parameters defined for this operation"}
end
end
defp check_query_params_defined(%Plug.Conn{} = conn, defined_params) when is_list(defined_params) do
defined_query_params = for param <- defined_params, param.in == :query, into: MapSet.new(), do: to_string(param.name)
case validate_parameter_keys(Map.keys(conn.query_params), defined_query_params) do
{:error, param} -> {:error, "Undefined query parameter: #{inspect(param)}"}
:ok -> :ok
end
end
@spec validate_parameter_keys([String.t], MapSet.t) :: {:error, String.t} | :ok
defp validate_parameter_keys([], _defined_params), do: :ok
defp validate_parameter_keys([param|params], defined_params) do
case MapSet.member?(defined_params, param) do
false -> {:error, param}
_ -> validate_parameter_keys(params, defined_params)
end
end
@spec cast_parameters([Parameter.t], map, %{String.t => Schema.t}) :: {:ok, map} | {:error, String.t}
defp cast_parameters([], _params, _schemas), do: {:ok, %{}}
defp cast_parameters([p | rest], params = %{}, schemas) do
with {:ok, cast_val} <- Schema.cast(Parameter.schema(p), params[Atom.to_string(p.name)], schemas),
{:ok, cast_tail} <- cast_parameters(rest, params, schemas) do
{:ok, Map.put_new(cast_tail, p.name, cast_val)}
end
end
@spec cast_request_body(RequestBody.t | nil, map, String.t | nil, %{String.t => Schema.t}) :: {:ok, map} | {:error, String.t}
defp cast_request_body(nil, _, _, _), do: {:ok, %{}}
defp cast_request_body(%RequestBody{content: content}, params, content_type, schemas) do
schema = content[content_type].schema
Schema.cast(schema, params, schemas)
end
@doc """
Validate params against the schemas of the operation parameters and requestBody
"""
@spec validate(Operation.t, Conn.t, String.t | nil, %{String.t => Schema.t}) :: :ok | {:error, String.t}
def validate(operation = %Operation{}, conn = %Plug.Conn{}, content_type, schemas) do
with :ok <- validate_required_parameters(operation.parameters || [], conn.params),
parameters <- Enum.filter(operation.parameters || [], &Map.has_key?(conn.params, &1.name)),
:ok <- validate_parameter_schemas(parameters, conn.params, schemas),
:ok <- validate_body_schema(operation.requestBody, conn.body_params, content_type, schemas) do
:ok
end
end
@spec validate_required_parameters([Parameter.t], map) :: :ok | {:error, String.t}
defp validate_required_parameters(parameter_list, params = %{}) do
required =
parameter_list
|> Stream.filter(fn parameter -> parameter.required end)
|> Enum.map(fn parameter -> parameter.name end)
missing = required -- Map.keys(params)
case missing do
[] -> :ok
_ -> {:error, "Missing required parameters: #{inspect(missing)}"}
end
end
@spec validate_parameter_schemas([Parameter.t()], map, %{String.t() => Schema.t()}) ::
:ok | {:error, String.t()}
defp validate_parameter_schemas([], %{} = _params, _schemas), do: :ok
defp validate_parameter_schemas([p | rest], %{} = params, schemas) do
{:ok, parameter_value} = Map.fetch(params, p.name)
with :ok <- Schema.validate(Parameter.schema(p), parameter_value, schemas) do
validate_parameter_schemas(rest, params, schemas)
end
end
@spec validate_body_schema(RequestBody.t | nil, map, String.t | nil, %{String.t => Schema.t}) :: :ok | {:error, String.t}
defp validate_body_schema(nil, _, _, _), do: :ok
defp validate_body_schema(%RequestBody{required: false}, params, _content_type, _schemas) when map_size(params) == 0 do
:ok
end
defp validate_body_schema(%RequestBody{content: content}, params, content_type, schemas) do
content
|> Map.get(content_type)
|> Map.get(:schema)
|> Schema.validate(params, schemas)
end
end
diff --git a/lib/open_api_spex/path_item.ex b/lib/open_api_spex/path_item.ex
index 670513e..51893f9 100644
--- a/lib/open_api_spex/path_item.ex
+++ b/lib/open_api_spex/path_item.ex
@@ -1,74 +1,86 @@
defmodule OpenApiSpex.PathItem do
@moduledoc """
Defines the `OpenApiSpex.PathItem.t` type.
"""
alias OpenApiSpex.{Operation, Server, Parameter, PathItem, Reference}
+
defstruct [
:"$ref",
:summary,
:description,
:get,
:put,
:post,
:delete,
:options,
:head,
:patch,
:trace,
:servers,
:parameters
]
@typedoc """
[Path Item Object](https://swagger.io/specification/#pathItemObject)
Describes the operations available on a single path.
A Path Item MAY be empty, due to ACL constraints.
The path itself is still exposed to the documentation viewer
but they will not know which operations and parameters are available.
"""
@type t :: %__MODULE__{
- "$ref": String.t | nil,
- summary: String.t | nil,
- description: String.t | nil,
- get: Operation.t | nil,
- put: Operation.t | nil,
- post: Operation.t | nil,
- delete: Operation.t | nil,
- options: Operation.t | nil,
- head: Operation.t | nil,
- patch: Operation.t | nil,
- trace: Operation.t | nil,
- servers: [Server.t] | nil,
- parameters: [Parameter.t | Reference.t] | nil
- }
+ "$ref": String.t() | nil,
+ summary: String.t() | nil,
+ description: String.t() | nil,
+ get: Operation.t() | nil,
+ put: Operation.t() | nil,
+ post: Operation.t() | nil,
+ delete: Operation.t() | nil,
+ options: Operation.t() | nil,
+ head: Operation.t() | nil,
+ patch: Operation.t() | nil,
+ trace: Operation.t() | nil,
+ servers: [Server.t()] | nil,
+ parameters: [Parameter.t() | Reference.t()] | nil
+ }
@typedoc """
Represents a route from in a Plug/Phoenix application.
Eg from the generated `__routes__` function in a Phoenix.Router module.
"""
@type route ::
- %{verb: atom, plug: atom, opts: any} |
- %{verb: atom, plug: atom, plug_opts: any}
+ %{verb: atom, plug: atom, opts: any}
+ | %{verb: atom, plug: atom, plug_opts: any}
@doc """
Builds a PathItem struct from a list of routes that share a path.
"""
@spec from_routes([route]) :: nil | t
def from_routes(routes) do
Enum.each(routes, fn route ->
Code.ensure_loaded(route.plug)
end)
routes
|> Enum.filter(&function_exported?(&1.plug, :open_api_operation, 1))
|> from_valid_routes()
end
@spec from_valid_routes([route]) :: nil | t
defp from_valid_routes([]), do: nil
+
defp from_valid_routes(routes) do
- struct(PathItem, Enum.map(routes, &{&1.verb, Operation.from_route(&1)}))
+ attrs =
+ routes
+ |> Enum.map(fn route ->
+ case Operation.from_route(route) do
+ nil -> nil
+ op -> {route.verb, op}
+ end
+ end)
+ |> Enum.filter(& &1)
+
+ struct(PathItem, attrs)
end
end
diff --git a/lib/open_api_spex/schema_resolver.ex b/lib/open_api_spex/schema_resolver.ex
index c09eae5..f0770b4 100644
--- a/lib/open_api_spex/schema_resolver.ex
+++ b/lib/open_api_spex/schema_resolver.ex
@@ -1,185 +1,227 @@
defmodule OpenApiSpex.SchemaResolver do
@moduledoc """
Internal module used to resolve `OpenApiSpex.Schema` structs from atoms.
"""
alias OpenApiSpex.{
OpenApi,
Components,
PathItem,
Operation,
Parameter,
Reference,
MediaType,
Schema,
RequestBody,
Response
}
@doc """
Adds schemas to the api spec from the modules specified in the Operations.
Eg, if the response schema for an operation is defined with:
responses: %{
200 => Operation.response("User", "application/json", UserResponse)
}
Then the `UserResponse.schema()` function will be called to load the schema, and
a `Reference` to the loaded schema will be used in the operation response.
See `OpenApiSpex.schema` macro for a convenient syntax for defining schema modules.
"""
- @spec resolve_schema_modules(OpenApi.t) :: OpenApi.t
+ @spec resolve_schema_modules(OpenApi.t()) :: OpenApi.t()
def resolve_schema_modules(spec = %OpenApi{}) do
components = spec.components || %Components{}
schemas = components.schemas || %{}
{paths, schemas} = resolve_schema_modules_from_paths(spec.paths, schemas)
schemas = resolve_schema_modules_from_schemas(schemas)
- %{spec | paths: paths, components: %{components| schemas: schemas}}
+ %{spec | paths: paths, components: %{components | schemas: schemas}}
end
defp resolve_schema_modules_from_paths(paths = %{}, schemas = %{}) do
Enum.reduce(paths, {paths, schemas}, fn {path, path_item}, {paths, schemas} ->
{new_path_item, schemas} = resolve_schema_modules_from_path_item(path_item, schemas)
{Map.put(paths, path, new_path_item), schemas}
end)
end
defp resolve_schema_modules_from_path_item(path = %PathItem{}, schemas) do
path
|> Map.from_struct()
|> Enum.filter(fn {_k, v} -> match?(%Operation{}, v) end)
|> Enum.reduce({path, schemas}, fn {k, operation}, {path, schemas} ->
{new_operation, schemas} = resolve_schema_modules_from_operation(operation, schemas)
{Map.put(path, k, new_operation), schemas}
end)
end
defp resolve_schema_modules_from_operation(operation = %Operation{}, schemas) do
{parameters, schemas} = resolve_schema_modules_from_parameters(operation.parameters, schemas)
- {request_body, schemas} = resolve_schema_modules_from_request_body(operation.requestBody, schemas)
+
+ {request_body, schemas} =
+ resolve_schema_modules_from_request_body(operation.requestBody, schemas)
+
{responses, schemas} = resolve_schema_modules_from_responses(operation.responses, schemas)
- new_operation = %{operation | parameters: parameters, requestBody: request_body, responses: responses}
+
+ new_operation = %{
+ operation
+ | parameters: parameters,
+ requestBody: request_body,
+ responses: responses
+ }
+
{new_operation, schemas}
end
defp resolve_schema_modules_from_parameters(nil, schemas), do: {nil, schemas}
+
defp resolve_schema_modules_from_parameters(parameters, schemas) do
{parameters, schemas} =
Enum.reduce(parameters, {[], schemas}, fn parameter, {parameters, schemas} ->
{new_parameter, schemas} = resolve_schema_modules_from_parameter(parameter, schemas)
{[new_parameter | parameters], schemas}
end)
+
{Enum.reverse(parameters), schemas}
end
- defp resolve_schema_modules_from_parameter(parameter = %Parameter{schema: schema, content: nil}, schemas) do
+ defp resolve_schema_modules_from_parameter(
+ parameter = %Parameter{schema: schema, content: nil},
+ schemas
+ ) do
{schema, schemas} = resolve_schema_modules_from_schema(schema, schemas)
new_parameter = %{parameter | schema: schema}
{new_parameter, schemas}
end
- defp resolve_schema_modules_from_parameter(parameter = %Parameter{schema: nil, content: content = %{}}, schemas) do
+
+ defp resolve_schema_modules_from_parameter(
+ parameter = %Parameter{schema: nil, content: content = %{}},
+ schemas
+ ) do
{new_content, schemas} = resolve_schema_modules_from_content(content, schemas)
{%{parameter | content: new_content}, schemas}
end
+
defp resolve_schema_modules_from_parameter(parameter = %Parameter{}, schemas) do
{parameter, schemas}
end
+
defp resolve_schema_modules_from_parameter(parameter = %Reference{}, schemas) do
{parameter, schemas}
end
defp resolve_schema_modules_from_content(nil, schemas), do: {nil, schemas}
+
defp resolve_schema_modules_from_content(content, schemas) do
Enum.reduce(content, {content, schemas}, fn {mime, media}, {content, schemas} ->
{new_media, schemas} = resolve_schema_modules_from_media_type(media, schemas)
{Map.put(content, mime, new_media), schemas}
end)
end
defp resolve_schema_modules_from_media_type(media = %MediaType{schema: schema}, schemas) do
{schema, schemas} = resolve_schema_modules_from_schema(schema, schemas)
new_media = %{media | schema: schema}
{new_media, schemas}
end
+
defp resolve_schema_modules_from_media_type(media = %MediaType{}, schemas) do
{media, schemas}
end
defp resolve_schema_modules_from_request_body(nil, schemas), do: {nil, schemas}
+
defp resolve_schema_modules_from_request_body(request_body = %RequestBody{}, schemas) do
{content, schemas} = resolve_schema_modules_from_content(request_body.content, schemas)
new_request_body = %{request_body | content: content}
{new_request_body, schemas}
end
+
defp resolve_schema_modules_from_request_body(request_body = %Reference{}, schemas) do
{request_body, schemas}
end
+ defp resolve_schema_modules_from_responses(responses, schemas = %{}) when is_list(responses) do
+ resolve_schema_modules_from_responses(Map.new(responses), schemas)
+ end
+
defp resolve_schema_modules_from_responses(responses = %{}, schemas = %{}) do
Enum.reduce(responses, {responses, schemas}, fn {status, response}, {responses, schemas} ->
{new_response, schemas} = resolve_schema_modules_from_response(response, schemas)
{Map.put(responses, status, new_response), schemas}
end)
end
defp resolve_schema_modules_from_response(response = %Response{}, schemas = %{}) do
{content, schemas} = resolve_schema_modules_from_content(response.content, schemas)
new_response = %{response | content: content}
{new_response, schemas}
end
defp resolve_schema_modules_from_schemas(schemas = %{}) do
Enum.reduce(schemas, schemas, fn {name, schema}, schemas ->
{schema, schemas} = resolve_schema_modules_from_schema(schema, schemas)
Map.put(schemas, name, schema)
end)
end
defp resolve_schema_modules_from_schema(false, schemas), do: {false, schemas}
defp resolve_schema_modules_from_schema(true, schemas), do: {true, schemas}
defp resolve_schema_modules_from_schema(nil, schemas), do: {nil, schemas}
+
defp resolve_schema_modules_from_schema(schema_list, schemas) when is_list(schema_list) do
Enum.map_reduce(schema_list, schemas, &resolve_schema_modules_from_schema/2)
end
+
defp resolve_schema_modules_from_schema(schema, schemas) when is_atom(schema) do
title = schema.schema().title
+
new_schemas =
if Map.has_key?(schemas, title) do
schemas
else
{new_schema, schemas} = resolve_schema_modules_from_schema(schema.schema(), schemas)
Map.put(schemas, title, new_schema)
end
+
{%Reference{"$ref": "#/components/schemas/#{title}"}, new_schemas}
end
+
defp resolve_schema_modules_from_schema(schema = %Schema{}, schemas) do
{all_of, schemas} = resolve_schema_modules_from_schema(schema.allOf, schemas)
{one_of, schemas} = resolve_schema_modules_from_schema(schema.oneOf, schemas)
{any_of, schemas} = resolve_schema_modules_from_schema(schema.anyOf, schemas)
{not_schema, schemas} = resolve_schema_modules_from_schema(schema.not, schemas)
{items, schemas} = resolve_schema_modules_from_schema(schema.items, schemas)
- {additional, schemas} = resolve_schema_modules_from_schema(schema.additionalProperties, schemas)
- {properties, schemas} = resolve_schema_modules_from_schema_properties(schema.properties, schemas)
- schema =
- %{schema |
- allOf: all_of,
+
+ {additional, schemas} =
+ resolve_schema_modules_from_schema(schema.additionalProperties, schemas)
+
+ {properties, schemas} =
+ resolve_schema_modules_from_schema_properties(schema.properties, schemas)
+
+ schema = %{
+ schema
+ | allOf: all_of,
oneOf: one_of,
anyOf: any_of,
not: not_schema,
items: items,
additionalProperties: additional,
properties: properties
- }
+ }
+
{schema, schemas}
end
+
defp resolve_schema_modules_from_schema(ref = %Reference{}, schemas), do: {ref, schemas}
defp resolve_schema_modules_from_schema_properties(nil, schemas), do: {nil, schemas}
+
defp resolve_schema_modules_from_schema_properties(properties, schemas) do
Enum.reduce(properties, {properties, schemas}, fn {name, property}, {properties, schemas} ->
{new_property, schemas} = resolve_schema_modules_from_schema(property, schemas)
{Map.put(properties, name, new_property), schemas}
end)
end
end
diff --git a/test/controller_test.exs b/test/controller_test.exs
new file mode 100644
index 0000000..be2f983
--- /dev/null
+++ b/test/controller_test.exs
@@ -0,0 +1,33 @@
+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(:show)
+ end
+
+ test "summary matches 'Endpoint summary'" do
+ assert %{summary: "Endpoint summary"} = @controller.open_api_operation(:show)
+ end
+
+ test "has response for HTTP 200" do
+ assert %{responses: %{200 => _}} = @controller.open_api_operation(:show)
+ end
+
+ test "has parameter `:id`" do
+ assert %{parameters: [param]} = @controller.open_api_operation(:show)
+ assert param.name == :id
+ assert param.required
+ end
+ end
+end
diff --git a/test/support/user_controller_annotated.ex b/test/support/user_controller_annotated.ex
new file mode 100644
index 0000000..06657a2
--- /dev/null
+++ b/test/support/user_controller_annotated.ex
@@ -0,0 +1,19 @@
+defmodule OpenApiSpexTest.UserControllerAnnotated do
+ use OpenApiSpex.Controller
+
+ @moduledoc tags: ["Foo"]
+
+ @doc """
+ Endpoint summary
+
+ More docs
+ """
+ @doc parameters: [
+ id: [in: :path, type: :string, required: true]
+ ]
+ @doc responses: [
+ ok: {"Foo document", "application/json", FooSchema}
+ ]
+ def show, do: :ok
+end
+

File Metadata

Mime Type
text/x-diff
Expires
Thu, Nov 28, 1:27 AM (1 d, 21 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40789
Default Alt Text
(27 KB)

Event Timeline