Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F114490
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
20 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..f619561 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.
### `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
+ alias OpenApiSpex.{Operation, Response}
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
+ Map.new(responses, fn
+ {status, {description, mime, schema}} ->
+ {Plug.Conn.Status.code(status), Operation.response(description, mime, schema)}
+
+ {status, %Response{} = response} ->
+ {Plug.Conn.Status.code(status), response}
+ 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..c387f61 100644
--- a/test/controller_test.exs
+++ b/test/controller_test.exs
@@ -1,44 +1,52 @@
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 response for HTTP 401" do
+ assert %{responses: %{401 => _}} = @controller.open_api_operation(:update)
+ end
+
+ test "has response for HTTP 404" do
+ assert %{responses: %{404 => _}} = @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
end
end
diff --git a/test/support/schemas.ex b/test/support/schemas.ex
index ed2003a..38c5e76 100644
--- a/test/support/schemas.ex
+++ b/test/support/schemas.ex
@@ -1,409 +1,482 @@
defmodule OpenApiSpexTest.Schemas do
require OpenApiSpex
alias OpenApiSpex.Reference
alias OpenApiSpex.Schema
defmodule Helper do
def prepare_struct([%Reference{"$ref": "#/components/schemas/" <> name} | tail]) do
schema =
apply(String.to_existing_atom("Elixir.OpenApiSpexTest.Schemas." <> name), :schema, [])
prepare_struct([schema | tail])
end
def prepare_struct([%Schema{properties: props} | tail]) when is_map(props) do
keys = Map.keys(props)
keys ++ prepare_struct(tail)
end
def prepare_struct([%Schema{allOf: allOf} | tail]) when is_list(allOf) do
prepare_struct(allOf ++ tail)
end
def prepare_struct([module_name | tail]) when is_atom(module_name) do
prepare_struct([module_name.schema() | tail])
end
def prepare_struct([]) do
[]
end
end
+ defmodule Unauthorized do
+ @moduledoc """
+ 401 - Unauthorized
+ """
+ require OpenApiSpex
+ alias OpenApiSpex.Operation
+
+ # OpenApiSpex.schema/1 macro can be optionally used to reduce boilerplate code
+ OpenApiSpex.schema(%{
+ title: "Unauthorized",
+ type: :object,
+ properties: %{
+ errors: %Schema{
+ type: :array,
+ items: %Schema{
+ type: :object,
+ properties: %{
+ detail: %Schema{
+ type: :string,
+ example: "Authentication credentials were not provided, or they were malformed."
+ },
+ title: %Schema{type: :string, example: "Authorization Required"}
+ }
+ }
+ }
+ }
+ })
+
+ @doc """
+ Unauthorized object, as a whole response.
+ """
+ def response do
+ Operation.response(
+ "Authorization Required",
+ "application/json",
+ __MODULE__
+ )
+ end
+ end
+
+ defmodule NotFound do
+ @moduledoc """
+ 404 - Not Found
+ """
+ require OpenApiSpex
+ alias OpenApiSpex.Operation
+
+ OpenApiSpex.schema(%{
+ title: "NotFound",
+ type: :object,
+ properties: %{
+ errors: %Schema{
+ type: :array,
+ items: %Schema{
+ type: :object,
+ properties: %{
+ detail: %Schema{type: :string, example: "The requested resource cannot be found."},
+ title: %Schema{type: :string, example: "Not Found"}
+ }
+ }
+ }
+ }
+ })
+
+ def response do
+ Operation.response(
+ "Not Found",
+ "application/json",
+ __MODULE__
+ )
+ end
+ end
+
defmodule Size do
OpenApiSpex.schema(%{
title: "Size",
description: "A size of a pet",
type: :object,
properties: %{
unit: %Schema{type: :string, description: "SI unit name", default: "cm"},
value: %Schema{type: :integer, description: "Size in given unit", default: 100}
},
required: [:unit, :value]
})
end
defmodule User do
OpenApiSpex.schema(%{
title: "User",
description: "A user of the app",
type: :object,
properties: %{
id: %Schema{type: :integer, description: "User ID"},
name: %Schema{type: :string, description: "User name", pattern: ~r/[a-zA-Z][a-zA-Z0-9_]+/},
email: %Schema{type: :string, description: "Email address", format: :email},
inserted_at: %Schema{
type: :string,
description: "Creation timestamp",
format: :"date-time"
},
updated_at: %Schema{type: :string, description: "Update timestamp", format: :"date-time"}
},
required: [:name, :email],
additionalProperties: false,
example: %{
"id" => 123,
"name" => "Joe User",
"email" => "joe@gmail.com",
"inserted_at" => "2017-09-12T12:34:55Z",
"updated_at" => "2017-09-13T10:11:12Z"
}
})
end
defmodule ContactInfo do
OpenApiSpex.schema(%{
title: "ContactInfo",
description: "A users contact information",
type: :object,
properties: %{
phone_number: %Schema{type: :string, description: "Phone number"},
postal_address: %Schema{type: :string, description: "Postal address"}
},
required: [:phone_number],
additionalProperties: false,
example: %{
"phone_number" => "555-123-456",
"postal_address" => "123 Evergreen Tce"
}
})
end
defmodule CreditCardPaymentDetails do
OpenApiSpex.schema(%{
title: "CreditCardPaymentDetails",
description: "Payment details when using credit-card method",
type: :object,
properties: %{
credit_card_number: %Schema{type: :string, description: "Credit card number"},
name_on_card: %Schema{type: :string, description: "Name as appears on card"},
expiry: %Schema{type: :string, description: "4 digit expiry MMYY"}
},
required: [:credit_card_number, :name_on_card, :expiry],
example: %{
"credit_card_number" => "1234-5678-1234-6789",
"name_on_card" => "Joe User",
"expiry" => "1234"
}
})
end
defmodule DirectDebitPaymentDetails do
OpenApiSpex.schema(%{
title: "DirectDebitPaymentDetails",
description: "Payment details when using direct-debit method",
type: :object,
properties: %{
account_number: %Schema{type: :string, description: "Bank account number"},
account_name: %Schema{type: :string, description: "Name of account"},
bsb: %Schema{type: :string, description: "Branch identifier"}
},
required: [:account_number, :account_name, :bsb],
example: %{
"account_number" => "12349876",
"account_name" => "Joes Savings Account",
"bsb" => "123-4567"
}
})
end
defmodule PaymentDetails do
OpenApiSpex.schema(%{
title: "PaymentDetails",
description: "Abstract Payment details type",
type: :object,
oneOf: [
CreditCardPaymentDetails,
DirectDebitPaymentDetails
]
})
end
defmodule UserRequest do
OpenApiSpex.schema(%{
title: "UserRequest",
description: "POST body for creating a user",
type: :object,
properties: %{
user: User
},
example: %{
"user" => %{
"name" => "Joe User",
"email" => "joe@gmail.com"
}
}
})
end
defmodule UserResponse do
OpenApiSpex.schema(%{
title: "UserResponse",
description: "Response schema for single user",
type: :object,
properties: %{
data: User
},
example: %{
"data" => %{
"id" => 123,
"name" => "Joe User",
"email" => "joe@gmail.com",
"inserted_at" => "2017-09-12T12:34:55Z",
"updated_at" => "2017-09-13T10:11:12Z"
}
}
})
end
defmodule UsersResponse do
OpenApiSpex.schema(%{
title: "UsersResponse",
description: "Response schema for multiple users",
type: :object,
properties: %{
data: %Schema{description: "The users details", type: :array, items: User}
},
example: %{
"data" => [
%{
"id" => 123,
"name" => "Joe User",
"email" => "joe@gmail.com"
},
%{
"id" => 456,
"name" => "Jay Consumer",
"email" => "jay@yahoo.com"
}
]
}
})
end
defmodule EntityWithDict do
OpenApiSpex.schema(%{
title: "EntityWithDict",
description: "Entity with a dictionary defined via additionalProperties",
type: :object,
properties: %{
id: %Schema{type: :integer, description: "Entity ID"},
stringDict: %Schema{
type: :object,
description: "String valued dict",
additionalProperties: %Schema{type: :string}
},
anyTypeDict: %Schema{
type: :object,
description: "Untyped valued dict",
additionalProperties: true
}
},
example: %{
"id" => 123,
"stringDict" => %{"key1" => "value1", "key2" => "value2"},
"anyTypeDict" => %{"key1" => 42, "key2" => %{"foo" => "bar"}}
}
})
end
defmodule PetType do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "PetType",
type: :object,
required: [:pet_type],
properties: %{
pet_type: %Schema{
type: :string
}
}
})
end
defmodule Cat do
alias OpenApiSpex.Schema
@behaviour OpenApiSpex.Schema
@derive [Jason.Encoder]
@schema %Schema{
title: "Cat",
type: :object,
allOf: [
PetType,
%Schema{
type: :object,
properties: %{
meow: %Schema{type: :string}
},
required: [:meow]
}
],
"x-struct": __MODULE__
}
def schema, do: @schema
defstruct OpenApiSpexTest.Schemas.Helper.prepare_struct(@schema.allOf)
end
defmodule Dog do
alias OpenApiSpex.Schema
@behaviour OpenApiSpex.Schema
@derive [Jason.Encoder]
@schema %Schema{
title: "Dog",
type: :object,
allOf: [
PetType,
%Schema{
type: :object,
properties: %{
bark: %Schema{type: :string}
},
required: [:bark]
}
],
"x-struct": __MODULE__
}
def schema, do: @schema
defstruct OpenApiSpexTest.Schemas.Helper.prepare_struct(@schema.allOf)
end
defmodule CatOrDog do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "CatOrDog",
oneOf: [Cat, Dog]
})
end
defmodule Array do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "Array",
type: :array,
items: Dog,
example: [%{pet_type: "Dog", bark: "A lot"}]
})
end
defmodule Primitive do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "Primitive",
type: :integer,
example: 1
})
end
defmodule PrimitiveArray do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "PrimitiveArray",
type: :array,
items: %Schema{type: "string"},
example: ["Foo"]
})
end
defmodule PetResponse do
OpenApiSpex.schema(%{
title: "PetResponse",
description: "Response schema for single pet",
type: :object,
properties: %{
data: OpenApiSpexTest.Schemas.CatOrDog
},
example: %{
"data" => %{
"pet_type" => "Dog",
"bark" => "woof"
}
}
})
end
defmodule Pet do
require OpenApiSpex
alias OpenApiSpex.{Schema, Discriminator}
OpenApiSpex.schema(%{
title: "Pet",
type: :object,
oneOf: [Cat, Dog],
discriminator: %Discriminator{
propertyName: "pet_type"
}
})
end
defmodule PetsResponse do
OpenApiSpex.schema(%{
title: "PetsResponse",
description: "Response schema for multiple pets",
type: :object,
properties: %{
data: %Schema{
description: "The pets details",
type: :array,
items: OpenApiSpexTest.Schemas.CatOrDog
}
},
example: %{
"data" => [
%{
"pet_type" => "Dog",
"bark" => "woof"
},
%{
"pet_type" => "Cat",
"meow" => "meow"
}
]
}
})
end
defmodule PetRequest do
OpenApiSpex.schema(%{
title: "PetRequest",
description: "POST body for creating a pet",
type: :object,
properties: %{
pet: CatOrDog
},
example: %{
"pet" => %{
"pet_type" => "Dog",
"bark" => "woof"
}
}
})
end
end
diff --git a/test/support/user_controller_annotated.ex b/test/support/user_controller_annotated.ex
index 85b5ca8..92789d7 100644
--- a/test/support/user_controller_annotated.ex
+++ b/test/support/user_controller_annotated.ex
@@ -1,20 +1,22 @@
defmodule OpenApiSpexTest.UserControllerAnnotated do
use OpenApiSpex.Controller
- alias OpenApiSpexTest.Schemas.User
+ alias OpenApiSpexTest.Schemas.{NotFound, Unauthorized, 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}
+ ok: {"User response", "application/json", User},
+ unauthorized: Unauthorized.response(),
+ not_found: NotFound.response()
]
def update(_conn, _params), do: :ok
end
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Tue, Nov 26, 9:06 AM (1 d, 12 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40256
Default Alt Text
(20 KB)
Attached To
Mode
R22 open_api_spex
Attached
Detach File
Event Timeline
Log In to Comment