Page MenuHomePhorge

No OneTemporary

Size
23 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/open_api_spex/controller.ex b/lib/open_api_spex/controller.ex
index ceb2133..ac79793 100644
--- a/lib/open_api_spex/controller.ex
+++ b/lib/open_api_spex/controller.ex
@@ -1,184 +1,188 @@
defmodule OpenApiSpex.Controller do
@moduledoc ~S'''
Generation of OpenAPI documentation via ExDoc documentation and tags.
## Supported OpenAPI fields
### `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
+ 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: 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
+ 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 1e2f6ad..a1c51b2 100644
--- a/test/controller_test.exs
+++ b/test/controller_test.exs
@@ -1,49 +1,57 @@
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
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/schemas.ex b/test/support/schemas.ex
index b6eccef..ddec7fa 100644
--- a/test/support/schemas.ex
+++ b/test/support/schemas.ex
@@ -1,498 +1,571 @@
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
defmodule PetStatus do
OpenApiSpex.schema(%{
title: "PetStatus",
description: "The current status of a pet",
type: :string,
enum: ["adopted", "unadopted"]
})
end
defmodule AppointmentType do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "AppointmentType",
type: :object,
properties: %{
appointment_type: %Schema{
type: :string
}
},
required: [:appointment_type]
})
end
defmodule TrainingAppointment do
OpenApiSpex.schema(%{
title: "TrainingAppointment",
description: "Request for a training appointment",
type: :object,
allOf: [
AppointmentType,
%Schema{
type: :object,
properties: %{
level: %Schema{
description: "The level of training",
type: :integer
}
},
required: [:level]
}
]
})
end
defmodule GroomingAppointment do
OpenApiSpex.schema(%{
title: "GroomingAppointment",
description: "Request for a training appointment",
type: :object,
allOf: [
AppointmentType,
%Schema{
type: :object,
properties: %{
hair_trim: %Schema{
description: "Whether or not to include a hair trim",
type: :boolean
},
nail_clip: %Schema{
description: "Whether or not to include nail clip",
type: :boolean
}
},
required: [:hair_trim, :nail_clip]
}
]
})
end
defmodule PetAppointmentRequest do
OpenApiSpex.schema(%{
title: "PetAppointmentRequest",
description: "POST body for making a pet appointment",
type: :object,
oneOf: [
TrainingAppointment,
GroomingAppointment
],
discriminator: %OpenApiSpex.Discriminator{
propertyName: "appointment_type",
mapping: %{
"training" => "TrainingAppointment",
"grooming" => "GroomingAppointment"
}
}
})
end
end
diff --git a/test/support/user_controller_annotated.ex b/test/support/user_controller_annotated.ex
index 59f6e1c..d90aa9a 100644
--- a/test/support/user_controller_annotated.ex
+++ b/test/support/user_controller_annotated.ex
@@ -1,34 +1,36 @@
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
@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, 7:12 AM (1 d, 9 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40232
Default Alt Text
(23 KB)

Event Timeline