Talk a bit about keyword syntax in the Keyword module (#5574)

defmodule Keyword do
  @moduledoc """
  A set of functions for working with keywords.

  A keyword is a list of two-element tuples where the first
  element of the tuple is an atom and the second element
  can be any value.

  For example, the following is a keyword list:

      [{:exit_on_close, true}, {:active, :once}, {:packet_size, 1024}]

  Elixir provides a special and more concise syntax for keyword lists
  that looks like this:

      [exit_on_close: true, active: :once, packet_size: 1024]

  This is also the syntax that Elixir uses to inspect keyword lists:

      iex> [{:active, :once}]
      [active: :once]

  The two syntaxes are completely equivalent. Note that when keyword
  lists are passed as the last argument to a function, if the short-hand
  syntax is used then the square brackets around the keyword list can
  be omitted as well. For example, the following:

      String.split("1-0", "-", trim: true, parts: 2)

  is equivalent to:

      String.split("1-0", "-", [trim: true, parts: 2])

  A keyword may have duplicated keys so it is not strictly
  a key-value store. However most of the functions in this module
  behave exactly as a dictionary so they work similarly to
  the functions you would find in the `Map` module.

  For example, `Keyword.get/3` will get the first entry matching
  the given key, regardless if duplicated entries exist.
  Similarly, `Keyword.put/3` and `Keyword.delete/3` ensure all
  duplicated entries for a given key are removed when invoked.
  Note that operations that require keys to be found in the keyword
  list (like `Keyword.get/3`) need to traverse the list in order
  to find keys, so these operations may be slower than their map
  counterparts.

  A handful of functions exist to handle duplicated keys, in
  particular, `Enum.into/2` allows creating new keywords without
  removing duplicated keys, `get_values/2` returns all values for
  a given key and `delete_first/2` deletes just one of the existing
  entries.

  The functions in `Keyword` do not guarantee any property when
  it comes to ordering. However, since a keyword list is simply a
  list, all the operations defined in `Enum` and `List` can be
  applied too, especially when ordering is required.
  """

  @compile :inline_list_funcs

  @type key :: atom
  @type value :: any

  @type t :: [{key, value}]
  @type t(value) :: [{key, value}]

  @doc """
  Returns `true` if `term` is a keyword list; otherwise returns `false`.

  ## Examples

      iex> Keyword.keyword?([])
      true
      iex> Keyword.keyword?([a: 1])
      true
      iex> Keyword.keyword?([{Foo, 1}])
      true
      iex> Keyword.keyword?([{}])
      false
      iex> Keyword.keyword?([:key])
      false
      iex> Keyword.keyword?(%{})
      false

  """
  @spec keyword?(term) :: boolean
  def keyword?(term)

  def keyword?([{key, _value} | rest]) when is_atom(key), do: keyword?(rest)
  def keyword?([]),     do: true
  def keyword?(_other), do: false

  @doc """
  Returns an empty keyword list, i.e. an empty list.

  ## Examples

      iex> Keyword.new()
      []

  """
  @spec new :: []
  def new, do: []

  @doc """
  Creates a keyword from an enumerable.

  Duplicated entries are removed, the latest one prevails.
  Unlike `Enum.into(enumerable, [])`, `Keyword.new(enumerable)`
  guarantees the keys are unique.

  ## Examples

      iex> Keyword.new([{:b, 1}, {:a, 2}])
      [b: 1, a: 2]

      iex> Keyword.new([{:a, 1}, {:a, 2}, {:a, 3}])
      [a: 3]

  """
  @spec new(Enum.t) :: t
  def new(pairs) do
    new(pairs, fn pair -> pair end)
  end

  @doc """
  Creates a keyword from an enumerable via the transformation function.

  Duplicated entries are removed, the latest one prevails.
  Unlike `Enum.into(enumerable, [], fun)`,
  `Keyword.new(enumerable, fun)` guarantees the keys are unique.

  ## Examples

      iex> Keyword.new([:a, :b], fn(x) -> {x, x} end)
      [a: :a, b: :b]

  """
  @spec new(Enum.t, (term -> {key, value})) :: t
  def new(pairs, transform) do
    fun = fn el, acc ->
      {k, v} = transform.(el)
      put_new(acc, k, v)
    end
    :lists.foldl(fun, [], Enum.reverse(pairs))
  end

  @doc """
  Gets the value for a specific `key`.

  If `key` does not exist, return the default value
  (`nil` if no default value).

  If duplicated entries exist, the first one is returned.
  Use `get_values/2` to retrieve all entries.

  ## Examples

      iex> Keyword.get([], :a)
      nil
      iex> Keyword.get([a: 1], :a)
      1
      iex> Keyword.get([a: 1], :b)
      nil
      iex> Keyword.get([a: 1], :b, 3)
      3

  With duplicated keys:

      iex> Keyword.get([a: 1, a: 2], :a, 3)
      1
      iex> Keyword.get([a: 1, a: 2], :b, 3)
      3

  """
  @spec get(t, key) :: value
  @spec get(t, key, value) :: value
  def get(keywords, key, default \\ nil) when is_list(keywords) and is_atom(key) do
    case :lists.keyfind(key, 1, keywords) do
      {^key, value} -> value
      false -> default
    end
  end

  @doc """
  Gets the value for a specific `key`.

  If `key` does not exist, lazily evaluates `fun` and returns its result.

  This is useful if the default value is very expensive to calculate or
  generally difficult to setup and teardown again.

  If duplicated entries exist, the first one is returned.
  Use `get_values/2` to retrieve all entries.

  ## Examples

      iex> keyword = [a: 1]
      iex> fun = fn ->
      ...>   # some expensive operation here
      ...>   13
      ...> end
      iex> Keyword.get_lazy(keyword, :a, fun)
      1
      iex> Keyword.get_lazy(keyword, :b, fun)
      13

  """
  @spec get_lazy(t, key, (() -> value)) :: value
  def get_lazy(keywords, key, fun)
      when is_list(keywords) and is_atom(key) and is_function(fun, 0) do
    case :lists.keyfind(key, 1, keywords) do
      {^key, value} -> value
      false -> fun.()
    end
  end

  @doc """
  Gets the value from `key` and updates it, all in one pass.

  This `fun` argument receives the value of `key` (or `nil` if `key`
  is not present) and must return a two-element tuple: the "get" value
  (the retrieved value, which can be operated on before being returned)
  and the new value to be stored under `key`. The `fun` may also
  return `:pop`, implying the current value shall be removed from the
  keyword list and returned.

  The returned value is a tuple with the "get" value returned by
  `fun` and a new keyword list with the updated value under `key`.

  ## Examples

      iex> Keyword.get_and_update([a: 1], :a, fn current_value ->
      ...>   {current_value, "new value!"}
      ...> end)
      {1, [a: "new value!"]}

      iex> Keyword.get_and_update([a: 1], :b, fn current_value ->
      ...>   {current_value, "new value!"}
      ...> end)
      {nil, [b: "new value!", a: 1]}

      iex> Keyword.get_and_update([a: 1], :a, fn _ -> :pop end)
      {1, []}

      iex> Keyword.get_and_update([a: 1], :b, fn _ -> :pop end)
      {nil, [a: 1]}

  """
  @spec get_and_update(t, key, (value -> {get, value} | :pop)) :: {get, t} when get: term
  def get_and_update(keywords, key, fun)
    when is_list(keywords) and is_atom(key),
    do: get_and_update(keywords, [], key, fun)

  defp get_and_update([{key, current} | t], acc, key, fun) do
    case fun.(current) do
      {get, value} ->
        {get, :lists.reverse(acc, [{key, value} | t])}
      :pop ->
        {current, :lists.reverse(acc, t)}
      other ->
        raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
    end
  end

  defp get_and_update([{_, _} = h | t], acc, key, fun),
    do: get_and_update(t, [h | acc], key, fun)

  defp get_and_update([], acc, key, fun) do
    case fun.(nil) do
      {get, update} ->
        {get, [{key, update} | :lists.reverse(acc)]}
      :pop ->
        {nil, :lists.reverse(acc)}
      other ->
        raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
    end
  end

  @doc """
  Gets the value from `key` and updates it. Raises if there is no `key`.

  This `fun` argument receives the value of `key` and must return a
  two-element tuple: the "get" value (the retrieved value, which can be
  operated on before being returned) and the new value to be stored under
  `key`.

  The returned value is a tuple with the "get" value returned by `fun` and a new
  keyword list with the updated value under `key`.

  ## Examples

      iex> Keyword.get_and_update!([a: 1], :a, fn current_value ->
      ...>   {current_value, "new value!"}
      ...> end)
      {1, [a: "new value!"]}

      iex> Keyword.get_and_update!([a: 1], :b, fn current_value ->
      ...>   {current_value, "new value!"}
      ...> end)
      ** (KeyError) key :b not found in: [a: 1]

      iex> Keyword.get_and_update!([a: 1], :a, fn _ ->
      ...>   :pop
      ...> end)
      {1, []}

  """
  @spec get_and_update!(t, key, (value -> {get, value})) :: {get, t} | no_return when get: term
  def get_and_update!(keywords, key, fun) do
    get_and_update!(keywords, key, fun, [])
  end

  defp get_and_update!([{key, value} | keywords], key, fun, acc) do
    case fun.(value) do
      {get, value} ->
        {get, :lists.reverse(acc, [{key, value} | delete(keywords, key)])}
      :pop ->
        {value, :lists.reverse(acc, keywords)}
      other ->
        raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
    end
  end

  defp get_and_update!([{_, _} = e | keywords], key, fun, acc) do
    get_and_update!(keywords, key, fun, [e | acc])
  end

  defp get_and_update!([], key, _fun, acc) when is_atom(key) do
    raise(KeyError, key: key, term: acc)
  end

  @doc """
  Fetches the value for a specific `key` and returns it in a tuple.

  If the `key` does not exist, returns `:error`.

  ## Examples

      iex> Keyword.fetch([a: 1], :a)
      {:ok, 1}
      iex> Keyword.fetch([a: 1], :b)
      :error

  """
  @spec fetch(t, key) :: {:ok, value} | :error
  def fetch(keywords, key) when is_list(keywords) and is_atom(key) do
    case :lists.keyfind(key, 1, keywords) do
      {^key, value} -> {:ok, value}
      false -> :error
    end
  end

  @doc """
  Fetches the value for specific `key`.

  If `key` does not exist, a `KeyError` is raised.

  ## Examples

      iex> Keyword.fetch!([a: 1], :a)
      1
      iex> Keyword.fetch!([a: 1], :b)
      ** (KeyError) key :b not found in: [a: 1]

  """
  @spec fetch!(t, key) :: value | no_return
  def fetch!(keywords, key) when is_list(keywords) and is_atom(key) do
    case :lists.keyfind(key, 1, keywords) do
      {^key, value} -> value
      false -> raise(KeyError, key: key, term: keywords)
    end
  end

  @doc """
  Gets all values for a specific `key`.

  ## Examples

      iex> Keyword.get_values([], :a)
      []
      iex> Keyword.get_values([a: 1], :a)
      [1]
      iex> Keyword.get_values([a: 1, a: 2], :a)
      [1, 2]

  """
  @spec get_values(t, key) :: [value]
  def get_values(keywords, key) when is_list(keywords) and is_atom(key) do
    fun = fn
      {^key, val} -> {true, val}
      {_, _} -> false
    end
    :lists.filtermap(fun, keywords)
  end

  @doc """
  Returns all keys from the keyword list.

  Duplicated keys appear duplicated in the final list of keys.

  ## Examples

      iex> Keyword.keys([a: 1, b: 2])
      [:a, :b]
      iex> Keyword.keys([a: 1, b: 2, a: 3])
      [:a, :b, :a]

  """
  @spec keys(t) :: [key]
  def keys(keywords) when is_list(keywords) do
    :lists.map(fn {k, _} -> k end, keywords)
  end

  @doc """
  Returns all values from the keyword list.

  Values from duplicated keys will be kept in the final list of values.

  ## Examples

      iex> Keyword.values([a: 1, b: 2])
      [1, 2]
      iex> Keyword.values([a: 1, b: 2, a: 3])
      [1, 2, 3]

  """
  @spec values(t) :: [value]
  def values(keywords) when is_list(keywords) do
    :lists.map(fn {_, v} -> v end, keywords)
  end

  @doc """
  Deletes the entries in the keyword list for a `key` with `value`.

  If no `key` with `value` exists, returns the keyword list unchanged.

  ## Examples

      iex> Keyword.delete([a: 1, b: 2], :a, 1)
      [b: 2]
      iex> Keyword.delete([a: 1, b: 2, a: 3], :a, 3)
      [a: 1, b: 2]
      iex> Keyword.delete([a: 1], :a, 5)
      [a: 1]
      iex> Keyword.delete([a: 1], :b, 5)
      [a: 1]

  """
  @spec delete(t, key, value) :: t
  def delete(keywords, key, value) when is_list(keywords) and is_atom(key) do
    :lists.filter(fn {k, v} -> k != key or v != value end, keywords)
  end

  @doc """
  Deletes the entries in the keyword list for a specific `key`.

  If the `key` does not exist, returns the keyword list unchanged.
  Use `delete_first/2` to delete just the first entry in case of
  duplicated keys.

  ## Examples

      iex> Keyword.delete([a: 1, b: 2], :a)
      [b: 2]
      iex> Keyword.delete([a: 1, b: 2, a: 3], :a)
      [b: 2]
      iex> Keyword.delete([b: 2], :a)
      [b: 2]

  """
  @spec delete(t, key) :: t
  def delete(keywords, key) when is_list(keywords) and is_atom(key) do
    :lists.filter(fn {k, _} -> k != key end, keywords)
  end

  @doc """
  Deletes the first entry in the keyword list for a specific `key`.

  If the `key` does not exist, returns the keyword list unchanged.

  ## Examples

      iex> Keyword.delete_first([a: 1, b: 2, a: 3], :a)
      [b: 2, a: 3]
      iex> Keyword.delete_first([b: 2], :a)
      [b: 2]

  """
  @spec delete_first(t, key) :: t
  def delete_first(keywords, key) when is_list(keywords) and is_atom(key) do
    :lists.keydelete(key, 1, keywords)
  end

  @doc """
  Puts the given `value` under `key`.

  If a previous value is already stored, all entries are
  removed and the value is overridden.

  ## Examples

      iex> Keyword.put([a: 1], :b, 2)
      [b: 2, a: 1]
      iex> Keyword.put([a: 1, b: 2], :a, 3)
      [a: 3, b: 2]
      iex> Keyword.put([a: 1, b: 2, a: 4], :a, 3)
      [a: 3, b: 2]

  """
  @spec put(t, key, value) :: t
  def put(keywords, key, value) when is_list(keywords) and is_atom(key) do
    [{key, value} | delete(keywords, key)]
  end

  @doc """
  Evaluates `fun` and puts the result under `key`
  in keyword list unless `key` is already present.

  This is useful if the value is very expensive to calculate or
  generally difficult to setup and teardown again.

  ## Examples

      iex> keyword = [a: 1]
      iex> fun = fn ->
      ...>   # some expensive operation here
      ...>   3
      ...> end
      iex> Keyword.put_new_lazy(keyword, :a, fun)
      [a: 1]
      iex> Keyword.put_new_lazy(keyword, :b, fun)
      [b: 3, a: 1]

  """
  @spec put_new_lazy(t, key, (() -> value)) :: t
  def put_new_lazy(keywords, key, fun)
      when is_list(keywords) and is_atom(key) and is_function(fun, 0) do
    case :lists.keyfind(key, 1, keywords) do
      {^key, _} -> keywords
      false -> [{key, fun.()} | keywords]
    end
  end

  @doc """
  Puts the given `value` under `key` unless the entry `key`
  already exists.

  ## Examples

      iex> Keyword.put_new([a: 1], :b, 2)
      [b: 2, a: 1]
      iex> Keyword.put_new([a: 1, b: 2], :a, 3)
      [a: 1, b: 2]

  """
  @spec put_new(t, key, value) :: t
  def put_new(keywords, key, value) when is_list(keywords) and is_atom(key) do
    case :lists.keyfind(key, 1, keywords) do
      {^key, _} -> keywords
      false -> [{key, value} | keywords]
    end
  end

  @doc """
  Checks if two keywords are equal.

  Two keywords are considered to be equal if they contain
  the same keys and those keys contain the same values.

  ## Examples

      iex> Keyword.equal?([a: 1, b: 2], [b: 2, a: 1])
      true
      iex> Keyword.equal?([a: 1, b: 2], [b: 1, a: 2])
      false
      iex> Keyword.equal?([a: 1, b: 2, a: 3], [b: 2, a: 3, a: 1])
      true

  """
  @spec equal?(t, t) :: boolean
  def equal?(left, right) when is_list(left) and is_list(right) do
    :lists.sort(left) == :lists.sort(right)
  end

  @doc """
  Merges two keyword lists into one.

  All keys, including duplicated keys, given in `keywords2` will be added
  to `keywords1`, overriding any existing one.

  There are no guarantees about the order of keys in the returned keyword.

  ## Examples

      iex> Keyword.merge([a: 1, b: 2], [a: 3, d: 4])
      [b: 2, a: 3, d: 4]

      iex> Keyword.merge([a: 1, b: 2], [a: 3, d: 4, a: 5])
      [b: 2, a: 3, d: 4, a: 5]

      iex> Keyword.merge([a: 1], [2, 3])
      ** (ArgumentError) expected a keyword list as the second argument, got: [2, 3]

  """
  @spec merge(t, t) :: t
  def merge(keywords1, keywords2) when is_list(keywords1) and is_list(keywords2) do
    if keyword?(keywords2) do
      fun = fn
        {key, _value} when is_atom(key) ->
          not has_key?(keywords2, key)
        _ ->
          raise ArgumentError, message: "expected a keyword list as the first argument, got: #{inspect keywords1}"
      end
      :lists.filter(fun, keywords1) ++ keywords2
    else
      raise ArgumentError, message: "expected a keyword list as the second argument, got: #{inspect keywords2}"
    end
  end

  @doc """
  Merges two keyword lists into one.

  All keys, including duplicated keys, given in `keywords2` will be added
  to `keywords1`. The given function will be invoked to solve conflicts.

  If `keywords2` has duplicate keys, the given function will be invoked
  for each matching pair in `keywords1`.

  There are no guarantees about the order of keys in the returned keyword.

  ## Examples

      iex> Keyword.merge([a: 1, b: 2], [a: 3, d: 4], fn _k, v1, v2 ->
      ...>   v1 + v2
      ...> end)
      [b: 2, a: 4, d: 4]

      iex> Keyword.merge([a: 1, b: 2], [a: 3, d: 4, a: 5], fn :a, v1, v2 ->
      ...>  v1 + v2
      ...> end)
      [b: 2, a: 4, d: 4, a: 5]

      iex> Keyword.merge([a: 1, b: 2, a: 3], [a: 3, d: 4, a: 5], fn :a, v1, v2 ->
      ...>  v1 + v2
      ...> end)
      [b: 2, a: 4, d: 4, a: 8]

      iex> Keyword.merge([a: 1, b: 2], [:a, :b], fn :a, v1, v2 ->
      ...>  v1 + v2
      ...> end)
      ** (ArgumentError) expected a keyword list as the second argument, got: [:a, :b]

  """
  @spec merge(t, t, (key, value, value -> value)) :: t
  def merge(keywords1, keywords2, fun) when is_list(keywords1) and is_list(keywords2) and is_function(fun, 3) do
    if keyword?(keywords1) do
      do_merge(keywords2, [], keywords1, keywords1, fun, keywords2)
    else
      raise ArgumentError, message: "expected a keyword list as the first argument, got: #{inspect keywords1}"
    end
  end

  defp do_merge([{key, value2} | tail], acc, rest, original, fun, keywords2) when is_atom(key) do
    case :lists.keyfind(key, 1, original) do
      {^key, value1} ->
        do_merge(tail, [{key, fun.(key, value1, value2)} | acc],
                 delete(rest, key), :lists.keydelete(key, 1, original), fun, keywords2)

      false ->
        do_merge(tail, [{key, value2} | acc], rest, original, fun, keywords2)
    end
  end

  defp do_merge([], acc, rest, _original, _fun, _keywords2) do
    rest ++ :lists.reverse(acc)
  end

  defp do_merge(_other, _acc, _rest, _original, _fun, keywords2) do
    raise ArgumentError, message: "expected a keyword list as the second argument, got: #{inspect keywords2}"
  end

  @doc """
  Returns whether a given `key` exists in the given `keywords`.

  ## Examples

      iex> Keyword.has_key?([a: 1], :a)
      true
      iex> Keyword.has_key?([a: 1], :b)
      false

  """
  @spec has_key?(t, key) :: boolean
  def has_key?(keywords, key) when is_list(keywords) and is_atom(key) do
    :lists.keymember(key, 1, keywords)
  end

  @doc """
  Updates the `key` with the given function.

  If the `key` does not exist, raises `KeyError`.

  If there are duplicated keys, they are all removed and only the first one
  is updated.

  ## Examples

      iex> Keyword.update!([a: 1], :a, &(&1 * 2))
      [a: 2]
      iex> Keyword.update!([a: 1, a: 2], :a, &(&1 * 2))
      [a: 2]

      iex> Keyword.update!([a: 1], :b, &(&1 * 2))
      ** (KeyError) key :b not found in: [a: 1]

  """
  @spec update!(t, key, (value -> value)) :: t | no_return
  def update!(keywords, key, fun) do
    update!(keywords, key, fun, keywords)
  end

  defp update!([{key, value} | keywords], key, fun, _dict) do
    [{key, fun.(value)} | delete(keywords, key)]
  end

  defp update!([{_, _} = e | keywords], key, fun, dict) do
    [e | update!(keywords, key, fun, dict)]
  end

  defp update!([], key, _fun, dict) when is_atom(key) do
    raise(KeyError, key: key, term: dict)
  end

  @doc """
  Updates the `key` in `keywords` with the given function.

  If the `key` does not exist, inserts the given `initial` value.

  If there are duplicated keys, they are all removed and only the first one
  is updated.

  ## Examples

      iex> Keyword.update([a: 1], :a, 13, &(&1 * 2))
      [a: 2]
      iex> Keyword.update([a: 1, a: 2], :a, 13, &(&1 * 2))
      [a: 2]
      iex> Keyword.update([a: 1], :b, 11, &(&1 * 2))
      [a: 1, b: 11]

  """
  @spec update(t, key, value, (value -> value)) :: t
  def update(keywords, key, initial, fun)

  def update([{key, value} | keywords], key, _initial, fun) do
    [{key, fun.(value)} | delete(keywords, key)]
  end

  def update([{_, _} = e | keywords], key, initial, fun) do
    [e | update(keywords, key, initial, fun)]
  end

  def update([], key, initial, _fun) when is_atom(key) do
    [{key, initial}]
  end

  @doc """
  Takes all entries corresponding to the given keys and extracts them into a
  separate keyword list.

  Returns a tuple with the new list and the old list with removed keys.

  Keys for which there are no entries in the keyword list are ignored.

  Entries with duplicated keys end up in the same keyword list.

  ## Examples

      iex> Keyword.split([a: 1, b: 2, c: 3], [:a, :c, :e])
      {[a: 1, c: 3], [b: 2]}
      iex> Keyword.split([a: 1, b: 2, c: 3, a: 4], [:a, :c, :e])
      {[a: 1, c: 3, a: 4], [b: 2]}

  """
  @spec split(t, [key]) :: {t, t}
  def split(keywords, keys) when is_list(keywords) do
    fun = fn {k, v}, {take, drop} ->
      case k in keys do
        true  -> {[{k, v} | take], drop}
        false -> {take, [{k, v} | drop]}
      end
    end

    acc = {[], []}
    {take, drop} = :lists.foldl(fun, acc, keywords)
    {:lists.reverse(take), :lists.reverse(drop)}
  end

  @doc """
  Takes all entries corresponding to the given keys and returns them in a new
  keyword list.

  Duplicated keys are preserved in the new keyword list.

  ## Examples

      iex> Keyword.take([a: 1, b: 2, c: 3], [:a, :c, :e])
      [a: 1, c: 3]
      iex> Keyword.take([a: 1, b: 2, c: 3, a: 5], [:a, :c, :e])
      [a: 1, c: 3, a: 5]

  """
  @spec take(t, [key]) :: t
  def take(keywords, keys) when is_list(keywords) do
    :lists.filter(fn {k, _} -> k in keys end, keywords)
  end

  @doc """
  Drops the given keys from the keyword list.

  Duplicated keys are preserved in the new keyword list.

  ## Examples

      iex> Keyword.drop([a: 1, b: 2, c: 3], [:b, :d])
      [a: 1, c: 3]
      iex> Keyword.drop([a: 1, b: 2, b: 3, c: 3, a: 5], [:b, :d])
      [a: 1, c: 3, a: 5]

  """
  @spec drop(t, [key]) :: t
  def drop(keywords, keys) when is_list(keywords) do
    :lists.filter(fn {k, _} -> not k in keys end, keywords)
  end

  @doc """
  Returns and removes all values associated with `key` in the keyword list.

  All duplicated keys are removed. See `pop_first/3` for
  removing only the first entry.

  ## Examples

      iex> Keyword.pop([a: 1], :a)
      {1, []}
      iex> Keyword.pop([a: 1], :b)
      {nil, [a: 1]}
      iex> Keyword.pop([a: 1], :b, 3)
      {3, [a: 1]}
      iex> Keyword.pop([a: 1, a: 2], :a)
      {1, []}

  """
  @spec pop(t, key, value) :: {value, t}
  def pop(keywords, key, default \\ nil) when is_list(keywords) do
    case fetch(keywords, key) do
      {:ok, value} ->
        {value, delete(keywords, key)}
      :error ->
        {default, keywords}
    end
  end

  @doc """
  Lazily returns and removes all values associated with `key` in the keyword list.

  This is useful if the default value is very expensive to calculate or
  generally difficult to setup and teardown again.

  All duplicated keys are removed. See `pop_first/3` for
  removing only the first entry.

  ## Examples

      iex> keyword = [a: 1]
      iex> fun = fn ->
      ...>   # some expensive operation here
      ...>   13
      ...> end
      iex> Keyword.pop_lazy(keyword, :a, fun)
      {1, []}
      iex> Keyword.pop_lazy(keyword, :b, fun)
      {13, [a: 1]}

  """
  @spec pop_lazy(t, key, (() -> value)) :: {value, t}
  def pop_lazy(keywords, key, fun)
      when is_list(keywords) and is_function(fun, 0) do
    case fetch(keywords, key) do
      {:ok, value} ->
        {value, delete(keywords, key)}
      :error ->
        {fun.(), keywords}
    end
  end

  @doc """
  Returns and removes the first value associated with `key` in the keyword list.

  Duplicated keys are not removed.

  ## Examples

      iex> Keyword.pop_first([a: 1], :a)
      {1, []}
      iex> Keyword.pop_first([a: 1], :b)
      {nil, [a: 1]}
      iex> Keyword.pop_first([a: 1], :b, 3)
      {3, [a: 1]}
      iex> Keyword.pop_first([a: 1, a: 2], :a)
      {1, [a: 2]}

  """
  @spec pop_first(t, key, value) :: {value, t}
  def pop_first(keywords, key, default \\ nil) when is_list(keywords) do
    case :lists.keytake(key, 1, keywords) do
      {:value, {^key, value}, rest} -> {value, rest}
      false -> {default, keywords}
    end
  end

  @doc """
  Returns the keyword list itself.

  ## Examples

      iex> Keyword.to_list([a: 1])
      [a: 1]

  """
  @spec to_list(t) :: t
  def to_list(keyword) when is_list(keyword) do
    keyword
  end

  @doc false
  # TODO: Remove on 2.0
  # (hard-deprecated in elixir_dispatch)
  def size(keyword) do
    length(keyword)
  end
end

defmodule Keyword do
  @moduledoc """
  A set of functions for working with keywords.

  A keyword is a list of two-element tuples where the first
  element of the tuple is an atom and the second element
  can be any value.

  A keyword may have duplicated keys so it is not strictly
  a key-value store. However most of the functions in this module
  behave exactly as a dictionary so they work similarly to
  the functions you would find in the `Map` module.

  For example, `Keyword.get/3` will get the first entry matching
  the given key, regardless if duplicated entries exist.
  Similarly, `Keyword.put/3` and `Keyword.delete/3` ensure all
  duplicated entries for a given key are removed when invoked.

  A handful of functions exist to handle duplicated keys, in
  particular, `Enum.into/2` allows creating new keywords without
  removing duplicated keys, `get_values/2` returns all values for
  a given key and `delete_first/2` deletes just one of the existing
  entries.

  The functions in Keyword do not guarantee any property when
  it comes to ordering. However, since a keyword list is simply a
  list, all the operations defined in `Enum` and `List` can be
  applied too, especially when ordering is required.
  """

  @compile :inline_list_funcs

  @type key :: atom
  @type value :: any

  @type t :: [{key, value}]
  @type t(value) :: [{key, value}]

  @doc """
  Returns `true` if `term` is a keyword list; otherwise returns `false`.

  ## Examples

      iex> Keyword.keyword?([])
      true
      iex> Keyword.keyword?([a: 1])
      true
      iex> Keyword.keyword?([{Foo, 1}])
      true
      iex> Keyword.keyword?([{}])
      false
      iex> Keyword.keyword?([:key])
      false
      iex> Keyword.keyword?(%{})
      false

  """
  @spec keyword?(term) :: boolean
  def keyword?(term)

  def keyword?([{key, _value} | rest]) when is_atom(key), do: keyword?(rest)
  def keyword?([]),     do: true
  def keyword?(_other), do: false

  @doc """
  Returns an empty keyword list, i.e. an empty list.

  ## Examples

      iex> Keyword.new()
      []

  """
  @spec new :: []
  def new, do: []

  @doc """
  Creates a keyword from an enumerable.

  Duplicated entries are removed, the latest one prevails.
  Unlike `Enum.into(enumerable, [])`, `Keyword.new(enumerable)`
  guarantees the keys are unique.

  ## Examples

      iex> Keyword.new([{:b, 1}, {:a, 2}])
      [b: 1, a: 2]

      iex> Keyword.new([{:a, 1}, {:a, 2}, {:a, 3}])
      [a: 3]

  """
  @spec new(Enum.t) :: t
  def new(pairs) do
    new(pairs, fn pair -> pair end)
  end

  @doc """
  Creates a keyword from an enumerable via the transformation function.

  Duplicated entries are removed, the latest one prevails.
  Unlike `Enum.into(enumerable, [], fun)`,
  `Keyword.new(enumerable, fun)` guarantees the keys are unique.

  ## Examples

      iex> Keyword.new([:a, :b], fn(x) -> {x, x} end)
      [a: :a, b: :b]

  """
  @spec new(Enum.t, (term -> {key, value})) :: t
  def new(pairs, transform) do
    fun = fn el, acc ->
      {k, v} = transform.(el)
      put_new(acc, k, v)
    end
    :lists.foldl(fun, [], Enum.reverse(pairs))
  end

  @doc """
  Gets the value for a specific `key`.

  If `key` does not exist, return the default value
  (`nil` if no default value).

  If duplicated entries exist, the first one is returned.
  Use `get_values/2` to retrieve all entries.

  ## Examples

      iex> Keyword.get([], :a)
      nil
      iex> Keyword.get([a: 1], :a)
      1
      iex> Keyword.get([a: 1], :b)
      nil
      iex> Keyword.get([a: 1], :b, 3)
      3

  With duplicated keys:

      iex> Keyword.get([a: 1, a: 2], :a, 3)
      1
      iex> Keyword.get([a: 1, a: 2], :b, 3)
      3

  """
  @spec get(t, key) :: value
  @spec get(t, key, value) :: value
  def get(keywords, key, default \\ nil) when is_list(keywords) and is_atom(key) do
    case :lists.keyfind(key, 1, keywords) do
      {^key, value} -> value
      false -> default
    end
  end

  @doc """
  Gets the value for a specific `key`.

  If `key` does not exist, lazily evaluates `fun` and returns its result.

  This is useful if the default value is very expensive to calculate or
  generally difficult to setup and teardown again.

  If duplicated entries exist, the first one is returned.
  Use `get_values/2` to retrieve all entries.

  ## Examples

      iex> keyword = [a: 1]
      iex> fun = fn ->
      ...>   # some expensive operation here
      ...>   13
      ...> end
      iex> Keyword.get_lazy(keyword, :a, fun)
      1
      iex> Keyword.get_lazy(keyword, :b, fun)
      13

  """
  @spec get_lazy(t, key, (() -> value)) :: value
  def get_lazy(keywords, key, fun)
      when is_list(keywords) and is_atom(key) and is_function(fun, 0) do
    case :lists.keyfind(key, 1, keywords) do
      {^key, value} -> value
      false -> fun.()
    end
  end

  @doc """
  Gets the value from `key` and updates it, all in one pass.

  This `fun` argument receives the value of `key` (or `nil` if `key`
  is not present) and must return a two-element tuple: the "get" value
  (the retrieved value, which can be operated on before being returned)
  and the new value to be stored under `key`. The `fun` may also
  return `:pop`, implying the current value shall be removed from the
  keyword list and returned.

  The returned value is a tuple with the "get" value returned by
  `fun` and a new keyword list with the updated value under `key`.

  ## Examples

      iex> Keyword.get_and_update([a: 1], :a, fn current_value ->
      ...>   {current_value, "new value!"}
      ...> end)
      {1, [a: "new value!"]}

      iex> Keyword.get_and_update([a: 1], :b, fn current_value ->
      ...>   {current_value, "new value!"}
      ...> end)
      {nil, [b: "new value!", a: 1]}

      iex> Keyword.get_and_update([a: 1], :a, fn _ -> :pop end)
      {1, []}

      iex> Keyword.get_and_update([a: 1], :b, fn _ -> :pop end)
      {nil, [a: 1]}

  """
  @spec get_and_update(t, key, (value -> {get, value} | :pop)) :: {get, t} when get: term
  def get_and_update(keywords, key, fun)
    when is_list(keywords) and is_atom(key),
    do: get_and_update(keywords, [], key, fun)

  defp get_and_update([{key, current} | t], acc, key, fun) do
    case fun.(current) do
      {get, value} ->
        {get, :lists.reverse(acc, [{key, value} | t])}
      :pop ->
        {current, :lists.reverse(acc, t)}
      other ->
        raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
    end
  end

  defp get_and_update([{_, _} = h | t], acc, key, fun),
    do: get_and_update(t, [h | acc], key, fun)

  defp get_and_update([], acc, key, fun) do
    case fun.(nil) do
      {get, update} ->
        {get, [{key, update} | :lists.reverse(acc)]}
      :pop ->
        {nil, :lists.reverse(acc)}
      other ->
        raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
    end
  end

  @doc """
  Gets the value from `key` and updates it. Raises if there is no `key`.

  This `fun` argument receives the value of `key` and must return a
  two-element tuple: the "get" value (the retrieved value, which can be
  operated on before being returned) and the new value to be stored under
  `key`.

  The returned value is a tuple with the "get" value returned by `fun` and a new
  keyword list with the updated value under `key`.

  ## Examples

      iex> Keyword.get_and_update!([a: 1], :a, fn current_value ->
      ...>   {current_value, "new value!"}
      ...> end)
      {1, [a: "new value!"]}

      iex> Keyword.get_and_update!([a: 1], :b, fn current_value ->
      ...>   {current_value, "new value!"}
      ...> end)
      ** (KeyError) key :b not found in: [a: 1]

      iex> Keyword.get_and_update!([a: 1], :a, fn _ ->
      ...>   :pop
      ...> end)
      {1, []}

  """
  @spec get_and_update!(t, key, (value -> {get, value})) :: {get, t} | no_return when get: term
  def get_and_update!(keywords, key, fun) do
    get_and_update!(keywords, key, fun, [])
  end

  defp get_and_update!([{key, value} | keywords], key, fun, acc) do
    case fun.(value) do
      {get, value} ->
        {get, :lists.reverse(acc, [{key, value} | delete(keywords, key)])}
      :pop ->
        {value, :lists.reverse(acc, keywords)}
      other ->
        raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
    end
  end

  defp get_and_update!([{_, _} = e | keywords], key, fun, acc) do
    get_and_update!(keywords, key, fun, [e | acc])
  end

  defp get_and_update!([], key, _fun, acc) when is_atom(key) do
    raise(KeyError, key: key, term: acc)
  end

  @doc """
  Fetches the value for a specific `key` and returns it in a tuple.

  If the `key` does not exist, returns `:error`.

  ## Examples

      iex> Keyword.fetch([a: 1], :a)
      {:ok, 1}
      iex> Keyword.fetch([a: 1], :b)
      :error

  """
  @spec fetch(t, key) :: {:ok, value} | :error
  def fetch(keywords, key) when is_list(keywords) and is_atom(key) do
    case :lists.keyfind(key, 1, keywords) do
      {^key, value} -> {:ok, value}
      false -> :error
    end
  end

  @doc """
  Fetches the value for specific `key`.

  If `key` does not exist, a `KeyError` is raised.

  ## Examples

      iex> Keyword.fetch!([a: 1], :a)
      1
      iex> Keyword.fetch!([a: 1], :b)
      ** (KeyError) key :b not found in: [a: 1]

  """
  @spec fetch!(t, key) :: value | no_return
  def fetch!(keywords, key) when is_list(keywords) and is_atom(key) do
    case :lists.keyfind(key, 1, keywords) do
      {^key, value} -> value
      false -> raise(KeyError, key: key, term: keywords)
    end
  end

  @doc """
  Gets all values for a specific `key`.

  ## Examples

      iex> Keyword.get_values([], :a)
      []
      iex> Keyword.get_values([a: 1], :a)
      [1]
      iex> Keyword.get_values([a: 1, a: 2], :a)
      [1, 2]

  """
  @spec get_values(t, key) :: [value]
  def get_values(keywords, key) when is_list(keywords) and is_atom(key) do
    fun = fn
      {^key, val} -> {true, val}
      {_, _} -> false
    end
    :lists.filtermap(fun, keywords)
  end

  @doc """
  Returns all keys from the keyword list.

  Duplicated keys appear duplicated in the final list of keys.

  ## Examples

      iex> Keyword.keys([a: 1, b: 2])
      [:a, :b]
      iex> Keyword.keys([a: 1, b: 2, a: 3])
      [:a, :b, :a]

  """
  @spec keys(t) :: [key]
  def keys(keywords) when is_list(keywords) do
    :lists.map(fn {k, _} -> k end, keywords)
  end

  @doc """
  Returns all values from the keyword list.

  Values from duplicated keys will be kept in the final list of values.

  ## Examples

      iex> Keyword.values([a: 1, b: 2])
      [1, 2]
      iex> Keyword.values([a: 1, b: 2, a: 3])
      [1, 2, 3]

  """
  @spec values(t) :: [value]
  def values(keywords) when is_list(keywords) do
    :lists.map(fn {_, v} -> v end, keywords)
  end

  @doc """
  Deletes the entries in the keyword list for a `key` with `value`.

  If no `key` with `value` exists, returns the keyword list unchanged.

  ## Examples

      iex> Keyword.delete([a: 1, b: 2], :a, 1)
      [b: 2]
      iex> Keyword.delete([a: 1, b: 2, a: 3], :a, 3)
      [a: 1, b: 2]
      iex> Keyword.delete([a: 1], :a, 5)
      [a: 1]
      iex> Keyword.delete([a: 1], :b, 5)
      [a: 1]

  """
  @spec delete(t, key, value) :: t
  def delete(keywords, key, value) when is_list(keywords) and is_atom(key) do
    :lists.filter(fn {k, v} -> k != key or v != value end, keywords)
  end

  @doc """
  Deletes the entries in the keyword list for a specific `key`.

  If the `key` does not exist, returns the keyword list unchanged.
  Use `delete_first/2` to delete just the first entry in case of
  duplicated keys.

  ## Examples

      iex> Keyword.delete([a: 1, b: 2], :a)
      [b: 2]
      iex> Keyword.delete([a: 1, b: 2, a: 3], :a)
      [b: 2]
      iex> Keyword.delete([b: 2], :a)
      [b: 2]

  """
  @spec delete(t, key) :: t
  def delete(keywords, key) when is_list(keywords) and is_atom(key) do
    :lists.filter(fn {k, _} -> k != key end, keywords)
  end

  @doc """
  Deletes the first entry in the keyword list for a specific `key`.

  If the `key` does not exist, returns the keyword list unchanged.

  ## Examples

      iex> Keyword.delete_first([a: 1, b: 2, a: 3], :a)
      [b: 2, a: 3]
      iex> Keyword.delete_first([b: 2], :a)
      [b: 2]

  """
  @spec delete_first(t, key) :: t
  def delete_first(keywords, key) when is_list(keywords) and is_atom(key) do
    :lists.keydelete(key, 1, keywords)
  end

  @doc """
  Puts the given `value` under `key`.

  If a previous value is already stored, all entries are
  removed and the value is overridden.

  ## Examples

      iex> Keyword.put([a: 1], :b, 2)
      [b: 2, a: 1]
      iex> Keyword.put([a: 1, b: 2], :a, 3)
      [a: 3, b: 2]
      iex> Keyword.put([a: 1, b: 2, a: 4], :a, 3)
      [a: 3, b: 2]

  """
  @spec put(t, key, value) :: t
  def put(keywords, key, value) when is_list(keywords) and is_atom(key) do
    [{key, value} | delete(keywords, key)]
  end

  @doc """
  Evaluates `fun` and puts the result under `key`
  in keyword list unless `key` is already present.

  This is useful if the value is very expensive to calculate or
  generally difficult to setup and teardown again.

  ## Examples

      iex> keyword = [a: 1]
      iex> fun = fn ->
      ...>   # some expensive operation here
      ...>   3
      ...> end
      iex> Keyword.put_new_lazy(keyword, :a, fun)
      [a: 1]
      iex> Keyword.put_new_lazy(keyword, :b, fun)
      [b: 3, a: 1]

  """
  @spec put_new_lazy(t, key, (() -> value)) :: t
  def put_new_lazy(keywords, key, fun)
      when is_list(keywords) and is_atom(key) and is_function(fun, 0) do
    case :lists.keyfind(key, 1, keywords) do
      {^key, _} -> keywords
      false -> [{key, fun.()} | keywords]
    end
  end

  @doc """
  Puts the given `value` under `key` unless the entry `key`
  already exists.

  ## Examples

      iex> Keyword.put_new([a: 1], :b, 2)
      [b: 2, a: 1]
      iex> Keyword.put_new([a: 1, b: 2], :a, 3)
      [a: 1, b: 2]

  """
  @spec put_new(t, key, value) :: t
  def put_new(keywords, key, value) when is_list(keywords) and is_atom(key) do
    case :lists.keyfind(key, 1, keywords) do
      {^key, _} -> keywords
      false -> [{key, value} | keywords]
    end
  end

  @doc """
  Checks if two keywords are equal.

  Two keywords are considered to be equal if they contain
  the same keys and those keys contain the same values.

  ## Examples

      iex> Keyword.equal?([a: 1, b: 2], [b: 2, a: 1])
      true
      iex> Keyword.equal?([a: 1, b: 2], [b: 1, a: 2])
      false
      iex> Keyword.equal?([a: 1, b: 2, a: 3], [b: 2, a: 3, a: 1])
      true

  """
  @spec equal?(t, t) :: boolean
  def equal?(left, right) when is_list(left) and is_list(right) do
    :lists.sort(left) == :lists.sort(right)
  end

  @doc """
  Merges two keyword lists into one.

  All keys, including duplicated keys, given in `keywords2` will be added
  to `keywords1`, overriding any existing one.

  There are no guarantees about the order of keys in the returned keyword.

  ## Examples

      iex> Keyword.merge([a: 1, b: 2], [a: 3, d: 4])
      [b: 2, a: 3, d: 4]

      iex> Keyword.merge([a: 1, b: 2], [a: 3, d: 4, a: 5])
      [b: 2, a: 3, d: 4, a: 5]

      iex> Keyword.merge([a: 1], [2, 3])
      ** (ArgumentError) expected a keyword list as the second argument, got: [2, 3]

  """
  @spec merge(t, t) :: t
  def merge(keywords1, keywords2) when is_list(keywords1) and is_list(keywords2) do
    if keyword?(keywords2) do
      fun = fn
        {key, _value} when is_atom(key) ->
          not has_key?(keywords2, key)
        _ ->
          raise ArgumentError, message: "expected a keyword list as the first argument, got: #{inspect keywords1}"
      end
      :lists.filter(fun, keywords1) ++ keywords2
    else
      raise ArgumentError, message: "expected a keyword list as the second argument, got: #{inspect keywords2}"
    end
  end

  @doc """
  Merges two keyword lists into one.

  All keys, including duplicated keys, given in `keywords2` will be added
  to `keywords1`. The given function will be invoked to solve conflicts.

  If `keywords2` has duplicate keys, the given function will be invoked
  for each matching pair in `keywords1`.

  There are no guarantees about the order of keys in the returned keyword.

  ## Examples

      iex> Keyword.merge([a: 1, b: 2], [a: 3, d: 4], fn _k, v1, v2 ->
      ...>   v1 + v2
      ...> end)
      [b: 2, a: 4, d: 4]

      iex> Keyword.merge([a: 1, b: 2], [a: 3, d: 4, a: 5], fn :a, v1, v2 ->
      ...>  v1 + v2
      ...> end)
      [b: 2, a: 4, d: 4, a: 5]

      iex> Keyword.merge([a: 1, b: 2, a: 3], [a: 3, d: 4, a: 5], fn :a, v1, v2 ->
      ...>  v1 + v2
      ...> end)
      [b: 2, a: 4, d: 4, a: 8]

      iex> Keyword.merge([a: 1, b: 2], [:a, :b], fn :a, v1, v2 ->
      ...>  v1 + v2
      ...> end)
      ** (ArgumentError) expected a keyword list as the second argument, got: [:a, :b]

  """
  @spec merge(t, t, (key, value, value -> value)) :: t
  def merge(keywords1, keywords2, fun) when is_list(keywords1) and is_list(keywords2) and is_function(fun, 3) do
    if keyword?(keywords1) do
      do_merge(keywords2, [], keywords1, keywords1, fun, keywords2)
    else
      raise ArgumentError, message: "expected a keyword list as the first argument, got: #{inspect keywords1}"
    end
  end

  defp do_merge([{key, value2} | tail], acc, rest, original, fun, keywords2) when is_atom(key) do
    case :lists.keyfind(key, 1, original) do
      {^key, value1} ->
        do_merge(tail, [{key, fun.(key, value1, value2)} | acc],
                 delete(rest, key), :lists.keydelete(key, 1, original), fun, keywords2)

      false ->
        do_merge(tail, [{key, value2} | acc], rest, original, fun, keywords2)
    end
  end

  defp do_merge([], acc, rest, _original, _fun, _keywords2) do
    rest ++ :lists.reverse(acc)
  end

  defp do_merge(_other, _acc, _rest, _original, _fun, keywords2) do
    raise ArgumentError, message: "expected a keyword list as the second argument, got: #{inspect keywords2}"
  end

  @doc """
  Returns whether a given `key` exists in the given `keywords`.

  ## Examples

      iex> Keyword.has_key?([a: 1], :a)
      true
      iex> Keyword.has_key?([a: 1], :b)
      false

  """
  @spec has_key?(t, key) :: boolean
  def has_key?(keywords, key) when is_list(keywords) and is_atom(key) do
    :lists.keymember(key, 1, keywords)
  end

  @doc """
  Updates the `key` with the given function.

  If the `key` does not exist, raises `KeyError`.

  If there are duplicated keys, they are all removed and only the first one
  is updated.

  ## Examples

      iex> Keyword.update!([a: 1], :a, &(&1 * 2))
      [a: 2]
      iex> Keyword.update!([a: 1, a: 2], :a, &(&1 * 2))
      [a: 2]

      iex> Keyword.update!([a: 1], :b, &(&1 * 2))
      ** (KeyError) key :b not found in: [a: 1]

  """
  @spec update!(t, key, (value -> value)) :: t | no_return
  def update!(keywords, key, fun) do
    update!(keywords, key, fun, keywords)
  end

  defp update!([{key, value} | keywords], key, fun, _dict) do
    [{key, fun.(value)} | delete(keywords, key)]
  end

  defp update!([{_, _} = e | keywords], key, fun, dict) do
    [e | update!(keywords, key, fun, dict)]
  end

  defp update!([], key, _fun, dict) when is_atom(key) do
    raise(KeyError, key: key, term: dict)
  end

  @doc """
  Updates the `key` in `keywords` with the given function.

  If the `key` does not exist, inserts the given `initial` value.

  If there are duplicated keys, they are all removed and only the first one
  is updated.

  ## Examples

      iex> Keyword.update([a: 1], :a, 13, &(&1 * 2))
      [a: 2]
      iex> Keyword.update([a: 1, a: 2], :a, 13, &(&1 * 2))
      [a: 2]
      iex> Keyword.update([a: 1], :b, 11, &(&1 * 2))
      [a: 1, b: 11]

  """
  @spec update(t, key, value, (value -> value)) :: t
  def update(keywords, key, initial, fun)

  def update([{key, value} | keywords], key, _initial, fun) do
    [{key, fun.(value)} | delete(keywords, key)]
  end

  def update([{_, _} = e | keywords], key, initial, fun) do
    [e | update(keywords, key, initial, fun)]
  end

  def update([], key, initial, _fun) when is_atom(key) do
    [{key, initial}]
  end

  @doc """
  Takes all entries corresponding to the given keys and extracts them into a
  separate keyword list.

  Returns a tuple with the new list and the old list with removed keys.

  Keys for which there are no entries in the keyword list are ignored.

  Entries with duplicated keys end up in the same keyword list.

  ## Examples

      iex> Keyword.split([a: 1, b: 2, c: 3], [:a, :c, :e])
      {[a: 1, c: 3], [b: 2]}
      iex> Keyword.split([a: 1, b: 2, c: 3, a: 4], [:a, :c, :e])
      {[a: 1, c: 3, a: 4], [b: 2]}

  """
  @spec split(t, [key]) :: {t, t}
  def split(keywords, keys) when is_list(keywords) do
    fun = fn {k, v}, {take, drop} ->
      case k in keys do
        true  -> {[{k, v} | take], drop}
        false -> {take, [{k, v} | drop]}
      end
    end

    acc = {[], []}
    {take, drop} = :lists.foldl(fun, acc, keywords)
    {:lists.reverse(take), :lists.reverse(drop)}
  end

  @doc """
  Takes all entries corresponding to the given keys and returns them in a new
  keyword list.

  Duplicated keys are preserved in the new keyword list.

  ## Examples

      iex> Keyword.take([a: 1, b: 2, c: 3], [:a, :c, :e])
      [a: 1, c: 3]
      iex> Keyword.take([a: 1, b: 2, c: 3, a: 5], [:a, :c, :e])
      [a: 1, c: 3, a: 5]

  """
  @spec take(t, [key]) :: t
  def take(keywords, keys) when is_list(keywords) do
    :lists.filter(fn {k, _} -> k in keys end, keywords)
  end

  @doc """
  Drops the given keys from the keyword list.

  Duplicated keys are preserved in the new keyword list.

  ## Examples

      iex> Keyword.drop([a: 1, b: 2, c: 3], [:b, :d])
      [a: 1, c: 3]
      iex> Keyword.drop([a: 1, b: 2, b: 3, c: 3, a: 5], [:b, :d])
      [a: 1, c: 3, a: 5]

  """
  @spec drop(t, [key]) :: t
  def drop(keywords, keys) when is_list(keywords) do
    :lists.filter(fn {k, _} -> not k in keys end, keywords)
  end

  @doc """
  Returns and removes all values associated with `key` in the keyword list.

  All duplicated keys are removed. See `pop_first/3` for
  removing only the first entry.

  ## Examples

      iex> Keyword.pop([a: 1], :a)
      {1, []}
      iex> Keyword.pop([a: 1], :b)
      {nil, [a: 1]}
      iex> Keyword.pop([a: 1], :b, 3)
      {3, [a: 1]}
      iex> Keyword.pop([a: 1, a: 2], :a)
      {1, []}

  """
  @spec pop(t, key, value) :: {value, t}
  def pop(keywords, key, default \\ nil) when is_list(keywords) do
    case fetch(keywords, key) do
      {:ok, value} ->
        {value, delete(keywords, key)}
      :error ->
        {default, keywords}
    end
  end

  @doc """
  Lazily returns and removes all values associated with `key` in the keyword list.

  This is useful if the default value is very expensive to calculate or
  generally difficult to setup and teardown again.

  All duplicated keys are removed. See `pop_first/3` for
  removing only the first entry.

  ## Examples

      iex> keyword = [a: 1]
      iex> fun = fn ->
      ...>   # some expensive operation here
      ...>   13
      ...> end
      iex> Keyword.pop_lazy(keyword, :a, fun)
      {1, []}
      iex> Keyword.pop_lazy(keyword, :b, fun)
      {13, [a: 1]}

  """
  @spec pop_lazy(t, key, (() -> value)) :: {value, t}
  def pop_lazy(keywords, key, fun)
      when is_list(keywords) and is_function(fun, 0) do
    case fetch(keywords, key) do
      {:ok, value} ->
        {value, delete(keywords, key)}
      :error ->
        {fun.(), keywords}
    end
  end

  @doc """
  Returns and removes the first value associated with `key` in the keyword list.

  Duplicated keys are not removed.

  ## Examples

      iex> Keyword.pop_first([a: 1], :a)
      {1, []}
      iex> Keyword.pop_first([a: 1], :b)
      {nil, [a: 1]}
      iex> Keyword.pop_first([a: 1], :b, 3)
      {3, [a: 1]}
      iex> Keyword.pop_first([a: 1, a: 2], :a)
      {1, [a: 2]}

  """
  @spec pop_first(t, key, value) :: {value, t}
  def pop_first(keywords, key, default \\ nil) when is_list(keywords) do
    case :lists.keytake(key, 1, keywords) do
      {:value, {^key, value}, rest} -> {value, rest}
      false -> {default, keywords}
    end
  end

  @doc """
  Returns the keyword list itself.

  ## Examples

      iex> Keyword.to_list([a: 1])
      [a: 1]

  """
  @spec to_list(t) :: t
  def to_list(keyword) when is_list(keyword) do
    keyword
  end

  @doc false
  # TODO: Remove on 2.0
  # (hard-deprecated in elixir_dispatch)
  def size(keyword) do
    length(keyword)
  end
end


Restore page_size

Restore page_size

defmodule ElixirStatus.Repo do
  use Ecto.Repo, otp_app: :elixir_status
  use Scrivener, page_size: 20
end

defmodule ElixirStatus.Repo do
  use Ecto.Repo, otp_app: :elixir_status
  use Scrivener
end


fix typo

fix typo

defmodule Hound.Helpers.Page do

  @doc "Get source of current page"
  defmacro page_source do
    quote do: page_source(var!(meta[:session_id]))
  end

  @doc "Get the title of the current page"
  defmacro page_title do
    quote do: page_title(var!(meta[:session_id]))
  end

  @doc "Find element on current page"
  defmacro find_element(strategy, selector) do
    quote do: find_element(var!(meta[:session_id]), strategy, selector)
  end

  @doc "Find elements on current page"
  defmacro find_all_elements(strategy, selector) do
    quote do: find_all_elements(var!(meta[:session_id]), strategy, selector)
  end

  @doc "Find element within element"
  defmacro find_within_element(strategy, selector) do
    quote do: find_within_element(var!(meta[:session_id]), strategy, selector)
  end

  @doc "Find elements within element"
  defmacro find_all_within_element(strategy, selector) do
    quote do: find_all_within_element(var!(meta[:session_id]), strategy, selector)
  end

  @doc "Get element on page currently in focus"
  defmacro element_in_focus do
    quote do: element_in_focus(var!(meta[:session_id]))
  end

  @doc "Click on element"
  defmacro click_on(element_identifier) do
    quote do: click_on(var!(meta[:session_id]), element_identifier)
  end

  @doc "Submit form"
  defmacro submit(element_identifier) do
    quote do: submit(var!(meta[:session_id]), element_identifier)
  end

  @doc "Send sequence of key strokes to active element. The modifier keys are not released after this command is run."
  defmacro send_keys(keys) do
    quote do: send_keys(var!(meta[:session_id]), keys)
  end

end

defmodule Hound.Behaviours.Page do

  @doc "Get source of current page"
  defmacro page_source do
    quote do: page_source(var!(meta[:session_id]))
  end

  @doc "Get the title of the current page"
  defmacro page_title do
    quote do: page_title(var!(meta[:session_id]))
  end

  @doc "Find element on current page"
  defmacro find_element(strategy, selector) do
    quote do: find_element(var!(meta[:session_id]), strategy, selector)
  end

  @doc "Find elements on current page"
  defmacro find_all_elements(strategy, selector) do
    quote do: find_all_elements(var!(meta[:session_id]), strategy, selector)
  end

  @doc "Find element within element"
  defmacro find_within_element(strategy, selector) do
    quote do: find_within_element(var!(meta[:session_id]), strategy, selector)
  end

  @doc "Find elements within element"
  defmacro find_all_within_element(strategy, selector) do
    quote do: find_all_within_element(var!(meta[:session_id]), strategy, selector)
  end

  @doc "Get element on page currently in focus"
  defmacro element_in_focus do
    quote do: element_in_focus(var!(meta[:session_id]))
  end

  @doc "Click on element"
  defmacro click_on(element_identifier) do
    quote do: click_on(var!(meta[:session_id]), element_identifier)
  end

  @doc "Submit form"
  defmacro submit(element_identifier) do
    quote do: submit(var!(meta[:session_id]), element_identifier)
  end

  @doc "Send sequence of key strokes to active element. The modifier keys are not released after this command is run."
  defmacro send_keys(keys) do
    quote do: send_keys(var!(meta[:session_id]), keys)
  end

end


Add test for overriding erlinit ctty

Add test for overriding erlinit ctty

defmodule Nerves.ErlinitTest do
  use NervesTest.Case, async: false

  alias Nerves.Erlinit

  @example """
  # Additional configuration for erlinit

  # Turn on the debug prints
  #-v

  # Specify where erlinit should send the IEx prompt. Only one may be enabled at
  # a time.

  # Nowhere - let nbtty send it to the gadget USB serial port
  -c null

  # HDMI output
  # -c tty1

  # If more than one tty are available, always warn if the user is looking at the
  # wrong one.
  --warn-unused-tty

  # Use nbtty to improve terminal handling on serial ports.
  # Change this to ttyAMA0 to use the UART pins and remove for HDMI
  -s "/usr/bin/nbtty --tty /dev/ttyGS0 --wait-input"

  # Specify the user and group IDs for the Erlang VM
  #--uid 100
  #--gid 200

  # Uncomment to ensure that the system clock is set to at least the Nerves
  # System's build date/time. If you enable this, you'll still need to use NTP or
  # another mechanism to set the clock, but it won't be decades off.
  #--update-clock

  # Uncomment to hang the board rather than rebooting when Erlang exits
  # NOTE: Do not enable on production boards
  #--hang-on-exit

  # Change the graceful shutdown time. If 10 seconds isn't long enough between
  # calling "poweroff", "reboot", or "halt" and :init.stop/0 stopping all OTP
  # applications, enable this option with a new timeout in milliseconds.
  #--graceful-shutdown-timeout 15000

  # Optionally run a program if the Erlang VM exits
  #--run-on-exit /bin/sh

  # Enable UTF-8 filename handling in Erlang and custom inet configuration
  -e LANG=en_US.UTF-8;LANGUAGE=en;ERL_INETRC=/etc/erl_inetrc;ERL_CRASH_DUMP=/root/crash.dump

  # Mount the application partition (run "man fstab" for field names)
  # NOTE: This must match the location in the fwup.conf. If it doesn't the system
  #       will probably still work fine, but you won't get shell history since
  #       shoehorn/nerves_runtime can't mount the application filesystem before
  #       the history is loaded. If this mount fails due to corruption, etc.,
  #       nerves_runtime will auto-format it. Your applications will need to handle
  #       initializing any expected files and folders.
  -m /dev/mmcblk0p1:/boot:vfat:ro,nodev,noexec,nosuid:
  -m /dev/mmcblk0p3:/root:ext4:nodev:
  -m configfs:/sys/kernel/config:configfs:nodev,noexec,nosuid:
  -m pstore:/sys/fs/pstore:pstore:nodev,noexec,nosuid:
  -m tmpfs:/sys/fs/cgroup:tmpfs:nodev,noexec,nosuid:mode=755,size=1024k
  -m cpu:/sys/fs/cgroup/cpu:cgroup:nodev,noexec,nosuid:cpu
  -m memory:/sys/fs/cgroup/memory:cgroup:nodev,noexec,nosuid:memory

  # Erlang release search path
  -r /srv/erlang

  # Assign a hostname of the form "nerves-<serial_number>".
  # See /etc/boardid.config for locating the serial number.
  -d /usr/bin/boardid
  -n nerves-%s

  # If using shoehorn (https://github.com/nerves-project/shoehorn), start the
  # shoehorn OTP release up first. If shoehorn isn't around, erlinit fails back
  # to the main OTP release.
  --boot shoehorn
  """

  test "parse example file", context do
    in_tmp(context.test, fn ->
      erlinit_opts = Erlinit.decode_config(@example)
      assert erlinit_opts[:ctty] == "null"
      refute erlinit_opts[:verbose]
      assert erlinit_opts[:warn_unused_tty]
    end)
  end

  test "merge options", context do
    in_tmp(context.test, fn ->
      erlinit_opts = Erlinit.decode_config(@example)
      assert Erlinit.merge_opts(erlinit_opts, verbose: true)[:verbose] == true
    end)
  end

  test "remove option", context do
    in_tmp(context.test, fn ->
      erlinit_opts = Erlinit.decode_config(@example)
      merged_opts = Erlinit.merge_opts(erlinit_opts, alternate_exec: nil)
      refute Erlinit.encode_config(merged_opts) =~ "--alternate_exec"
    end)
  end

  test "merge keep options", context do
    in_tmp(context.test, fn ->
      erlinit_opts = Erlinit.decode_config(@example)
      merged_opts = Erlinit.merge_opts(erlinit_opts, mount: "1234")
      assert Erlinit.encode_config(merged_opts) =~ "--mount 1234"
    end)
  end

  test "override ctty", context do
    in_tmp(context.test, fn ->
      erlinit_opts = Erlinit.decode_config(@example)
      assert Erlinit.merge_opts(erlinit_opts, ctty: "1234")[:ctty] == "1234"
    end)
  end
end

defmodule Nerves.ErlinitTest do
  use NervesTest.Case, async: false

  alias Nerves.Erlinit

  @example """
  # Additional configuration for erlinit

  # Turn on the debug prints
  #-v

  # Specify where erlinit should send the IEx prompt. Only one may be enabled at
  # a time.

  # Nowhere - let nbtty send it to the gadget USB serial port
  -c null

  # HDMI output
  # -c tty1

  # If more than one tty are available, always warn if the user is looking at the
  # wrong one.
  --warn-unused-tty

  # Use nbtty to improve terminal handling on serial ports.
  # Change this to ttyAMA0 to use the UART pins and remove for HDMI
  -s "/usr/bin/nbtty --tty /dev/ttyGS0 --wait-input"

  # Specify the user and group IDs for the Erlang VM
  #--uid 100
  #--gid 200

  # Uncomment to ensure that the system clock is set to at least the Nerves
  # System's build date/time. If you enable this, you'll still need to use NTP or
  # another mechanism to set the clock, but it won't be decades off.
  #--update-clock

  # Uncomment to hang the board rather than rebooting when Erlang exits
  # NOTE: Do not enable on production boards
  #--hang-on-exit

  # Change the graceful shutdown time. If 10 seconds isn't long enough between
  # calling "poweroff", "reboot", or "halt" and :init.stop/0 stopping all OTP
  # applications, enable this option with a new timeout in milliseconds.
  #--graceful-shutdown-timeout 15000

  # Optionally run a program if the Erlang VM exits
  #--run-on-exit /bin/sh

  # Enable UTF-8 filename handling in Erlang and custom inet configuration
  -e LANG=en_US.UTF-8;LANGUAGE=en;ERL_INETRC=/etc/erl_inetrc;ERL_CRASH_DUMP=/root/crash.dump

  # Mount the application partition (run "man fstab" for field names)
  # NOTE: This must match the location in the fwup.conf. If it doesn't the system
  #       will probably still work fine, but you won't get shell history since
  #       shoehorn/nerves_runtime can't mount the application filesystem before
  #       the history is loaded. If this mount fails due to corruption, etc.,
  #       nerves_runtime will auto-format it. Your applications will need to handle
  #       initializing any expected files and folders.
  -m /dev/mmcblk0p1:/boot:vfat:ro,nodev,noexec,nosuid:
  -m /dev/mmcblk0p3:/root:ext4:nodev:
  -m configfs:/sys/kernel/config:configfs:nodev,noexec,nosuid:
  -m pstore:/sys/fs/pstore:pstore:nodev,noexec,nosuid:
  -m tmpfs:/sys/fs/cgroup:tmpfs:nodev,noexec,nosuid:mode=755,size=1024k
  -m cpu:/sys/fs/cgroup/cpu:cgroup:nodev,noexec,nosuid:cpu
  -m memory:/sys/fs/cgroup/memory:cgroup:nodev,noexec,nosuid:memory

  # Erlang release search path
  -r /srv/erlang

  # Assign a hostname of the form "nerves-<serial_number>".
  # See /etc/boardid.config for locating the serial number.
  -d /usr/bin/boardid
  -n nerves-%s

  # If using shoehorn (https://github.com/nerves-project/shoehorn), start the
  # shoehorn OTP release up first. If shoehorn isn't around, erlinit fails back
  # to the main OTP release.
  --boot shoehorn
  """

  test "parse example file", context do
    in_tmp(context.test, fn ->
      erlinit_opts = Erlinit.decode_config(@example)
      assert erlinit_opts[:ctty] == "null"
      refute erlinit_opts[:verbose]
      assert erlinit_opts[:warn_unused_tty]
    end)
  end

  test "merge options", context do
    in_tmp(context.test, fn ->
      erlinit_opts = Erlinit.decode_config(@example)
      assert Erlinit.merge_opts(erlinit_opts, verbose: true)[:verbose] == true
    end)
  end

  test "remove option", context do
    in_tmp(context.test, fn ->
      erlinit_opts = Erlinit.decode_config(@example)
      merged_opts = Erlinit.merge_opts(erlinit_opts, alternate_exec: nil)
      refute Erlinit.encode_config(merged_opts) =~ "--alternate_exec"
    end)
  end

  test "merge keep options", context do
    in_tmp(context.test, fn ->
      erlinit_opts = Erlinit.decode_config(@example)
      merged_opts = Erlinit.merge_opts(erlinit_opts, mount: "1234")
      assert Erlinit.encode_config(merged_opts) =~ "--mount 1234"
    end)
  end
end


Refactor Serum.Theme

Refactor Serum.Theme

defmodule Serum.Theme do
  @moduledoc """
  A behaviour that all Serum theme module must implement.

  A Serum theme is a set of predefined templates and assets which are used
  while Serum is building a project.

  More specifically, a Serum theme is a Mix project which has the following
  contents:

  - Modules that implement this behaviour,
  - And theme files such as templates, includes, and other assets.
    These files are usually stored in the `priv/` directory.

  Your theme package must have at least one module that implements this
  behaviour, as Serum will call callbacks of this behaviour to ensure that your
  modules provide appropriate theme resources when needed.

  ## Using Serum Themes

  To use a Serum theme, you first need to add the theme package to your
  dependencies list.

      # mix.exs
      defp deps do
      [
        {:serum, "~> 1.0"},
        # ...

        # If the theme is available on Hex.pm:
        {:serum_theme_sample, "~> 1.0"}

        # If the theme is available on somewhere else:
        {:serum_theme_sample, git: "https://github.com/..."}
      ]
      end

  Fetch and build the theme package using `mix`.

      $ mix do deps.get, deps.compile

  To configure your Serum project to use a theme, you need to put a `:theme`
  key in your `serum.exs`.

      # serum.exs:
      %{
        theme: Serum.Themes.Sample,
        # ...
      }

  Read the documentation provided by the theme author, to see the list of files
  the theme consists of. Files provided by the theme are always overridden by
  corresponding files in your project directory. So it is safe to remove files
  from your project if the theme has ones.

  Finally, try building your project to see if the theme is applied correctly.

      $ mix serum.server
      # Or,
      $ MIX_ENV=prod mix serum.build
  """

  import Serum.IOProxy, only: [put_err: 2]
  alias Serum.Result

  defstruct module: nil,
            name: "",
            description: "",
            author: "",
            legal: "",
            version: Version.parse!("0.0.0")

  @type t :: %__MODULE__{
          module: module() | nil,
          name: binary(),
          description: binary(),
          author: binary(),
          legal: binary(),
          version: Version.t()
        }

  @serum_version Version.parse!(Mix.Project.config()[:version])

  @doc "Returns the theme name."
  @callback name() :: binary()

  @doc "Returns a short descriptive text about the theme."
  @callback description() :: binary()

  @doc "Returns information about the theme author."
  @callback author() :: binary()

  @doc "Returns the legal information about the theme, such as license."
  @callback legal() :: binary()

  @doc """
  Returns the theme version.

  The returned value must follow the semantic versioning scheme.
  """
  @callback version() :: binary()

  @doc """
  Returns the required version of Serum.

  Read the "Requirements" section in the documentation for `Version` module
  for more information about version requirement format.
  """
  @callback serum() :: binary()

  @doc """
  Returns a list of paths to include files.

  All paths in the list must end with `".html.eex"`. Anything that does not end
  with `".html.eex"` will be ignored.

  ## Example Return Value

      [
        "/path/to/theme/priv/includes/nav.html.eex",
        "/path/to/theme/priv/includes/sidebar.html.eex",
        "/path/to/theme/priv/includes/footer.html.eex"
      ]
  """
  @callback get_includes() :: [binary()]

  @doc """
  Returns a list of paths to template files.

  Each path in the list must end with one of these: `"/base.html.eex"`,
  `"/list.html.eex"`, `"/page.html.eex"`, and `"/post.html.eex"`. Anything else
  will be ignored.

  ## Example Return Value

      [
        "/path/to/theme/priv/templates/base.html.eex",
        "/path/to/theme/priv/templates/list.html.eex",
        "/path/to/theme/priv/templates/post.html.eex"
      ]
  """
  @callback get_templates() :: [binary()]

  @doc """
  Returns a path to the assets directory.

  All files in the directory pointed by the returned value will be copied to
  the destination assets directory using `File.cp_r/2`.

  This callback may return `false` to indicate that no asset will be copied.
  """
  @callback get_assets() :: binary() | false

  #
  # Theme Consumer Functions
  #

  @doc false
  @spec load(module() | nil) :: Result.t(t())
  def load(module_or_nil)
  def load(nil), do: {:ok, %__MODULE__{}}

  def load(module) do
    name = module.name()
    version = Version.parse!(module.version())

    validate_serum_version(name, module.serum())

    result = %__MODULE__{
      module: module,
      name: name,
      description: module.description(),
      author: module.author(),
      legal: module.legal(),
      version: version
    }

    {:ok, result}
  rescue
    exception ->
      ex_name = module_name(exception.__struct__)
      ex_msg = Exception.message(exception)
      mod_name = module_name(module)
      msg = "#{ex_name} while loading theme (module: #{mod_name}): #{ex_msg}"

      {:error, msg}
  end

  @spec validate_serum_version(binary(), Version.requirement()) :: :ok
  defp validate_serum_version(name, requirement) do
    if Version.match?(@serum_version, requirement) do
      :ok
    else
      msg =
        "The theme \"#{name}\" is not compatible with " <>
          "the current version of Serum(#{@serum_version}). " <>
          "This theme may not work as intended."

      put_err(:warn, msg)
    end
  end

  @doc false
  @spec get_includes(t()) :: Result.t(%{optional(binary()) => binary()})
  def get_includes(theme)
  def get_includes(%__MODULE__{module: nil}), do: {:ok, %{}}

  def get_includes(%__MODULE__{module: module}) do
    case get_list(module, :get_includes, []) do
      {:ok, paths} ->
        result =
          paths
          |> Enum.filter(&String.ends_with?(&1, ".html.eex"))
          |> Enum.map(&{Path.basename(&1, ".html.eex"), &1})
          |> Map.new()

        {:ok, result}

      {:error, _} = error ->
        error
    end
  end

  @accepted_templates MapSet.new(["base", "list", "page", "post"])

  @doc false
  @spec get_templates(t()) :: Result.t(%{optional(binary()) => binary()})
  def get_templates(theme)
  def get_templates(%__MODULE__{module: nil}), do: {:ok, %{}}

  def get_templates(%__MODULE__{module: module}) do
    case get_list(module, :get_templates, []) do
      {:ok, paths} ->
        result =
          paths
          |> Enum.map(&{Path.basename(&1, ".html.eex"), &1})
          |> Enum.filter(&(elem(&1, 0) in @accepted_templates))
          |> Map.new()

        {:ok, result}

      {:error, _} = error ->
        error
    end
  end

  @spec get_list(atom(), atom(), list()) :: Result.t([binary()])
  defp get_list(module, fun, args) do
    with {:ok, paths} <- call_function(module, fun, args),
         :ok <- check_list_type(paths) do
      {:ok, paths}
    else
      {:error, _} = error ->
        error

      {:bad, x} ->
        callee = "#{module_name(module)}.#{fun}"
        msg = "#{callee}: expected a list of strings, got: #{inspect(x)}"

        {:error, msg}

      {:bad_item, x} ->
        callee = "#{module_name(module)}.#{fun}"
        msg = "#{callee} expected a list of strings, got: #{inspect(x)} in the list"

        {:error, msg}
    end
  end

  @spec check_list_type(term()) :: :ok | {:bad, term()} | {:bad_item, term()}
  defp check_list_type(maybe_list)
  defp check_list_type([]), do: :ok
  defp check_list_type([x | xs]) when is_binary(x), do: check_list_type(xs)
  defp check_list_type([x | _xs]), do: {:bad_item, x}
  defp check_list_type(x), do: {:bad, x}

  @doc false
  @spec get_assets(t()) :: Result.t(binary() | false)
  def get_assets(theme)
  def get_assets(%__MODULE__{module: nil}), do: {:ok, false}

  def get_assets(%__MODULE__{module: module}) do
    case call_function(module, :get_assets, []) do
      {:ok, path} when is_binary(path) ->
        do_get_assets(path)

      {:ok, false} ->
        {:ok, false}

      {:error, _} = error ->
        error

      x ->
        mod_name = module_name(module)
        msg = "#{mod_name}.get_assets: expected a string, got: #{inspect(x)}"

        {:error, msg}
    end
  end

  @spec do_get_assets(binary()) :: Result.t(binary())
  defp do_get_assets(path) do
    case File.stat(path) do
      {:ok, %File.Stat{type: :directory}} -> {:ok, path}
      {:ok, %File.Stat{}} -> {:error, {:enotdir, path, 0}}
      {:error, reason} -> {:error, {reason, path, 0}}
    end
  end

  @spec call_function(atom(), atom(), list()) :: Result.t(term())
  defp call_function(module, fun, args) do
    {:ok, apply(module, fun, args)}
  rescue
    exception ->
      ex_name = module_name(exception.__struct__)
      ex_msg = Exception.message(exception)
      mod_name = module_name(module)
      msg = "#{ex_name} from #{mod_name}.#{fun}: #{ex_msg}"

      {:error, msg}
  end

  @spec module_name(atom()) :: binary()
  defp module_name(module) do
    module |> to_string() |> String.replace_prefix("Elixir.", "")
  end
end

defmodule Serum.Theme do
  @moduledoc """
  A behaviour that all Serum theme module must implement.

  A Serum theme is a set of predefined templates and assets which are used
  while Serum is building a project.

  More specifically, a Serum theme is a Mix project which has the following
  contents:

  - Modules that implement this behaviour,
  - And theme files such as templates, includes, and other assets.
    These files are usually stored in the `priv/` directory.

  Your theme package must have at least one module that implements this
  behaviour, as Serum will call callbacks of this behaviour to ensure that your
  modules provide appropriate theme resources when needed.

  ## Using Serum Themes

  To use a Serum theme, you first need to add the theme package to your
  dependencies list.

      # mix.exs
      defp deps do
      [
        {:serum, "~> 1.0"},
        # ...

        # If the theme is available on Hex.pm:
        {:serum_theme_sample, "~> 1.0"}

        # If the theme is available on somewhere else:
        {:serum_theme_sample, git: "https://github.com/..."}
      ]
      end

  Fetch and build the theme package using `mix`.

      $ mix do deps.get, deps.compile

  To configure your Serum project to use a theme, you need to put a `:theme`
  key in your `serum.exs`.

      # serum.exs:
      %{
        theme: Serum.Themes.Sample,
        # ...
      }

  Read the documentation provided by the theme author, to see the list of files
  the theme consists of. Files provided by the theme are always overridden by
  corresponding files in your project directory. So it is safe to remove files
  from your project if the theme has ones.

  Finally, try building your project to see if the theme is applied correctly.

      $ mix serum.server
      # Or,
      $ MIX_ENV=prod mix serum.build
  """

  alias Serum.Result
  import Serum.IOProxy, only: [put_err: 2]

  defstruct module: nil,
            name: "",
            description: "",
            author: "",
            legal: "",
            version: Version.parse!("0.0.0")

  @type t :: %__MODULE__{
          module: module() | nil,
          name: binary(),
          description: binary(),
          author: binary(),
          legal: binary(),
          version: Version.t()
        }

  @serum_version Version.parse!(Mix.Project.config()[:version])

  @doc "Returns the theme name."
  @callback name() :: binary()

  @doc "Returns a short descriptive text about the theme."
  @callback description() :: binary()

  @doc "Returns information about the theme author."
  @callback author() :: binary()

  @doc "Returns the legal information about the theme, such as license."
  @callback legal() :: binary()

  @doc """
  Returns the theme version.

  The returned value must follow the semantic versioning scheme.
  """
  @callback version() :: binary()

  @doc """
  Returns the required version of Serum.

  Read the "Requirements" section in the documentation for `Version` module
  for more information about version requirement format.
  """
  @callback serum() :: binary()

  @doc """
  Returns a list of paths to include files.

  All paths in the list must end with `".html.eex"`. Anything that does not end
  with `".html.eex"` will be ignored.

  ## Example Return Value

      [
        "/path/to/theme/priv/includes/nav.html.eex",
        "/path/to/theme/priv/includes/sidebar.html.eex",
        "/path/to/theme/priv/includes/footer.html.eex"
      ]
  """
  @callback get_includes() :: [binary()]

  @doc """
  Returns a list of paths to template files.

  Each path in the list must end with one of these: `"/base.html.eex"`,
  `"/list.html.eex"`, `"/page.html.eex"`, and `"/post.html.eex"`. Anything else
  will be ignored.

  ## Example Return Value

      [
        "/path/to/theme/priv/templates/base.html.eex",
        "/path/to/theme/priv/templates/list.html.eex",
        "/path/to/theme/priv/templates/post.html.eex"
      ]
  """
  @callback get_templates() :: [binary()]

  @doc """
  Returns a path to the assets directory.

  All files in the directory pointed by the returned value will be copied to
  the destination assets directory using `File.cp_r/2`.

  This callback may return `false` to indicate that no asset will be copied.
  """
  @callback get_assets() :: binary() | false

  #
  # Theme Consumer Functions
  #

  @doc false
  @spec load(module() | nil) :: Result.t(t())
  def load(module_or_nil)
  def load(nil), do: {:ok, %__MODULE__{}}

  def load(module) do
    name = module.name()
    version = Version.parse!(module.version())

    unless Version.match?(@serum_version, module.serum()) do
      msg =
        "The theme \"#{name}\" is not compatible with " <>
          "the current version of Serum(#{@serum_version}). " <>
          "This theme may not work as intended."

      put_err(:warn, msg)
    end

    result = %__MODULE__{
      module: module,
      name: name,
      description: module.description(),
      author: module.author(),
      legal: module.legal(),
      version: version
    }

    {:ok, result}
  rescue
    exception ->
      ex_name = module_name(exception.__struct__)
      ex_msg = Exception.message(exception)
      mod_name = module_name(module)
      msg = "#{ex_name} while loading theme (module: #{mod_name}): #{ex_msg}"

      {:error, msg}
  end

  @doc false
  @spec get_includes(t()) :: Result.t(%{optional(binary()) => binary()})
  def get_includes(theme)
  def get_includes(%__MODULE__{module: nil}), do: {:ok, %{}}

  def get_includes(%__MODULE__{module: module}) do
    case get_list(module, :get_includes, []) do
      {:ok, paths} ->
        result =
          paths
          |> Enum.filter(&String.ends_with?(&1, ".html.eex"))
          |> Enum.map(&{Path.basename(&1, ".html.eex"), &1})
          |> Map.new()

        {:ok, result}

      {:error, _} = error ->
        error
    end
  end

  @accepted_templates MapSet.new(["base", "list", "page", "post"])

  @doc false
  @spec get_templates(t()) :: Result.t(%{optional(binary()) => binary()})
  def get_templates(theme)
  def get_templates(%__MODULE__{module: nil}), do: {:ok, %{}}

  def get_templates(%__MODULE__{module: module}) do
    case get_list(module, :get_templates, []) do
      {:ok, paths} ->
        result =
          paths
          |> Enum.map(&{Path.basename(&1, ".html.eex"), &1})
          |> Enum.filter(&(elem(&1, 0) in @accepted_templates))
          |> Map.new()

        {:ok, result}

      {:error, _} = error ->
        error
    end
  end

  @spec get_list(atom(), atom(), list()) :: Result.t([binary()])
  defp get_list(module, fun, args) do
    with {:ok, paths} <- call_function(module, fun, args),
         :ok <- check_list_type(paths) do
      {:ok, paths}
    else
      {:error, _} = error ->
        error

      {:bad, x} ->
        callee = "#{module_name(module)}.#{fun}"
        msg = "#{callee}: expected a list of strings, got: #{inspect(x)}"

        {:error, msg}

      {:bad_item, x} ->
        callee = "#{module_name(module)}.#{fun}"
        msg = "#{callee} expected a list of strings, got: #{inspect(x)} in the list"

        {:error, msg}
    end
  end

  @spec check_list_type(term()) :: :ok | {:bad, term()} | {:bad_item, term()}
  defp check_list_type(maybe_list)
  defp check_list_type([]), do: :ok
  defp check_list_type([x | xs]) when is_binary(x), do: check_list_type(xs)
  defp check_list_type([x | _xs]), do: {:bad_item, x}
  defp check_list_type(x), do: {:bad, x}

  @doc false
  @spec get_assets(t()) :: Result.t(binary() | false)
  def get_assets(theme)
  def get_assets(%__MODULE__{module: nil}), do: {:ok, false}

  def get_assets(%__MODULE__{module: module}) do
    case call_function(module, :get_assets, []) do
      {:ok, path} when is_binary(path) ->
        do_get_assets(path)

      {:ok, false} ->
        {:ok, false}

      {:error, _} = error ->
        error

      x ->
        mod_name = module_name(module)
        msg = "#{mod_name}.get_assets: expected a string, got: #{inspect(x)}"

        {:error, msg}
    end
  end

  @spec do_get_assets(binary()) :: Result.t(binary())
  defp do_get_assets(path) do
    case File.stat(path) do
      {:ok, %File.Stat{type: :directory}} -> {:ok, path}
      {:ok, %File.Stat{}} -> {:error, {:enotdir, path, 0}}
      {:error, reason} -> {:error, {reason, path, 0}}
    end
  end

  @spec call_function(atom(), atom(), list()) :: Result.t(term())
  defp call_function(module, fun, args) do
    {:ok, apply(module, fun, args)}
  rescue
    exception ->
      ex_name = module_name(exception.__struct__)
      ex_msg = Exception.message(exception)
      mod_name = module_name(module)
      msg = "#{ex_name} from #{mod_name}.#{fun}: #{ex_msg}"

      {:error, msg}
  end

  @spec module_name(atom()) :: binary()
  defp module_name(module) do
    module |> to_string() |> String.replace_prefix("Elixir.", "")
  end
end


Rename abbreviation to country code

Rename abbreviation to country code

defmodule Shippex.ISO do
  @moduledoc """
  This module contains data and functions for obtaining geographic data in
  compliance with the ISO-3166-2 standard.
  """

  @iso Shippex.json_library().decode!(File.read!(:code.priv_dir(:shippex) ++ '/iso-3166-2.json'))

  @doc """
  Returns all ISO-3166-2 data.
  """
  def data() do
    @iso
  end

  @doc """
  Returns a map of country codes and their full names.

      iex> countries = ISO.countries()
      ...> match? %{"US" => "United States"}, countries
      true
  """
  @spec countries() :: %{String.t() => String.t()}
  def countries() do
    Enum.reduce(@iso, %{}, fn {code, %{"name" => name}}, acc ->
      Map.put(acc, code, name)
    end)
  end

  @doc """
  Returns a map of state codes and full names for the given 2-letter country
  code.

      iex> states = ISO.states("US")
      ...> match? %{"TX" => "Texas", "PR" => "Puerto Rico"}, states
      true
      iex> states = ISO.states("MX")
      ...> match? %{"AGU" => "Aguascalientes"}, states
      true
      iex> ISO.states("Not a country.")
      %{}
  """
  @spec states(String.t()) :: %{String.t() => String.t()}
  def states(country \\ "US") do
    states = @iso[country] || %{}

    case states["divisions"] do
      nil ->
        %{}

      divisions ->
        divisions
        |> Enum.map(fn {code, name} ->
          code = String.replace_prefix(code, "#{country}-", "")
          {code, name}
        end)
        |> Map.new()
    end
  end

  @doc """
  Converts a country's 2-letter code to its full name.

      iex> ISO.country_code_to_name("US")
      "United States"
      iex> ISO.country_code_to_name("TN")
      "Tunisia"
      iex> ISO.country_code_to_name("TX")
      nil
  """
  @spec country_code_to_name(String.t()) :: nil | String.t()
  def country_code_to_name(abbr) when is_bitstring(abbr) do
    countries()[abbr]
  end
end

defmodule Shippex.ISO do
  @moduledoc """
  This module contains data and functions for obtaining geographic data in
  compliance with the ISO-3166-2 standard.
  """

  @iso Shippex.json_library().decode!(File.read!(:code.priv_dir(:shippex) ++ '/iso-3166-2.json'))

  @doc """
  Returns all ISO-3166-2 data.
  """
  def data() do
    @iso
  end

  @doc """
  Returns a map of country codes and their full names.

      iex> countries = ISO.countries()
      ...> match? %{"US" => "United States"}, countries
      true
  """
  @spec countries() :: %{String.t() => String.t()}
  def countries() do
    Enum.reduce(@iso, %{}, fn {code, %{"name" => name}}, acc ->
      Map.put(acc, code, name)
    end)
  end

  @doc """
  Returns a map of state codes and full names for the given 2-letter country
  code.

      iex> states = ISO.states("US")
      ...> match? %{"TX" => "Texas", "PR" => "Puerto Rico"}, states
      true
      iex> states = ISO.states("MX")
      ...> match? %{"AGU" => "Aguascalientes"}, states
      true
      iex> ISO.states("Not a country.")
      %{}
  """
  @spec states(String.t()) :: %{String.t() => String.t()}
  def states(country \\ "US") do
    states = @iso[country] || %{}

    case states["divisions"] do
      nil ->
        %{}

      divisions ->
        divisions
        |> Enum.map(fn {code, name} ->
          code = String.replace_prefix(code, "#{country}-", "")
          {code, name}
        end)
        |> Map.new()
    end
  end

  @doc """
  Converts a country's 2-letter code to its full name.

      iex> ISO.abbreviation_to_country_name("US")
      "United States"
      iex> ISO.abbreviation_to_country_name("TN")
      "Tunisia"
      iex> ISO.abbreviation_to_country_name("TX")
      nil
  """
  @spec abbreviation_to_country_name(String.t()) :: nil | String.t()
  def abbreviation_to_country_name(abbr) when is_bitstring(abbr) do
    countries()[abbr]
  end
end


and a test

and a test

defmodule PedalApp.PhotoTest do
  use PedalApp.ModelCase

  alias PedalApp.Photo

  defp build_upload(path \\ "test/files/test.jpg") do
    %{__struct__: Plug.Upload, content_type: "image/jpg", path: path, filename: Path.basename(path)}
  end

  describe "changeset" do
    test "with valid attributes" do
      waypoint = insert(:waypoint)
      params = %{params_for(:photo) | waypoint_id: waypoint.id, file: build_upload()}
      changeset = Photo.changeset(%Photo{}, params)
      assert changeset.valid?
    end

    test "with invalid attributes" do
      changeset = Photo.changeset(%Photo{}, %{})
      refute changeset.valid?
    end

    test "with no uuid" do
      changeset = Photo.changeset(%Photo{}, %{})
      assert String.length(changeset.changes.uuid) == 36
    end

    test "with existing uuid" do
      photo = build(:photo)
      uuid = photo.uuid
      changeset = Photo.changeset(photo, %{uuid: "invalid"})
      assert get_field(changeset, :uuid) == uuid
    end
  end

  test "insert" do
    waypoint = insert(:waypoint)
    params = %{params_for(:photo) | waypoint_id: waypoint.id, file: build_upload()}
    {status, _} = Repo.insert(Photo.changeset(%Photo{}, params))
    assert status == :ok
  end

  test "delete!" do
    photo = insert(:photo)
    Photo.delete!(photo)
    refute Repo.get(Photo, photo.id)
  end
end

defmodule PedalApp.PhotoTest do
  use PedalApp.ModelCase

  alias PedalApp.Photo

  defp build_upload(path \\ "test/files/test.jpg") do
    %{__struct__: Plug.Upload, content_type: "image/jpg", path: path, filename: Path.basename(path)}
  end

  describe "changeset" do
    test "with valid attributes" do
      waypoint = insert(:waypoint)
      params = %{params_for(:photo) | waypoint_id: waypoint.id, file: build_upload()}
      changeset = Photo.changeset(%Photo{}, params)
      assert changeset.valid?
    end

    test "with invalid attributes" do
      changeset = Photo.changeset(%Photo{}, %{})
      refute changeset.valid?
    end

    test "with no uuid" do
      changeset = Photo.changeset(%Photo{}, %{})
      assert String.length(changeset.changes.uuid) == 36
    end

    test "with existing uuid" do
      photo = build(:photo)
      uuid = photo.uuid
      changeset = Photo.changeset(photo, %{uuid: "invalid"})
      assert get_field(changeset, :uuid) == uuid
    end
  end

  test "insert" do
    waypoint = insert(:waypoint)
    params = %{params_for(:photo) | waypoint_id: waypoint.id, file: build_upload()}
    {status, _} = Repo.insert(Photo.changeset(%Photo{}, params))
    assert status == :ok
  end
end


Search group by number

Search group by number

defmodule APITest do
  use ExUnit.Case
  use Maru.Test, for: GnServer.API

  setup_all do
    {:ok, hello: Poison.encode!(%{"I am": :genenetwork})}
  end

  test "/hey",%{hello: state} do
    # IO.inspect state
    %Plug.Conn{resp_body: value} = conn(:get, "/hey") |> make_response
    assert state == value
  end

  test "/", %{hello: state} do
    %Plug.Conn{resp_body: value} = conn(:get, "/") |> make_response
    assert state == value
  end

  test "/species" do
    %Plug.Conn{resp_body: value} = conn(:get, "/species") |> make_response
    assert Poison.decode!(value) == [[1,"mouse","Mus musculus"],[4,"human","Homo sapiens"]]
  end

  test "/groups/mouse" do
    %Plug.Conn{resp_body: value} = conn(:get, "/groups/mouse") |> make_response
    assert Poison.decode!(value) == [[1,"BXD","BXD"]]
  end

  test "/groups/1" do
    %Plug.Conn{resp_body: value} = conn(:get, "/groups/1") |> make_response
    assert Poison.decode!(value) == [[1,"BXD","BXD"]]
  end

  test "/group/'name'.json" do
    %Plug.Conn{resp_body: value} = conn(:get, "/group/BXD.json") |> make_response

    assert Poison.decode!(value) == %{"genetic_type" => "riset",
                                      "group" => "BXD", "group_id" => 1,
                                      "mapping_method_id" => 1,
                                      "species" => "mouse", "species_id" => 1}
  end

  test "/group/1.json" do
    %Plug.Conn{resp_body: value} = conn(:get, "/group/1.json") |> make_response

    assert Poison.decode!(value) == %{"genetic_type" => "riset",
                                      "group" => "BXD", "group_id" => 1,
                                      "mapping_method_id" => 1,
                                      "species" => "mouse", "species_id" => 1}
  end

  test "/datasets/BXD" do
    %Plug.Conn{resp_body: value} = conn(:get, "/datasets/BXD") |> make_response
    assert Poison.decode!(value) == [[1,"mouse","Mus musculus"],[4,"human","Homo sapiens"]]
  end

  test "/dataset/'name'.json" do
    %Plug.Conn{resp_body: value} = conn(:get, "/dataset/HC_M2_0606_P.json") |> make_response

    assert Poison.decode!(value) == %{
                                      "group" => "BXD", "group_id" => 1,
                                      "species" => "mouse", "species_id" => 1}
  end

end

defmodule APITest do
  use ExUnit.Case
  use Maru.Test, for: GnServer.API

  setup_all do
    {:ok, hello: Poison.encode!(%{"I am": :genenetwork})}
  end

  test "/hey",%{hello: state} do
    # IO.inspect state
    %Plug.Conn{resp_body: value} = conn(:get, "/hey") |> make_response
    assert state == value
  end

  test "/", %{hello: state} do
    %Plug.Conn{resp_body: value} = conn(:get, "/") |> make_response
    assert state == value
  end

  test "/species" do
    %Plug.Conn{resp_body: value} = conn(:get, "/species") |> make_response
    assert Poison.decode!(value) == [[1,"mouse","Mus musculus"],[4,"human","Homo sapiens"]]
  end

  test "/groups/mouse" do
    %Plug.Conn{resp_body: value} = conn(:get, "/groups/mouse") |> make_response
    assert Poison.decode!(value) == [[1,"BXD","BXD"]]
  end

  test "/groups/1" do
    %Plug.Conn{resp_body: value} = conn(:get, "/groups/1") |> make_response
    assert Poison.decode!(value) == [[1,"BXD","BXD"]]
  end

  test "/group/'name'.json" do
    %Plug.Conn{resp_body: value} = conn(:get, "/group/BXD.json") |> make_response

    assert Poison.decode!(value) == %{"genetic_type" => "riset",
                                      "group" => "BXD", "group_id" => 1,
                                      "mapping_method_id" => 1,
                                      "species" => "mouse", "species_id" => 1}
  end

  test "/group/1.json" do
    %Plug.Conn{resp_body: value} = conn(:get, "/group/1.json") |> make_response

    assert Poison.decode!(value) == %{"genetic_type" => "riset",
                                      "group" => "BXD", "group_id" => 1,
                                      "mapping_method_id" => 1,
                                      "species" => "mouse", "species_id" => 1}
  end

  test "/datasets/'group'" do
    %Plug.Conn{resp_body: value} = conn(:get, "/datasets/mouse") |> make_response
    assert Poison.decode!(value) == [[1,"mouse","Mus musculus"],[4,"human","Homo sapiens"]]
    %Plug.Conn{resp_body: value} = conn(:get, "/datasets/BXD") |> make_response
    assert Poison.decode!(value) == [[1,"mouse","Mus musculus"],[4,"human","Homo sapiens"]]
  end

  test "/dataset/'name'.json" do
    %Plug.Conn{resp_body: value} = conn(:get, "/dataset/HC_M2_0606_P.json") |> make_response

    assert Poison.decode!(value) == %{
                                      "group" => "BXD", "group_id" => 1,
                                      "species" => "mouse", "species_id" => 1}
  end

end


typespec: fix type for tag1 family

typespec: fix type for tag1 family

defmodule Rockside.Doc do

  @type tagname  :: atom
  @type attrs :: [{atom, String.t}]
  @type content :: String.t | list
  @type outlist  :: [any(), ...]
  @type out_tag1 :: [[String.t] | String.t] | String.t

  @spec tag(tagname, attrs, content)  :: outlist
  @spec tag(tagname)                  :: outlist

  def tag(tag, attrs\\[], inner) do
    if attrs == [] do
      ["<#{tag}>", inner, "</#{tag}>"]
    else
      ["<#{tag} ", htmlize_attrs(attrs), ">", inner, "</#{tag}>"]
    end
  end

  def tag(tag), do: tag(tag, [], [])

  @spec tag1(tagname, attrs) :: out_tag1
  def tag1(tag, attrs\\[]) do
    if attrs == [] do
      "<#{tag} />"
    else
      ["<#{tag} ", htmlize_attrs(attrs), " />"]
    end
  end

  @spec flush(outlist | String.t) :: String.t
  def flush(chunks) when is_list(chunks) do
    chunks |> List.flatten |> Enum.join
  end
  def flush(chunk) when is_binary(chunk), do: chunk
  # ^ needed only for plug-free tests, because patched Plug
  # accepts iolist as a resp body

  @spec html(attrs, list) :: outlist
  def html(attrs\\[], inner) do
    [ "<!DOCTYPE html>" | tag(:html, attrs, inner) ]
  end

  ~w[head title body]
    |> Enum.each fn name ->
      sym = :"#{name}"
      @spec unquote(sym)(attrs, content) :: outlist
      @spec unquote(sym)() :: outlist
      def unquote(sym)(attrs\\[], inner), do: tag(unquote(sym), attrs, inner)
      def unquote(sym)(), do: tag(unquote(sym))
    end
  ~w[meta link]
    |> Enum.each fn name ->
      sym = :"#{name}"
      @spec unquote(sym)(attrs) :: out_tag1
      def unquote(sym)(attrs\\[]), do: tag1(unquote(sym), attrs)
    end

  def css(path), do: link([rel: "stylesheet", type: "text/css", href: path])

  @spec htmlize_attrs(attrs) :: [String.t]
  defp htmlize_attrs(attrs) do
    Enum.map(attrs, fn {k,v} ->
      k = k |> to_string |> String.replace("_", "-")
      ~s/#{k}="#{v}"/
    end)
      |> Enum.intersperse(" ")
  end
end

defmodule Rockside.Doc do

  @type tagname  :: atom
  @type attrs :: [{atom, String.t}]
  @type content :: String.t | list
  @type outlist  :: [any(), ...]

  @spec tag(tagname, attrs, content)  :: outlist
  @spec tag(tagname)                  :: outlist

  def tag(tag, attrs\\[], inner) do
    if attrs == [] do
      ["<#{tag}>", inner, "</#{tag}>"]
    else
      ["<#{tag} ", htmlize_attrs(attrs), ">", inner, "</#{tag}>"]
    end
  end

  def tag(tag), do: tag(tag, [], [])

  @spec tag1(tagname, attrs) :: outlist | String.t
  def tag1(tag, attrs\\[]) do
    if attrs == [] do
      "<#{tag} />"
    else
      ["<#{tag} ", htmlize_attrs(attrs), " />"]
    end
  end

  @spec flush(outlist | String.t) :: String.t
  def flush(chunks) when is_list(chunks) do
    chunks |> List.flatten |> Enum.join
  end
  def flush(chunk) when is_binary(chunk), do: chunk
  # ^ needed only for plug-free tests, because patched Plug
  # accepts iolist as a resp body

  @spec html(attrs, list) :: outlist
  def html(attrs\\[], inner) do
    [ "<!DOCTYPE html>" | tag(:html, attrs, inner) ]
  end

  ~w[head title body]
    |> Enum.each fn name ->
      sym = :"#{name}"
      @spec unquote(sym)(attrs, content) :: outlist
      @spec unquote(sym)() :: outlist
      def unquote(sym)(attrs\\[], inner), do: tag(unquote(sym), attrs, inner)
      def unquote(sym)(), do: tag(unquote(sym))
    end
  ~w[meta link]
    |> Enum.each fn name ->
      sym = :"#{name}"
      @spec unquote(sym)(attrs) :: outlist | String.t
      def unquote(sym)(attrs\\[]), do: tag1(unquote(sym), attrs)
    end

  def css(path), do: link([rel: "stylesheet", type: "text/css", href: path])

  @spec htmlize_attrs(attrs) :: [String.t]
  defp htmlize_attrs(attrs) do
    Enum.map(attrs, fn {k,v} ->
      k = k |> to_string |> String.replace("_", "-")
      ~s/#{k}="#{v}"/
    end)
      |> Enum.intersperse(" ")
  end
end


Remove unused alias

Remove unused alias

defmodule Credo.Check.Design.TagHelper do
  alias Credo.Check.CodeHelper
  alias Credo.SourceFile

  @doc_attribute_names [:doc, :moduledoc, :shortdoc]

  def tags(source_file, tag_name, include_doc?) do
    tags_from_module_attributes(source_file, tag_name, include_doc?) ++ tags_from_comments(source_file, tag_name)
  end

  defp tags_from_module_attributes(source_file, tag_name, true) do
    regex = Regex.compile!("\\s*#{tag_name}:?\\s*.+", "i")

    Credo.Code.prewalk(source_file, &traverse(&1, &2, regex))
  end
  defp tags_from_module_attributes(_source_file, _tag_name, false) do
    []
  end

  defp tags_from_comments(source_file, tag_name) do
    regex = Regex.compile!("(\\A|[^\\?])#\\s*#{tag_name}:?\\s*.+", "i")
    source = SourceFile.source(source_file)

    if source =~ regex do
      source
      |> CodeHelper.clean_charlists_strings_and_sigils
      |> String.split("\n")
      |> Enum.with_index
      |> Enum.map(&find_tag_in_line(&1, regex))
      |> Enum.filter(&tags?/1)
    else
      []
    end
  end

  defp traverse({:@, _, [{name, meta, [string]} | _]} = ast, issues, regex) when name in @doc_attribute_names do
    if string =~ regex do
      {nil, issues ++ [{meta[:line], string, string}]}
    else
      {ast, issues}
    end
  end
  defp traverse(ast, issues, _regex) do
    {ast, issues}
  end

  defp find_tag_in_line({line, index}, regex) do
    tag_list =
      regex
      |> Regex.run(line)
      |> List.wrap
      |> Enum.map(&String.strip/1)

    {index + 1, line, List.first(tag_list)}
  end

  defp tags?({_line_no, _line, nil}), do: false
  defp tags?({_line_no, _line, _tag}), do: true
end

defmodule Credo.Check.Design.TagHelper do
  alias Credo.Check.CodeHelper
  alias Credo.IssueMeta
  alias Credo.SourceFile

  @doc_attribute_names [:doc, :moduledoc, :shortdoc]

  def tags(source_file, tag_name, include_doc?) do
    tags_from_module_attributes(source_file, tag_name, include_doc?) ++ tags_from_comments(source_file, tag_name)
  end

  defp tags_from_module_attributes(source_file, tag_name, true) do
    regex = Regex.compile!("\\s*#{tag_name}:?\\s*.+", "i")

    Credo.Code.prewalk(source_file, &traverse(&1, &2, regex))
  end
  defp tags_from_module_attributes(_source_file, _tag_name, false) do
    []
  end

  defp tags_from_comments(source_file, tag_name) do
    regex = Regex.compile!("(\\A|[^\\?])#\\s*#{tag_name}:?\\s*.+", "i")
    source = SourceFile.source(source_file)

    if source =~ regex do
      source
      |> CodeHelper.clean_charlists_strings_and_sigils
      |> String.split("\n")
      |> Enum.with_index
      |> Enum.map(&find_tag_in_line(&1, regex))
      |> Enum.filter(&tags?/1)
    else
      []
    end
  end

  defp traverse({:@, _, [{name, meta, [string]} | _]} = ast, issues, regex) when name in @doc_attribute_names do
    if string =~ regex do
      {nil, issues ++ [{meta[:line], string, string}]}
    else
      {ast, issues}
    end
  end
  defp traverse(ast, issues, _regex) do
    {ast, issues}
  end

  defp find_tag_in_line({line, index}, regex) do
    tag_list =
      regex
      |> Regex.run(line)
      |> List.wrap
      |> Enum.map(&String.strip/1)

    {index + 1, line, List.first(tag_list)}
  end

  defp tags?({_line_no, _line, nil}), do: false
  defp tags?({_line_no, _line, _tag}), do: true
end