lib/jsonapi_plug/plug/content_type_negotiation.ex

defmodule JSONAPIPlug.Plug.ContentTypeNegotiation do
  @moduledoc """
  Provides content type negotiation by validating the `content-type` and `accept` headers.

  The proper jsonapi.org content type is `application/vnd.api+json` as per
  [the specification](http://jsonapi.org/format/#content-negotiation-servers).
  """

  alias JSONAPIPlug.Exceptions.InvalidHeader
  alias Plug.Conn

  @behaviour Plug

  @impl Plug
  def init(opts), do: opts

  @impl Plug
  def call(%Conn{method: method} = conn, _opts) when method in ["DELETE", "GET", "HEAD"],
    do: conn

  def call(conn, _opts) do
    case {validate_header(conn, "content-type"), validate_header(conn, "accept")} do
      {true, true} ->
        conn

      {false, _} ->
        raise InvalidHeader,
          status: :unsupported_content_type,
          message:
            "The 'content-type' request header must contain the JSON:API mime type (#{JSONAPIPlug.mime_type()})",
          reference: "https://jsonapi.org/format/#content-negotiation.",
          header: "content-type"

      {_, false} ->
        raise InvalidHeader,
          status: :not_acceptable,
          message:
            "The 'accept' request header must contain the JSON:API mime type (#{JSONAPIPlug.mime_type()})",
          reference: "https://jsonapi.org/format/#content-negotiation",
          header: "accept"
    end
  end

  defp validate_header(conn, header) do
    value =
      conn
      |> Conn.get_req_header(header)
      |> List.first()

    (value || JSONAPIPlug.mime_type())
    |> String.split(",", trim: true)
    |> Enum.member?(JSONAPIPlug.mime_type())
  end
end