defmodule Plug.CacheControl.Helpers do
@moduledoc """
Contains helper functions for working with cache-control header on Plug
connections.
"""
alias Plug.CacheControl.Header
alias Plug.Conn
@type unit ::
:second
| :seconds
| :minute
| :minutes
| :hour
| :hours
| :day
| :days
| :week
| :weeks
| :year
| :years
@type duration :: {integer(), unit()}
@doc """
Sets a period after which the response should be considered stale.
"""
@spec expires_in(Conn.t(), integer() | duration(), Enum.t()) :: Conn.t()
def expires_in(%Conn{} = conn, duration, opts \\ []) do
set_expires_in(conn, duration, opts)
end
defp set_expires_in(conn, seconds, opts) when is_integer(seconds) and seconds >= 0 do
set_expires_in(conn, {seconds, :seconds}, opts)
end
defp set_expires_in(conn, {_, _} = duration, opts) do
opts =
opts
|> normalize_opts()
|> Keyword.put(:max_age, duration)
patch_header_value(conn, opts)
end
@doc """
Marks the response as stale from the get-go.
"""
@spec expires_now(Conn.t(), Enum.t()) :: Conn.t()
def expires_now(%Conn{} = conn, opts \\ []) do
set_expires_now(conn, opts)
end
defp set_expires_now(conn, opts) do
opts =
opts
|> normalize_opts()
|> Keyword.put(:max_age, 0)
patch_header_value(conn, opts)
end
@doc """
Sets an expiration period of 100 years.
"""
@spec cached_forever(Conn.t(), Enum.t()) :: Conn.t()
def cached_forever(%Conn{} = conn, opts \\ []) do
opts =
opts
|> normalize_opts()
|> Keyword.put(:max_age, {100, :years})
patch_header_value(conn, opts)
end
@doc """
Marks the response as uncacheable.
"""
@spec cached_never(Conn.t()) :: Conn.t()
def cached_never(%Conn{} = conn) do
put_cache_control(conn, [:no_store])
end
@doc """
Serializes the cache control clauses and sets them on the connection.
"""
@spec put_cache_control(Conn.t(), [any()]) :: Conn.t()
def put_cache_control(conn, clauses) do
value =
clauses
|> normalize_opts()
|> Header.new()
|> Header.to_string()
Conn.put_resp_header(conn, "cache-control", value)
end
defp patch_header_value(conn, opts) do
value =
conn
|> Conn.get_resp_header("cache-control")
|> List.first("")
|> Header.from_string()
|> Header.put_many(opts)
|> Header.to_string()
Conn.put_resp_header(conn, "cache-control", value)
end
defp normalize_opts(opts) when is_list(opts) do
mapper = fn
{key, _} = tuple when is_atom(key) ->
tuple
key when is_atom(key) ->
{key, true}
other ->
raise ArgumentError, "Options' names must be atoms, got: #{inspect(other)}"
end
:lists.map(mapper, opts)
end
end