defmodule Moar.Opts do
# @related [test](/test/opts_test.exs)
@moduledoc """
Extracts keys and values from enumerables, especially from function options.
There are three main functions, each of which takes an opts enumerable as input. `get/3` extracts
one value from the opts with an optional default value. `pop/3` is like `get/3` but removes the opt from opts
and returns the opt and the remaining opts as a tuple. `take/2` extracts multiple values from the opts
with optional default values for some or all keys.
`get/3`, `pop/3`, and `take/2` differ from their `Map` and `Keyword` counterparts in the following ways:
* `get/3`, `pop/3`, and `take/2` accept any enumerable, including maps, keyword lists, and mixed lists like
`[:a, :b, c: 3, d: 4]`.
* `get/3`, `pop/3`, and `take/2` will fall back to the default value if the given key's value is blank
as defined by `Moar.Term.blank?/1` (`nil`, empty strings, strings made up only of whitespace,
empty lists, and empty maps), and will default valueless keys (e.g., `:a` in `[:a, b: 2]`) to `true` unless a
different default is specified. The corresponding `Map` and `Keyword` functions only fall back to
the default value if the value is exactly `nil`, and `Keyword` functions don't support valueless keys.
* `get/3`, `pop/3`, and `take/2` allow default values to be specified.
* `take/2` will return the value for a requested key even if the key is not in the input enumerable.
Example using `get/2` and `get/3`:
```elixir
def build_url(path, opts \\\\ []) do
%URI{
path: path,
host: Moar.Opts.get(opts, :host, "localhost"),
port: Moar.Opts.get(opts, :port),
scheme: "https"
} |> URI.to_string()
end
```
Examples using `take/2`:
```elixir
# example using pattern matching
def build_url(path, opts \\ []) do
%{host: h, port: p} = Moar.Opts.take(opts, [:port, host: "localhost"])
%URI{path: path, host: h, port: p, scheme: "https"} |> URI.to_string()
end
# example rebinding `opts` to the parsed opts
def build_url(path, opts \\ []) do
opts = Moar.Opts.take(opts, [:port, host: "localhost"])
%URI{path: path, host: opts.host, port: opts.port, scheme: "https"} |> URI.to_string()
end
```
"""
@doc """
Deletes an opt given a key, a {key, value}, or a function that accepts opts and a map, list, or term.
```elixir
iex> Moar.Opts.delete(%{a: 1, b: 2}, :a)
%{b: 2}
iex> Moar.Opts.delete([a: 1, b: 2], :a)
[b: 2]
iex> Moar.Opts.delete([:a, b: 2], :a)
[b: 2]
iex> Moar.Opts.delete([a: 1, b: 2], :a, 1)
[b: 2]
iex> Moar.Opts.delete([a: 1, b: 2], :a, 99)
[a: 1, b: 2]
iex> Moar.Opts.delete([:a, b: 2], :a, 1)
[:a, b: 2]
iex> Moar.Opts.delete([:trim, :downcase, :reverse], fn k -> k == :downcase end)
[:trim, :reverse]
iex> Moar.Opts.delete([a: 1, b: 2, c: 3, d: 4], fn {_k, v} -> Integer.mod(v, 2) == 0 end)
[a: 1, c: 3]
```
"""
@spec delete(map() | list(), any()) :: map() | list()
@spec delete(map() | list(), any(), any()) :: map() | list()
def delete(input, fun) when is_function(fun) and is_map(input) do
Enum.reduce(input, %{}, fn
{k, v}, acc -> if fun.({k, v}), do: acc, else: Map.put(acc, k, v)
end)
end
def delete(input, fun) when is_function(fun) and is_list(input) do
List.foldr(input, [], fn
{k, v}, acc -> if fun.({k, v}), do: acc, else: [{k, v} | acc]
k, acc -> if fun.(k), do: acc, else: [k | acc]
end)
end
def delete(input, fun) when is_function(fun) do
if fun.(input),
do: input,
else: nil
end
def delete(input, key) do
delete(input, fn
{k, _v} -> k == key
k -> k == key
end)
end
def delete(input, key, value) do
delete(input, fn
{k, v} -> k == key && v == value
_ -> false
end)
end
@doc """
Get the value of `key` from `input`, falling back to optional `default` if the key does not exist,
or if its value is blank (via `Moar.Term.blank?/1`).
```elixir
iex> [a: 1, b: 2] |> Moar.Opts.get(:a)
1
iex> [:a, b: 2] |> Moar.Opts.get(:a)
true
iex> [a: 1, b: 2] |> Moar.Opts.get(:c)
nil
iex> [a: 1, b: 2, c: ""] |> Moar.Opts.get(:c)
nil
iex> [a: 1, b: 2, c: %{}] |> Moar.Opts.get(:c)
nil
iex> [a: 1, b: 2, c: " "] |> Moar.Opts.get(:c, 300)
300
```
"""
@spec get(Enum.t(), binary() | atom(), any()) :: any()
def get(input, key, default \\ nil) do
Enum.find_value(input, fn
{k, v} -> k == key && Moar.Term.presence(v)
k -> k == key && Moar.Term.presence(default, true)
end) || default
end
@doc """
Removes an opt from the opts (via `get/3`), returning `{opt, remaining_opts}`.
```elixir
iex> [a: 1, b: 2] |> Moar.Opts.pop(:a)
{1, [b: 2]}
iex> [:a, b: 2] |> Moar.Opts.pop(:a)
{true, [b: 2]}
```
"""
@spec pop(Enum.t(), binary() | atom(), any()) :: {any(), Enum.t()}
def pop(input, key, default \\ nil)
def pop(input, key, default) when is_list(input) do
value = get(input, key, default)
list =
cond do
key in input -> List.delete(input, key)
Keyword.has_key?(input, key) -> Keyword.delete(input, key)
:else -> input
end
{value, list}
end
def pop(input, key, default) when is_map(input),
do: {get(input, key, default), Map.delete(input, key)}
@doc """
Replace an opt.
```elixir
iex> Moar.Opts.replace(%{a: 1, b: 2}, {:a, 1}, {:a, 100})
%{a: 100, b: 2}
iex> Moar.Opts.replace([a: 1, b: 2], {:a, 1}, {:a, 100})
[a: 100, b: 2]
iex> Moar.Opts.replace([:a, b: 2], :a, :aa)
[:aa, b: 2]
```
"""
@spec replace(map(), {any(), any()}, {any(), any()}) :: map()
@spec replace(list(), {any(), any()} | any(), any()) :: list()
def replace(opts, {key, value}, {replacement_key, replacement_value}) when is_map(opts) do
Enum.reduce(opts, %{}, fn {k, v}, acc ->
if key == k && value == v,
do: Map.put(acc, replacement_key, replacement_value),
else: Map.put(acc, k, v)
end)
end
def replace(opts, {key, value}, replacement) when is_list(opts) do
List.foldr(opts, [], fn {k, v}, acc ->
if key == k && value == v,
do: [replacement | acc],
else: [{k, v} | acc]
end)
end
def replace(opts, key, replacement) when is_list(opts) do
List.foldr(opts, [], fn k, acc ->
if key == k,
do: [replacement | acc],
else: [k | acc]
end)
end
@doc """
Get the value each key in `keys` from `input`, falling back to optional default values for keys that
do not exist, or for values that are blank (via `Moar.Term.blank?/1`).
If `key` does not exist in `keys`, returns `nil`, or returns the default value if provided.
`keys` is a list of keys (e.g., `[:a, :b]`),
a keyword list of keys and default values (e.g., `[a: 1, b: 2]`),
or a hybrid list/keyword list (e.g., `[:a, b: 2]`)
```elixir
iex> [a: 1, b: 2] |> Moar.Opts.take([:a, :c])
%{a: 1, c: nil}
iex> [:a, b: 2] |> Moar.Opts.take([:a, :c])
%{a: true, c: nil}
iex> [a: 1, b: 2] |> Moar.Opts.take([:a, b: 0, c: 3])
%{a: 1, b: 2, c: 3}
```
"""
@spec take(Enum.t(), list()) :: map()
def take(input, keys) do
Enum.reduce(keys, %{}, fn
{key, default}, acc -> Map.put(acc, key, get(input, key, default))
key, acc -> Map.put(acc, key, get(input, key))
end)
end
end