defmodule OpenApiSpex.Plug.PutApiSpec do
@moduledoc """
Module plug that calls a given module to obtain the Api Spec and store it as private in the Conn.
This allows downstream plugs to use the API spec for casting, validating and rendering.
## Options
- module: A module implementing the `OpenApiSpex.OpenApi` behaviour
## Example
plug OpenApiSpex.Plug.PutApiSpec, module: MyAppWeb.ApiSpec
"""
alias OpenApiSpex.{OpenApi, Operation, Plug.Cache}
alias Plug.Conn
@behaviour Plug
@missing_oas_key "Missing private.open_api_spex key in conn. Check that PutApiSpec is being called in the Conn pipeline"
@impl Plug
def init(opts) do
opts[:module] ||
raise "A :module option is required, but none was given to #{__MODULE__}.init/1"
end
@impl Plug
def call(conn, spec_module) do
private_data =
conn
|> Map.get(:private)
|> Map.get(:open_api_spex, %{})
|> Map.put(:spec_module, spec_module)
Plug.Conn.put_private(conn, :open_api_spex, private_data)
end
@spec get_spec_and_operation_lookup(Conn.t()) ::
{spec :: OpenApi.t(), operation_lookup :: %{any => Operation.t()}}
def get_spec_and_operation_lookup(conn) do
spec_module = spec_module(conn)
spec_and_lookup = cache().get(spec_module)
if spec_and_lookup do
spec_and_lookup
else
spec = spec_module.spec()
operation_lookup = build_operation_lookup(spec)
spec_and_lookup = {spec, operation_lookup}
cache().put(spec_module, spec_and_lookup)
spec_and_lookup
end
end
@spec get_and_cache_controller_action(Conn.t(), String.t(), {module, atom}) ::
Operation.t()
def get_and_cache_controller_action(conn, operation_id, {controller, action}) do
spec_module = spec_module(conn)
{spec, operation_lookup} = get_spec_and_operation_lookup(conn)
operation = operation_lookup[operation_id]
operation_lookup = Map.put(operation_lookup, {controller, action}, operation)
cache().put(spec_module, {spec, operation_lookup})
operation
end
@spec spec_module(Conn.t()) :: module
def spec_module(conn) do
private_data = Map.get(conn.private, :open_api_spex) || raise @missing_oas_key
private_data.spec_module
end
@spec build_operation_lookup(OpenApi.t()) :: %{
String.t() => OpenApiSpex.Operation.t()
}
defp build_operation_lookup(%OpenApi{} = spec) do
spec
|> Map.get(:paths)
|> Stream.flat_map(fn {_name, item} -> Map.values(item) end)
|> Stream.filter(fn x -> match?(%OpenApiSpex.Operation{}, x) end)
|> Stream.map(fn operation -> {operation.operationId, operation} end)
|> Enum.into(%{})
end
@spec cache() :: module
defp cache do
Cache.adapter()
end
end