Current section
Files
Jump to
Current section
Files
lib/multiverse.ex
defmodule Multiverse do
@moduledoc """
This is a Plug that allows to manage multiple API versions via API gateways.
## Example Usage
pipeline :api do
...
plug Multiverse, gates: [
"2016-07-31": GateName
], version_header: "x-api-version", error_callback: &custom_error_callback/1
end
"""
@behaviour Plug
import Plug.Conn
defmodule Settings do
@moduledoc """
This is a struct that saves Multiverse options.
"""
@type gates :: [{String.t(), module()}]
@type error_callback :: Fun
@type version_header :: String.t()
@type t :: %__MODULE__{
gates: gates,
error_callback: error_callback,
version_header: version_header}
defstruct gates: [],
error_callback: &Multiverse.default_error_callback/2,
version_header: "x-api-version"
end
@latest_version_keyword "latest"
@type opts :: [
gates: Settings.gates,
error_callback: Settings.error_callback,
version_header: Settings.version_header]
@spec init(opts) :: Settings.t
def init(opts) do
%Settings{
gates: opts[:gates],
error_callback: opts[:error_callback],
version_header: opts[:version_header]
}
|> Map.merge(%Settings{}, fn (_k, curv, defv) -> curv || defv end)
end
@spec call(Plug.Conn.t, Settings.t) :: Plug.Conn.t
def call(conn, %Settings{gates: [], error_callback: error_callback, version_header: version_header}) do
conn
|> assign_client_version(version_header, error_callback)
end
def call(conn, %Settings{gates: gates, error_callback: error_callback, version_header: version_header}) do
conn
|> assign_client_version(version_header, error_callback)
|> assign_active_gates(gates)
|> apply_request_gates()
|> apply_response_gates()
end
defp assign_client_version(%Plug.Conn{} = conn, version_header, error_callback) do
conn
|> get_req_header(version_header)
|> List.first
|> normalize_api_version(error_callback, conn)
|> (&assign(conn, :client_api_version, &1)).()
end
defp normalize_api_version(nil, error_callback, %Plug.Conn{} = conn),
do: error_callback.(conn, "version header is empty")
defp normalize_api_version(version, _, _) when version == @latest_version_keyword,
do: get_latest_version()
defp normalize_api_version(version, error_callback, %Plug.Conn{} = conn) when is_function(error_callback) do
case Date.from_iso8601(version) do
{:error, reason} ->
error_callback.(conn, reason)
{:ok, date} ->
Date.to_string(date)
end
end
@doc """
Default error callback, that will be used if no error_callback was specified in options.
Returns string with a current date in format YYYY-MM-DD.
"""
def default_error_callback(_, _),
do: get_latest_version()
@doc """
Returns string with a current date in format YYYY-MM-DD.
"""
def get_latest_version,
do: Date.to_string(Date.utc_today())
defp assign_active_gates(%Plug.Conn{assigns: %{client_api_version: version}} = conn, gates) do
gates
|> Enum.filter(&version_comparator(&1, version))
|> (&assign(conn, :active_api_gates, &1)).()
end
# TODO: allow to pass any version comparator to a plug
defp version_comparator({gate_version, _}, api_version),
do: Atom.to_string(gate_version) > api_version
defp apply_request_gates(%Plug.Conn{assigns: %{active_api_gates: gates}} = conn) do
gates
|> List.keysort(0)
|> Enum.reduce(conn, &apply_request_gate(&1, &2))
end
defp apply_request_gate({_, gate_controller}, %Plug.Conn{} = conn),
do: apply(gate_controller, :mutate_request, [conn])
defp apply_response_gates(%Plug.Conn{assigns: %{active_api_gates: gates}} = conn) do
gates
|> List.keysort(1)
|> Enum.reduce(conn, &apply_response_gate(&1, &2))
end
defp apply_response_gate({_, gate_controller}, %Plug.Conn{} = conn),
do: register_before_send(conn, &response_callback(&1, gate_controller))
defp response_callback(%Plug.Conn{} = conn, gate_controller),
do: apply(gate_controller, :mutate_response, [conn])
end