lib/frugality/conditional_get.ex

defmodule Frugality.ConditionalGET do
  @moduledoc """
  Automatically computes and sets a shallow entity tag and evaluates
  the request preconditions against it.

  This plug generates shallow entity tags. Shallow means that the
  generated entity-tag is not context sensitive and the whole response
  body is used as a source for the generation.

  ## Usage

      plug Frugality.ConditionalGET,
        encoder: Frugality.Encoder.MD5

  """

  @behaviour Plug

  alias Frugality.Core.Utils
  alias Frugality.Generator
  alias Frugality.Generator.Conn
  alias Frugality.Generator.Response

  import Plug.Conn, only: [register_before_send: 2, get_resp_header: 2]

  @default_opts %{
    encoder: MD5
  }

  @impl true
  def init(opts) do
    opts
    |> Map.new()
    |> Map.merge(@default_opts)
  end

  @impl true
  def call(conn, opts) do
    register_before_send(conn, &do_call(&1, opts))
  end

  # Skip evaluation when the preconditions has already been manually
  # evaluated by `evaluate_preconditions[!]/2`.
  defp do_call(%Plug.Conn{private: %{frugality_evaluated: true}} = conn, _),
    do: conn

  defp do_call(conn, opts) do
    conn
    |> generate_metadata(opts)
    |> evaluate_preconditions(opts)
  end

  defp generate_metadata(%Plug.Conn{status: status} = conn, %{encoder: encoder})
       when status in [200, 201] do
    cond do
      has_metadata?(conn) ->
        conn

      metadata = response_metadata(conn, encoder) ->
        Utils.put_metadata(conn, metadata)

      true ->
        conn
    end
  end

  defp generate_metadata(conn, _), do: conn

  defp has_metadata?(conn) do
    ["etag", "last-modified"]
    |> Enum.map(&get_resp_header(conn, &1))
    |> Enum.all?(&Enum.empty?/1)
    |> Kernel.not()
  end

  # It's usually the static file server's job to generate metadata
  # when files are being sent.
  defp response_metadata(%Plug.Conn{state: :file}, _), do: nil
  defp response_metadata(%Plug.Conn{resp_body: ""}, _), do: nil

  defp response_metadata(%Plug.Conn{resp_body: response}, encoder) do
    Generator.generate(Response, %{response: response}, encoder)
  end

  defp evaluate_preconditions(%Plug.Conn{method: method} = conn, _)
       when method in ["GET", "HEAD"] do
    conn
    |> Frugality.put_generator(Conn)
    |> Frugality.evaluate_preconditions(%{})
  end

  defp evaluate_preconditions(conn, _), do: conn
end