lib/versionary/plug/verify_header.ex

defmodule Versionary.Plug.VerifyHeader do
  @moduledoc """
  Use this plug to verify a version string in the header.

  This plug will add a `:version_verified` private key to the conn. This value
  will be `true` if the version has been verified. Otherwise, it will be
  `false`.

  Note that this plug will only flag the conn as having a valid or invalid
  version. If you would like to halt the request and handle an invalid version
  please see `Versionary.Plug.EnsureVersion`.

  ## Options

  * `:versions` - a list of strings representing valid versions. If at least
    one of the provided versions is valid then the request is considered valid.

  * `:accepts` - a list of strings or atoms representing versions registered as
    MIME types. If at least one of the registered versions is valid then the
    request is considered valid.

  * `:header` - the header used to provide the requested version (Default:
    `Accept`)

  ## Example

      plug Versionary.Plug.VerifyHeader, versions: ["application/vnd.app.v1+json"]

  ## Multiple Versions

  You may pass multiple version strings to the `:versions` option. If at least
  one version matches the request will be considered valid.

  ```elixir
  plug Versionary.Plug.VerifyHeader, versions: ["application/vnd.app.v1+json",
                                                "application/vnd.app.v2+json"]
  ```

  ## MIME Support

  It's also possible to verify versions against configured MIME types. If
  multiple MIME types are passed and at least one matches the version will be
  considered valid.

  ```elixir
  config :mime, :types, %{
    "application/vnd.app.v1+json" => [:v1]
  }
  ```

  ```elixir
  plug Versionary.Plug.VerifyHeader, accepts: [:v1]
  ```

  Please note that whenever you change media type configurations you must
  recompile the `mime` library.

  To force `mime` to recompile run `mix deps.clean --build mime`.

  ## Identifying Versions

  When a version has been verified this plug will add `:version` and
  `:raw_version` private keys to the conn. These keys will contain version that
  has been verified.

  The `:version` key may contain either the string version provided by the
  request or, if configured, the MIME extension. The `:raw_version` key will
  always contain the string version provided by the request.
  """

  import Plug.Conn

  @default_header_opt "accept"

  @doc false
  def init(opts) do
    %{
      accepts: Keyword.get(opts, :accepts, []),
      header: Keyword.get(opts, :header, @default_header_opt),
      versions: Keyword.get(opts, :versions, [])
    }
  end

  @doc false
  def call(conn, opts) do
    conn
    |> verify_version(opts)
    |> put_version(opts)
  end

  #
  # private
  #

  defp verify_version(conn, opts) do
    verified = Enum.member?(get_valid_versions(opts), get_req_version(conn, opts))
    put_private(conn, :version_verified, verified)
  end

  defp put_version(%{private: %{version_verified: true}} = conn, opts) do
    raw_version = get_req_version(conn, opts)
    version = Map.get(MIME.compiled_custom_types(), raw_version, raw_version)

    conn
    |> put_private(:version, version)
    |> put_private(:raw_version, raw_version)
  end

  defp put_version(conn, _opts) do
    conn
  end

  #
  # helpers
  #

  defp get_valid_versions(opts) do
    opts[:versions] ++ get_mime_versions(opts)
  end

  defp get_mime_versions(%{accepts: accepts}), do: do_get_mime_versions(accepts)
  defp get_mime_versions(_opts), do: []

  defp do_get_mime_versions([h | t]), do: [MIME.type(h)] ++ do_get_mime_versions(t)
  defp do_get_mime_versions([]), do: []
  defp do_get_mime_versions(nil), do: []

  defp get_req_version(%Plug.Conn{} = conn, %{header: header} = opts) do
    get_req_header(conn, header)
    |> List.first()
    |> get_req_version(opts)
  end
  defp get_req_version([], _opts), do: false
  defp get_req_version(nil, _opts), do: nil
  defp get_req_version(headers, opts) when is_binary(headers) do
    String.split(headers, ",")
    |> Enum.map(&String.split(&1, ";") |> hd)
    |> Enum.map(&String.trim/1)
    |> get_req_version(opts)
  end
  defp get_req_version([head | tail], opts) do
    case Enum.member?(get_valid_versions(opts), head) do
      true  -> head
      false -> get_req_version(tail, opts)
    end
  end
end