Current section
Files
Jump to
Current section
Files
lib/plug_cache_control/header.ex
defmodule PlugCacheControl.Header do
@moduledoc false
@typep maybe(t) :: t | nil
@type t :: %__MODULE__{
must_revalidate: maybe(boolean()),
no_cache: maybe(boolean()),
no_store: maybe(boolean()),
no_transform: maybe(boolean()),
proxy_revalidate: maybe(boolean()),
private: maybe(boolean()),
public: maybe(boolean()),
max_age: maybe(integer()),
s_maxage: maybe(integer()),
stale_while_revalidate: maybe(integer()),
stale_if_error: maybe(integer())
}
@directives [
:must_revalidate,
:no_cache,
:no_store,
:no_transform,
:proxy_revalidate,
:private,
:public,
:max_age,
:s_maxage,
:stale_while_revalidate,
:stale_if_error
]
@numeric_dir [:max_age, :s_maxage, :stale_while_revalidate, :stale_if_error]
defstruct @directives
defguardp is_directive(directive) when directive in @directives
defguardp is_numeric(directive) when directive in @numeric_dir
defguardp is_delta(time)
when is_integer(time) and time >= 0
@doc """
Creates a new header struct.
"""
@spec new() :: t()
def new, do: %__MODULE__{}
@doc """
Creates a new header struct from an enumerable.
"""
@spec new(Enum.t()) :: t()
def new(directives) do
put_many(%__MODULE__{}, directives)
end
@doc """
Puts a value of a directive in the header struct.
"""
@spec put(t(), Utils.directive(), term()) :: t()
def put(%__MODULE__{} = header, directive, value) do
do_put(header, directive, value)
end
@spec put_many(t(), Enum.t()) :: t()
def put_many(%__MODULE__{} = header, directives) do
Enum.reduce(directives, header, fn {directive, value}, header ->
put(header, directive, value)
end)
end
@spec merge(t(), t()) :: t()
def merge(%__MODULE__{} = ha, %__MODULE__{} = hb) do
ham = Map.from_struct(ha)
hbm = Map.from_struct(hb)
ham
|> Map.merge(hbm)
|> new()
end
@spec from_string(String.t()) :: t()
def from_string(str) when is_binary(str) do
str
|> String.split(",", trim: true)
|> Enum.map(&String.trim/1)
|> Enum.map(&String.split(&1, "=", trim: true))
|> Enum.map(fn
[key] -> {directive_to_atom(key), true}
[key, value] -> {directive_to_atom(key), value}
end)
|> new()
end
@spec to_string(t()) :: String.t()
def to_string(%__MODULE__{} = header) do
Kernel.to_string(header)
end
defp do_put(_, directive, _) when not is_directive(directive) do
raise ArgumentError, "Invalid directive #{inspect(directive)}"
end
defp do_put(header, :public, value) when is_boolean(value) do
%{header | public: value, private: !value}
end
defp do_put(header, :private, value) when is_boolean(value) do
%{header | public: !value, private: value}
end
defp do_put(header, :no_cache, fields) when is_list(fields) do
joined_fields = Enum.join(fields, ", ")
%{header | no_cache: "\"#{joined_fields}\""}
end
defp do_put(header, directive, {time, _unit} = dur)
when is_numeric(directive) and is_delta(time) do
do_put(header, directive, duration_to_seconds(dur))
end
defp do_put(header, directive, field) when is_numeric(directive) and is_delta(field) do
struct_put!(header, directive, field)
end
defp do_put(header, directive, value) when is_boolean(value) do
struct_put!(header, directive, value)
end
defp do_put(_, directive, value) do
raise ArgumentError, "Invalid value #{inspect(value)} for directive #{inspect(directive)}."
end
defimpl String.Chars do
defp atom_to_directive(atom) when is_atom(atom) do
atom
|> Atom.to_string()
|> String.replace("_", "-")
end
def to_string(header) do
header
|> Map.from_struct()
|> Enum.reduce([], fn
{_key, nil}, acc -> acc
{_key, false}, acc -> acc
{key, true}, acc -> [atom_to_directive(key) | acc]
{key, value}, acc -> ["#{atom_to_directive(key)}=#{value}" | acc]
end)
|> Enum.join(", ")
end
end
defp duration_to_seconds({period, _unit} = dur) when is_integer(period) and period >= 0,
do: do_duration_to_seconds(dur)
defp do_duration_to_seconds({period, unit}) when unit in [:second, :seconds], do: period
defp do_duration_to_seconds({period, unit}) when unit in [:minute, :minutes], do: period * 60
defp do_duration_to_seconds({period, unit}) when unit in [:hour, :hours], do: period * 60 * 60
defp do_duration_to_seconds({period, unit}) when unit in [:day, :days],
do: period * 60 * 60 * 24
defp do_duration_to_seconds({period, unit}) when unit in [:week, :weeks],
do: period * 60 * 60 * 24 * 7
defp do_duration_to_seconds({period, unit}) when unit in [:year, :years],
do: period * 60 * 60 * 24 * 365
defp do_duration_to_seconds({_, unit}),
do: raise(ArgumentError, "Invalid unit #{inspect(unit)}.")
defp directive_to_atom(directive) when is_binary(directive) do
directive
|> String.replace("-", "_")
|> String.to_existing_atom()
end
defp struct_put!(struct, field, value) when is_struct(struct) and is_atom(field) do
to_merge = Map.put(%{}, field, value)
struct!(struct, to_merge)
end
end