defmodule Mojito.Headers do @moduledoc ~S""" Functions for working with HTTP request and response headers, as described in the [HTTP 1.1 specification](https://www.w3.org/Protocols/rfc2616/rfc2616.html). Headers are represented in Elixir as a list of `{"header_name", "value"}` tuples. Multiple entries for the same header name are allowed. Capitalization of header names is preserved during insertion, however header names are handled case-insensitively during lookup and deletion. """ @type headers :: Mojito.headers() @doc ~S""" Returns the value for the given HTTP request or response header, or `nil` if not found. Header names are matched case-insensitively. If more than one matching header is found, the values are joined with `","` as specified in [RFC 2616](https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2). Example: iex> headers = [ ...> {"header1", "foo"}, ...> {"header2", "bar"}, ...> {"Header1", "baz"} ...> ] iex> Mojito.Headers.get(headers, "header2") "bar" iex> Mojito.Headers.get(headers, "HEADER1") "foo,baz" iex> Mojito.Headers.get(headers, "header3") nil """ @spec get(headers, String.t()) :: String.t() | nil def get(headers, name) do case get_values(headers, name) do [] -> nil values -> values |> Enum.join(",") end end @doc ~S""" Returns all values for the given HTTP request or response header. Returns an empty list if none found. Header names are matched case-insensitively. Example: iex> headers = [ ...> {"header1", "foo"}, ...> {"header2", "bar"}, ...> {"Header1", "baz"} ...> ] iex> Mojito.Headers.get_values(headers, "header2") ["bar"] iex> Mojito.Headers.get_values(headers, "HEADER1") ["foo", "baz"] iex> Mojito.Headers.get_values(headers, "header3") [] """ @spec get_values(headers, String.t()) :: [String.t()] def get_values(headers, name) do get_values(headers, String.downcase(name), []) end defp get_values([], _name, values), do: values defp get_values([{key, value} | rest], name, values) do new_values = if String.downcase(key) == name do values ++ [value] else values end get_values(rest, name, new_values) end @doc ~S""" Puts the given header `value` under `name`, removing any values previously stored under `name`. The new header is placed at the end of the list. Header names are matched case-insensitively, but case of `name` is preserved when adding the header. Example: iex> headers = [ ...> {"header1", "foo"}, ...> {"header2", "bar"}, ...> {"Header1", "baz"} ...> ] iex> Mojito.Headers.put(headers, "HEADER1", "quux") [{"header2", "bar"}, {"HEADER1", "quux"}] """ @spec put(headers, String.t(), String.t()) :: headers def put(headers, name, value) do delete(headers, name) ++ [{name, value}] end @doc ~S""" Removes all instances of the given header. Header names are matched case-insensitively. Example: iex> headers = [ ...> {"header1", "foo"}, ...> {"header2", "bar"}, ...> {"Header1", "baz"} ...> ] iex> Mojito.Headers.delete(headers, "HEADER1") [{"header2", "bar"}] """ @spec delete(headers, String.t()) :: headers def delete(headers, name) do name = String.downcase(name) Enum.filter(headers, fn {key, _value} -> String.downcase(key) != name end) end @doc ~S""" Returns an ordered list of the header names from the given headers. Header names are returned in lowercase. Example: iex> headers = [ ...> {"header1", "foo"}, ...> {"header2", "bar"}, ...> {"Header1", "baz"} ...> ] iex> Mojito.Headers.keys(headers) ["header1", "header2"] """ @spec keys(headers) :: [String.t()] def keys(headers) do keys(headers, []) end defp keys([], names), do: Enum.reverse(names) defp keys([{name, _value} | rest], names) do name = String.downcase(name) if name in names do keys(rest, names) else keys(rest, [name | names]) end end @doc ~S""" Returns a copy of the given headers where all header names are lowercased and multiple values for the same header have been joined with `","`. Example: iex> headers = [ ...> {"header1", "foo"}, ...> {"header2", "bar"}, ...> {"Header1", "baz"} ...> ] iex> Mojito.Headers.normalize(headers) [{"header1", "foo,baz"}, {"header2", "bar"}] """ @spec normalize(headers) :: headers def normalize(headers) do headers_map = Enum.reduce(headers, %{}, fn {name, value}, acc -> name = String.downcase(name) values = Map.get(acc, name, []) Map.put(acc, name, values ++ [value]) end) headers |> keys |> Enum.map(fn name -> {name, Map.get(headers_map, name) |> Enum.join(",")} end) end @doc ~S""" Returns an HTTP Basic Auth header from the given username and password. Example: iex> Mojito.Headers.auth_header("hello", "world") {"authorization", "Basic aGVsbG86d29ybGQ="} """ @spec auth_header(String.t(), String.t()) :: Mojito.header() def auth_header(username, password) do auth64 = "#{username}:#{password}" |> Base.encode64() {"authorization", "Basic #{auth64}"} end @doc ~S""" Convert non string values to string where is possible. Example: iex> Mojito.Headers.convert_values_to_string([{"content-length", 0}]) [{"content-length", "0"}] """ @spec convert_values_to_string(headers) :: headers def convert_values_to_string(headers) do convert_values_to_string(headers, []) end defp convert_values_to_string([], converted_headers), do: Enum.reverse(converted_headers) defp convert_values_to_string([{name, value} | rest], converted_headers) when is_number(value) or is_atom(value) do convert_values_to_string(rest, [ {name, to_string(value)} | converted_headers ]) end defp convert_values_to_string([headers | rest], converted_headers) do convert_values_to_string(rest, [headers | converted_headers]) end end