defmodule Frugality.Generator do
@moduledoc """
A behaviour for producing HTTP document metadata.
Defines two callbacks - `c:Frugality.Generator.etag/1` and
`c:Frugality.Generator.last_modified/1`.
Request preconditions are evaluated agains at least one of two
pieces of metadata - an entity-tag and a last-modified timestamp.
"""
alias Frugality.Core.Metadata
alias Frugality.Encoder
@type t() :: module()
@type etag() ::
{:weak | :strong, binary()}
| {:source, iolist()}
| {:source, iolist(), Access.t()}
@type datetime() :: DateTime.t() | NaiveDateTime.t()
@callback etag(map()) :: etag() | nil
@callback last_modified(map()) :: datetime() | nil
@doc false
@spec generate(module(), map(), Access.container()) :: Metadata.t() | nil
def generate(impl, data, opts \\ []) when is_atom(impl) and is_map(data) do
encoder = Access.get(opts, :encoder)
etag =
data
|> impl.etag()
|> resolve_etag(encoder)
last_modified = impl.last_modified(data)
if etag || last_modified do
Metadata.new(etag, last_modified)
end
end
@spec resolve_etag(etag() | nil, module() | nil) :: EntityTag.t() | nil
defp resolve_etag(nil, _), do: nil
defp resolve_etag({:source, iodata}, encoder),
do: resolve_etag({:source, iodata, []}, encoder)
defp resolve_etag({_, _} = etag, _), do: etag
defp resolve_etag({:source, iodata, opts}, encoder) do
# The encoder specified in the `c:Frugality.Encoder.etag/1`
# callback has a higher precedence than the one stored in the
# connection.
encoder =
Access.get(opts, :encoder, encoder) ||
raise "no encoder provided"
binary = Encoder.encode(encoder, iodata)
{:weak, binary}
end
end