Current section
Files
Jump to
Current section
Files
lib/spectral/openapi.ex
defmodule Spectral.OpenAPI do
@moduledoc """
Elixir wrapper for spectra OpenAPI specification generation.
This module provides idiomatic Elixir functions for generating OpenAPI 3.1
specifications from Elixir type definitions using spectra.
## Response Builder Pattern
Responses are built using a fluent API:
response = Spectral.OpenAPI.response(200, "Success")
|> Spectral.OpenAPI.response_with_body(Person, :t)
|> Spectral.OpenAPI.response_with_header("X-Rate-Limit", RateLimit, %{
schema: :t,
description: "Remaining requests",
required: false
})
endpoint = Spectral.OpenAPI.endpoint(:get, "/users/{id}")
|> Spectral.OpenAPI.add_response(response)
"""
@doc """
Creates a new OpenAPI endpoint definition.
## Parameters
- `method` - HTTP method as an atom (`:get`, `:post`, `:put`, `:delete`, `:patch`, etc.)
- `path` - URL path as a binary (e.g., `"/users/{id}"`)
- `doc` - Optional documentation map with optional keys:
- `:summary` - Short summary of the endpoint
- `:description` - Longer description of the endpoint
- `:operationId` - Unique string to identify the operation
- `:tags` - List of tags for grouping endpoints
- `:deprecated` - Whether the endpoint is deprecated (boolean)
- `:externalDocs` - Map with `:url` (required) and `:description` (optional)
## Returns
- `endpoint` - OpenAPI endpoint structure
## Example
iex> endpoint = Spectral.OpenAPI.endpoint(:get, "/users/{id}", %{summary: "Get user by ID"})
iex> endpoint.doc
%{summary: "Get user by ID"}
"""
@spec endpoint(:spectra_openapi.http_method(), binary(), :spectra_openapi.endpoint_doc()) ::
:spectra_openapi.endpoint_spec()
def endpoint(method, path, doc \\ %{}) do
:spectra_openapi.endpoint(method, path, doc)
end
@doc """
Creates a new OpenAPI endpoint definition using metadata from a `spectral/1` macro
placed before a function definition.
The function's metadata (set via `spectral summary: "...", description: "..."` before
the corresponding `@spec`) is retrieved from the module's type info and used as the
endpoint documentation.
## Parameters
- `method` - HTTP method as an atom (`:get`, `:post`, `:put`, `:delete`, `:patch`, etc.)
- `path` - URL path as a binary (e.g., `"/users/{id}"`)
- `module` - The module containing the function with `spectral` metadata
- `function_name` - The function name as an atom
- `arity` - The function arity
## Returns
- `endpoint` - OpenAPI endpoint structure with documentation from the function's metadata
## Example
defmodule MyController do
use Spectral
spectral summary: "Get user", description: "Returns a user by ID"
@spec get_user(map(), map()) :: map()
def get_user(_conn, _params), do: %{}
end
endpoint = Spectral.OpenAPI.endpoint(:get, "/users/{id}", MyController, :get_user, 2)
"""
@spec endpoint(:spectra_openapi.http_method(), binary(), module(), atom(), non_neg_integer()) ::
:spectra_openapi.endpoint_spec()
def endpoint(method, path, module, function_name, arity)
when is_atom(method) and is_binary(path) and is_atom(module) and is_atom(function_name) and
is_integer(arity) and arity >= 0 do
doc = function_doc(module, function_name, arity)
endpoint(method, path, doc)
end
@doc """
Creates a response builder.
This creates a response specification that can be further configured with
`response_with_body/3-4` and `response_with_header/4` before being added
to an endpoint with `add_response/2`.
## Parameters
- `status_code` - HTTP status code (e.g., `200`, `404`, `500`)
- `description` - Human-readable description of the response
## Returns
- `response` - Response builder structure
## Example
response = Spectral.OpenAPI.response(200, "User found successfully")
"""
@spec response(:spectra_openapi.http_status_code(), binary()) ::
:spectra_openapi.response_spec()
def response(status_code, description) do
:spectra_openapi.response(status_code, description)
end
@doc """
Adds a response body to a response builder.
You can optionally provide a `content_type` as a 4th argument.
## Parameters
- `response` - Response builder from `response/2`
- `module` - Module containing the type definition
- `schema` - Schema reference (typically an atom like `:t`)
- `content_type` - Optional content type (e.g., `"application/json"`, `"application/xml"`)
## Returns
- `response` - Updated response builder with body schema
## Example
response = Spectral.OpenAPI.response(200, "Success")
|> Spectral.OpenAPI.response_with_body(Person, :t)
response_xml = Spectral.OpenAPI.response(200, "Success")
|> Spectral.OpenAPI.response_with_body(Person, :t, "application/xml")
"""
@spec response_with_body(
:spectra_openapi.response_spec(),
module(),
atom() | :spectra.sp_type_or_ref()
) :: :spectra_openapi.response_spec()
def response_with_body(response, module, schema) do
:spectra_openapi.response_with_body(response, module, schema)
end
@doc false
@spec response_with_body(
:spectra_openapi.response_spec(),
module(),
atom() | :spectra.sp_type_or_ref(),
binary()
) :: :spectra_openapi.response_spec()
def response_with_body(response, module, schema, content_type) do
:spectra_openapi.response_with_body(response, module, schema, content_type)
end
@doc """
Adds a header to a response builder.
## Parameters
- `response` - Response builder from `response/2`
- `header_name` - Name of the response header (e.g., `"X-Rate-Limit"`)
- `module` - Module containing the type definition for the header value
- `header_spec` - Header specification map with keys:
- `:schema` - Schema reference for the header value (atom like `:t`, or a type ref tuple)
- `:description` (optional) - Description of the header
- `:required` (optional) - Whether the header is required (default: false)
- `:deprecated` (optional) - Whether the header is deprecated (boolean)
## Returns
- `response` - Updated response builder with header added
## Example
response = Spectral.OpenAPI.response(200, "Success")
|> Spectral.OpenAPI.response_with_header("X-Rate-Limit", RateLimit, %{
schema: :t,
description: "Requests remaining",
required: false
})
"""
@spec response_with_header(:spectra_openapi.response_spec(), binary(), module(), map()) ::
:spectra_openapi.response_spec()
def response_with_header(response, header_name, module, header_spec) do
:spectra_openapi.response_with_header(response, header_name, module, header_spec)
end
@doc """
Adds a complete response specification to an endpoint.
This function adds a response that was built using the response builder pattern.
## Parameters
- `endpoint` - The endpoint to add the response to
- `response` - Response specification built with `response/2` and related functions
## Returns
- `endpoint` - Updated endpoint with the response added
## Example
response = Spectral.OpenAPI.response(200, "User found")
|> Spectral.OpenAPI.response_with_body(Person, :t)
endpoint = Spectral.OpenAPI.endpoint(:get, "/users/{id}")
|> Spectral.OpenAPI.add_response(response)
"""
@spec add_response(:spectra_openapi.endpoint_spec(), :spectra_openapi.response_spec()) ::
:spectra_openapi.endpoint_spec()
def add_response(endpoint, response) do
:spectra_openapi.add_response(endpoint, response)
end
@doc """
Adds a request body specification to an endpoint.
You can optionally provide a `content_type` as a 4th argument.
## Parameters
- `endpoint` - The endpoint to modify
- `module` - Module containing type definitions
- `schema` - Schema reference (typically an atom like `:t`)
- `content_type` - Optional content type binary (e.g., `"application/xml"`; defaults to `"application/json"`)
## Returns
- `endpoint` - Modified endpoint with request body
## Example
endpoint = Spectral.OpenAPI.endpoint(:post, "/users")
|> Spectral.OpenAPI.with_request_body(Person, :t)
endpoint_xml = Spectral.OpenAPI.endpoint(:post, "/users")
|> Spectral.OpenAPI.with_request_body(Person, :t, "application/xml")
"""
@spec with_request_body(
:spectra_openapi.endpoint_spec(),
module(),
atom() | :spectra.sp_type_or_ref()
) :: :spectra_openapi.endpoint_spec()
def with_request_body(endpoint, module, schema) do
:spectra_openapi.with_request_body(endpoint, module, schema)
end
@doc false
@spec with_request_body(
:spectra_openapi.endpoint_spec(),
module(),
atom() | :spectra.sp_type_or_ref(),
binary()
) :: :spectra_openapi.endpoint_spec()
def with_request_body(endpoint, module, schema, content_type) when is_binary(content_type) do
:spectra_openapi.with_request_body(endpoint, module, schema, content_type)
end
@doc """
Adds a parameter to an endpoint.
Parameters can be in the path, query string, headers, or cookies.
## Parameters
- `endpoint` - The endpoint to modify
- `module` - Module containing the type definition for the parameter
- `parameter_spec` - Parameter specification map with keys:
- `:name` - Parameter name
- `:in` - Location (`:path`, `:query`, `:header`, `:cookie`)
- `:required` - Whether the parameter is required
- `:schema` - Schema for the parameter value (atom or `{:type, name, arity}` tuple)
The `description` and `deprecated` fields in the rendered OpenAPI output are sourced from
the type's `spectral` annotation, not from the parameter spec. Type aliases and remote type
references are followed automatically to inherit their documentation.
## Returns
- `endpoint` - Modified endpoint with parameter added
## Example
endpoint = Spectral.OpenAPI.endpoint(:get, "/users/{id}")
|> Spectral.OpenAPI.with_parameter(User, %{
name: "id",
in: :path,
required: true,
schema: :string
})
"""
@spec with_parameter(
:spectra_openapi.endpoint_spec(),
module(),
:spectra_openapi.parameter_input_spec()
) :: :spectra_openapi.endpoint_spec()
def with_parameter(endpoint, module, parameter_spec) do
:spectra_openapi.with_parameter(endpoint, module, parameter_spec)
end
@doc """
Converts a list of endpoints to a complete OpenAPI specification.
## Parameters
- `metadata` - OpenAPI metadata map with keys:
- `:title` - API title (required)
- `:version` - API version (required)
- `:summary` (optional) - Short summary of the API
- `:description` (optional) - Longer description of the API
- `:terms_of_service` (optional) - URL to the terms of service
- `:contact` (optional) - Contact map with optional `:name`, `:url`, `:email`
- `:license` (optional) - License map with required `:name` and optional `:url`, `:identifier`
- `:servers` (optional) - List of server objects, each with required `:url` and optional `:description`
- `endpoints` - List of endpoint definitions
## Returns
- `{:ok, iodata}` - Complete OpenAPI 3.1 specification serialised as JSON iodata
- `{:error, [%Spectral.Error{}]}` - List of errors if generation fails
## Example
metadata = %{title: "My API", version: "1.0.0"}
endpoints = [
Spectral.OpenAPI.endpoint(:get, "/users/{id}")
|> Spectral.OpenAPI.add_response(
Spectral.OpenAPI.response(200, "User found")
|> Spectral.OpenAPI.response_with_body(Person, :t)
)
]
{:ok, json} = Spectral.OpenAPI.endpoints_to_openapi(metadata, endpoints)
"""
@spec endpoints_to_openapi(
:spectra_openapi.openapi_metadata(),
[:spectra_openapi.endpoint_spec()]
) :: {:ok, iodata()} | {:error, [Spectral.Error.t()]}
def endpoints_to_openapi(metadata, endpoints) do
metadata
|> :spectra_openapi.endpoints_to_openapi(endpoints)
|> convert_result()
end
@doc """
Converts a list of endpoints to a complete OpenAPI specification, with options.
Like `endpoints_to_openapi/2` but accepts an options list.
## Parameters
- `metadata` - OpenAPI metadata map (same as `endpoints_to_openapi/2`)
- `endpoints` - List of endpoint definitions
- `opts` - Options list. Supported options:
- `:pre_encoded` - Return a map instead of iodata, skipping JSON encoding.
## Returns
- `{:ok, iodata()}` - Complete OpenAPI 3.1 spec as JSON iodata (default)
- `{:ok, map()}` - Spec as a decoded map when `:pre_encoded` option is set
- `{:error, [%Spectral.Error{}]}` - List of errors if generation fails
"""
@spec endpoints_to_openapi(
:spectra_openapi.openapi_metadata(),
[:spectra_openapi.endpoint_spec()],
[Spectral.schema_option()]
) :: {:ok, iodata() | dynamic()} | {:error, [Spectral.Error.t()]}
def endpoints_to_openapi(metadata, endpoints, opts) when is_list(opts) do
:spectra_openapi.endpoints_to_openapi(metadata, endpoints, opts)
|> convert_result()
end
# Private helper to convert Erlang results to Elixir
defp convert_result({:ok, result}), do: {:ok, result}
defp convert_result({:error, erlang_errors}) when is_list(erlang_errors) do
{:error, Spectral.Error.from_erlang_list(erlang_errors)}
end
defp function_doc(module, function_name, arity) do
Code.ensure_loaded!(module)
unless function_exported?(module, :__spectra_type_info__, 0) do
raise ArgumentError,
"#{inspect(module)} does not use Spectral — add `use Spectral` to the module"
end
# credo:disable-for-next-line Credo.Check.Readability.WithSingleClause
with {:ok, doc} <-
Spectral.TypeInfo.get_function_doc(
module.__spectra_type_info__(),
function_name,
arity
) do
doc
else
{:error, :function_not_found} ->
raise ArgumentError,
"#{inspect(module)}.#{function_name}/#{arity} has no @spec — add a @spec before using spectral/1 to annotate it"
{:error, :no_doc_found} ->
%{}
end
end
end