Current section

Files

Jump to
blue_bird lib blue_bird controller.ex
Raw

lib/blue_bird/controller.ex

defmodule BlueBird.Controller do
@moduledoc """
Defines macros used to add documentation to api routes.
## Usage
Use `api/3` in your controllers. Optionally add the `apigroup/1` or
`apigroup/2` macro to your controllers.
defmodule MyApp.Web.UserController do
use BlueBird.Controller
alias MyApp.Accounts
apigroup "Customers", "These are the routes that we'll talk about."
api :GET, "users" do
title "List users"
description "Lists all active users"
end
def index(conn, _params) do
users = Accounts.list_users()
render(conn, "index.html", users: users)
end
end
Instead of adding `use BlueBird.Controller` to every controller module, you
can also add it to the `web.ex` controller function to make it available
in every controller.
def controller do
quote do
...
use BlueBird.Controller
...
end
end
"""
alias BlueBird.{Parameter, Route}
defmacro __using__(_) do
quote do
import BlueBird.Controller, only: [api: 3, apigroup: 1, apigroup: 2]
end
end
@doc """
Describes a route.
```
api <method> <url> do ... end
```
- `method`: HTTP method (GET, POST, PUT etc.)
- `url`: Route as defined in the Phoenix router
- `title` (optional): Title for the action
- `description` (optional): Description of the route
- `note` (optional): Note
- `warning` (optional): Warning
- `parameter` (optional): Used for path and query parameters. It takes the
name as defined in the route and the type. The third parameter is an
optional keyword list with additional options. See `BlueBird.Parameter`
for descriptions of the available options.
## Example
api :GET, "user/:id/posts/:slug" do
title "Show post"
description "Show post by user ID and post slug"
note "You should really know this."
warning "Please don't ever do this."
parameter :id, :integer
parameter :slug, :string, [
description: "This is the post slug.",
example: "whatever"
]
end
"""
defmacro api(method, path, do: block) do
method_str = method_to_string(method)
metadata = extract_metadata(block)
title = extract_option(metadata, :title)
description = extract_option(metadata, :description)
note = extract_option(metadata, :note)
warning = extract_option(metadata, :warning)
parameters = extract_parameters(metadata)
quote do
def api_doc(unquote(method_str), unquote(path)) do
%Route{
title: unquote(title),
description: unquote(description),
note: unquote(note),
method: unquote(method_str),
warning: unquote(warning),
path: unquote(path),
parameters: unquote(Macro.escape(parameters))
}
end
end
end
@doc """
Defines the name and an optional description for a resource group.
BlueBird defines groups by the controller. By default, the group name
is taken from the controller name. If you want to specify a different name,
you can use this macro. You can also add a group description as a second
parameter.
## Example
apigroup "resource group name"
or
apigroup "resource group name", "description"
"""
defmacro apigroup(name, description \\ "") do
name = to_string(name)
description = to_string(description)
quote do
def api_group do
%{
name: unquote(name),
description: unquote(description)
}
end
end
end
@spec method_to_string(String.t() | atom) :: String.t()
defp method_to_string(method) when is_binary(method) or is_atom(method) do
method
|> to_string
|> String.upcase()
end
@spec extract_metadata(
{:__block__, any, {String.t(), any, [any]}}
| {String.t(), any, [any]}
| nil
) :: [{atom, any}]
defp extract_metadata({:__block__, _, data}) do
Enum.map(data, fn {name, _line, params} ->
{name, params}
end)
end
defp extract_metadata({key, _, data}), do: [{key, data}]
defp extract_metadata(nil), do: []
@spec extract_option([{atom, any}], atom) :: nil | any
defp extract_option(metadata, key) do
values = metadata |> Keyword.get(key)
cond do
is_nil(values) ->
nil
length(values) == 1 ->
List.first(values)
true ->
raise ArgumentError,
"Expected single value for #{key}, got #{length(values)}"
end
end
@spec extract_parameters([{atom, any}]) :: [Parameter.t()]
defp extract_parameters(metadata) do
metadata
|> Keyword.get_values(:parameter)
|> Enum.reduce([], fn param, list -> [param_to_map(param) | list] end)
|> Enum.reverse()
end
@spec param_to_map([any]) :: Parameter.t()
defp param_to_map([name, type, options]) when is_list(options) do
Map.merge(
%Parameter{
name: to_string(name),
type: to_string(type)
},
Enum.into(options, %{})
)
end
defp param_to_map([name, type]) do
%Parameter{
name: to_string(name),
type: to_string(type)
}
end
defp param_to_map([_, _, _]) do
raise ArgumentError,
"The parameter macro expects a keyword list as " <> "third argument."
end
defp param_to_map(_) do
raise ArgumentError, """
Wrong number of arguments for parameter option.
Expected either two or three arguments: The name, the type
and an optional keyword list. Correct usage:
parameter :name, :type
or
parameter :name, :type, [description: "description",
optional: true]
"""
end
end