Current section
Files
Jump to
Current section
Files
lib/rolodex/response.ex
defmodule Rolodex.Response do
@moduledoc """
Exposes functions and macros for defining reusable responses.
It exposes the following macros, which when used together will setup a response:
- `response/2` - for declaring a response
- `desc/1` - for setting an (optional) response description
- `content/2` - for defining a response shape for a specific content type
- `schema/1` and `schema/2` - for defining the shape for a content type
- `example/2` - for defining an (optional) response example for a content type
It also exposes the following functions:
- `is_response_module?/1` - determines if the provided item is a module that
has defined a reusable response
- `to_map/1` - serializes a response module into a map
- `get_refs/1` - traverses a response and searches for any nested `Rolodex.Schema`
refs within
"""
alias Rolodex.DSL
defmacro __using__(_opts) do
quote do
use Rolodex.DSL
import Rolodex.Response, only: :macros
end
end
@doc """
Opens up the response definition for the current module. Will name the response
and generate metadata for the response based on macro calls within the provided
block.
**Accept**
- `name` - the response name
- `block` - response shape definitions
## Example
defmodule MyResponse do
use Rolodex.Response
response "MyResponse" do
desc "A demo response with multiple content types"
content "application/json" do
schema MyResponseSchema
example :response, %{foo: "bar"}
example :other_response, %{bar: "baz"}
end
content "foo/bar" do
schema AnotherResponseSchema
example :response, %{foo: "bar"}
end
end
end
"""
defmacro response(name, opts) do
DSL.def_content_body(:__response__, name, opts)
end
@doc """
Sets a description for the response
"""
defmacro desc(str), do: DSL.set_desc(str)
@doc """
Sets headers to be included in the response. You can use a shared headers ref
defined via `Rolodex.Headers`, or just pass in a bare map or keyword list. If
the macro is called multiple times, all headers passed in will be merged together
in the docs result.
## Examples
# Shared headers module
defmodule MyResponse do
use Rolodex.Response
response "MyResponse" do
headers MyResponseHeaders
headers MyAdditionalResponseHeaders
end
end
# Headers defined in place
defmodule MyResponse do
use Rolodex.Response
response "MyResponse" do
headers %{
"X-Pagination" => %{
type: :integer,
description: "Pagination information"
}
}
end
end
"""
defmacro headers(metadata), do: DSL.set_headers(metadata)
@doc """
Defines a response shape for the given content type key
**Accepts**
- `key` - a valid content-type key
- `block` - metadata about the response shape for this content type
"""
defmacro content(key, opts) do
DSL.def_content_type_shape(:__response__, key, opts)
end
@doc """
Sets an example for the content type. This macro can be used multiple times
within a content type block to allow multiple examples.
**Accepts**
- `name` - a name for the example
- `body` - a map, which is the example data
"""
defmacro example(name, example_body) do
DSL.set_example(:__response__, name, example_body)
end
@doc """
Sets a schema for the current response content type. There are three ways
you can define a schema for a content-type chunk:
1. You can pass in an alias for a reusable schema defined via `Rolodex.Schema`
2. You can define a schema inline via the same macro syntax used in `Rolodex.Schema`
3. You can define a schema inline via a bare map, which will be parsed with `Rolodex.Field`
## Examples
# Via a reusable schema alias
content "application/json" do
schema MySchema
end
# Can define a schema inline via the schema + field + partial macros
content "application/json" do
schema do
field :id, :uuid
field :name, :string, desc: "The name"
partial PaginationParams
end
end
# Can provide a bare map, which will be parsed via `Rolodex.Field`
content "application/json" do
schema %{
type: :object,
properties: %{
id: :uuid,
name: :string
}
}
end
"""
defmacro schema(mod), do: DSL.set_schema(:__response__, mod)
@doc """
Sets a collection of schemas for the current response content type.
## Examples
# Response is a list
content "application/json" do
schema :list, of: [MySchema]
end
# Response is one of the provided types
content "application/json" do
schema :one_of, of: [MySchema, MyOtherSchema]
end
"""
defmacro schema(collection_type, opts) do
DSL.set_schema(:__response__, collection_type, opts)
end
@doc """
Adds a new field to the schema when defining a schema inline via macros. See
`Rolodex.Field` for more information about valid field metadata.
Accepts
- `identifier` - field name
- `type` - either an atom or another Rolodex.Schema module
- `opts` - a keyword list of options, looks for `desc` and `of` (for array types)
## Example
defmodule MyResponse do
use Rolodex.Response
response "MyResponse" do
content "application/json" do
schema do
# Atomic field with no description
field :id, :uuid
# Atomic field with a description
field :name, :string, desc: "The object's name"
# A field that refers to another, nested object
field :other, OtherSchema
# A field that is an array of items of one-or-more types
field :multi, :list, of: [:string, OtherSchema]
# You can use a shorthand to define a list field, the below is identical
# to the above
field :multi, [:string, OtherSchema]
# A field that is one of the possible provided types
field :any, :one_of, of: [:string, OtherSchema]
end
end
end
end
"""
defmacro field(identifier, type, opts \\ []) do
DSL.set_field(:fields, identifier, type, opts)
end
@doc """
Adds a new partial to the schema when defining a schema inline via macros. A
partial is another schema that will be serialized and merged into the top-level
properties map for the current schema. Partials are useful for shared parameters
used across multiple schemas. Bare keyword lists and maps that are parseable
by `Rolodex.Field` are also supported.
## Example
defmodule PaginationParams do
use Rolodex.Schema
schema "PaginationParams" do
field :page, :integer
field :page_size, :integer
field :total_pages, :integer
end
end
defmodule MyResponse do
use Rolodex.Response
response "MyResponse" do
content "application/json" do
schema do
field :id, :uuid
partial PaginationParams
end
end
end
end
"""
defmacro partial(mod), do: DSL.set_partial(mod)
@doc """
Determines if an arbitrary item is a module that has defined a reusable response
via `Rolodex.Response` macros
## Example
iex> defmodule SimpleResponse do
...> use Rolodex.Response
...> response "SimpleResponse" do
...> content "application/json" do
...> schema MySchema
...> end
...> end
...> end
iex>
iex> # Validating a response module
iex> Rolodex.Response.is_response_module?(SimpleResponse)
true
iex> # Validating some other module
iex> Rolodex.Response.is_response_module?(OtherModule)
false
"""
@spec is_response_module?(any()) :: boolean()
def is_response_module?(mod), do: DSL.is_module_of_type?(mod, :__response__)
@doc """
Serializes the `Rolodex.Response` metadata into a formatted map.
## Example
iex> defmodule MySimpleSchema do
...> use Rolodex.Schema
...>
...> schema "MySimpleSchema" do
...> field :id, :uuid
...> end
...> end
iex>
iex> defmodule MyResponse do
...> use Rolodex.Response
...>
...> response "MyResponse" do
...> desc "A demo response"
...>
...> headers %{"X-Rate-Limited" => :boolean}
...>
...> content "application/json" do
...> schema MySimpleSchema
...> example :response, %{id: "123"}
...> end
...>
...> content "application/json-list" do
...> schema [MySimpleSchema]
...> example :response, [%{id: "123"}]
...> example :another_response, [%{id: "234"}]
...> end
...> end
...> end
iex>
iex> Rolodex.Response.to_map(MyResponse)
%{
desc: "A demo response",
headers: [
%{"X-Rate-Limited" => %{type: :boolean}}
],
content: %{
"application/json" => %{
examples: %{
response: %{id: "123"}
},
schema: %{
type: :ref,
ref: Rolodex.ResponseTest.MySimpleSchema
}
},
"application/json-list" => %{
examples: %{
response: [%{id: "123"}],
another_response: [%{id: "234"}],
},
schema: %{
type: :list,
of: [
%{type: :ref, ref: Rolodex.ResponseTest.MySimpleSchema}
]
}
}
}
}
"""
@spec to_map(module()) :: map()
def to_map(mod), do: DSL.to_content_body_map(&mod.__response__/1)
@doc """
Traverses a serialized Response and collects any nested references to any
Schemas within. See `Rolodex.Field.get_refs/1` for more info.
"""
@spec get_refs(module()) :: [module()]
def get_refs(mod), do: DSL.get_refs_in_content_body(&mod.__response__/1)
end