Packages

plymio_option: Utility Functions for Managing (Keyword) Options

Current section

Files

Jump to
plymio_option lib option utility.ex
Raw

lib/option/utility.ex

defmodule Plymio.Option.Utility do
@moduledoc ~S"""
Utility Function for Managing (Keyword) Options ("opts")
## Documentation Terms
In the documentation there are terms, usually in *italics*, used to mean the same thing (e.g. *opts*).
### opts
*opts* is a `Keyword`.
### derivable opts
*derivable opts* is either a `Keyword` or `Map` with `Atom` keys (from which the *opts* can be *derived* simply using `Map.to_list/1`).
### *key*
A *key* is an `Atom`.
### key list
A *key list* is a list of *key*s.
### key spec
A *key spec* is usually a *key list*.
Alternatively a `Map` with `Atom` keys or a `Keyword` can be given and the (unique) keys will be used.
### key alias dict
A *key alias dict* is usually a `Map` with `Atom` keys and values used for canonicalising keys (e.g. as the 2nd argument to `opts_canonical_keys/2`).
Alternatively a `Keyword` with `Atom` values can be given and will be converted on the fly.
### key dict
A *key alias dict* is usually a `Map` with `Atom` keys.
Alternatively a `Keyword` with `Atom` values can be given and will be converted on the fly.
### tuple predicate
A *tuple predicate* is an arity one function that when passed a `{key,value}` tuple returns `true` or `false`.
## Return Values
Many functions support an API that returns either `{:ok, result}` or {`:error, error}` where `error` will be an `Exception`.
The default action for bang function when fielding `{:error, error}` is to raise the `error`.
In many cases the `error` will be a `KeyError` where its `key` field is set to the key, or list of keys, that is missing, unknown, etc.
"""
@type key :: atom
@type keys :: key | [key]
@type alias_key :: key
@type alias_keys :: keys
@type alias_value :: nil | alias_keys
@type aliases_kvs :: [{alias_key, alias_value}]
@type aliases_tuples :: [{alias_key, alias_key}]
@type aliases_dict :: %{optional(alias_key) => alias_key}
@type defaults_map :: %{optional(alias_key) => any}
@type opts :: Keyword.t
@type dict :: %{optional(alias_key) => any}
@type error :: struct
defdelegate opts_take_keys(arg0,arg1), to: Keyword, as: :take
defdelegate opts_drop_keys(arg0,arg1), to: Keyword, as: :drop
defp normalise_key_spec(value)
defp normalise_key_spec(value) when is_list(value) do
cond do
Keyword.keyword?(value) -> {:ok, value |> Keyword.keys |> Enum.uniq}
true ->
value
|> Enum.reject(&is_atom/1)
|> case do
[] -> {:ok, value |> Enum.uniq}
not_atom_keys -> {:error, %KeyError{key: not_atom_keys, term: value}}
end
end
end
defp normalise_key_spec(value) when is_map(value) do
value |> Map.keys |> normalise_key_spec
end
defp normalise_key_spec(value) do
new_error_result(m: "expected enum", v: value)
end
@spec validate_key_list(any) :: {:ok, keys} | {:error, error}
defp validate_key_list(keys)
defp validate_key_list(keys) when is_list(keys) do
keys
|> Enum.reject(&is_atom/1)
|> case do
[] -> {:ok, keys}
not_atoms -> not_atoms |> new_key_error_result(keys)
end
end
defp validate_key_list(keys) do
new_error_result(m: "expected valid key list", v: keys)
end
defp normalise_key_list(keys) do
keys |> validate_key_list
end
@spec validate_key_alias_dict(any) :: {:ok, aliases_dict} | {:error, error}
defp validate_key_alias_dict(dict)
defp validate_key_alias_dict(dict) when is_map(dict) do
with true <- dict |> Map.keys |> Enum.all?(&is_atom/1),
true <- dict |> Map.values |> Enum.all?(&is_atom/1) do
{:ok, dict}
else
false -> new_error_result(m: "expected valid key alias dictionary", v: dict)
end
end
@spec normalise_key_alias_dict(any) :: {:ok, aliases_dict} | {:error, error}
defp normalise_key_alias_dict(dict)
defp normalise_key_alias_dict(dict) when is_map(dict) do
dict |> validate_key_alias_dict
end
defp normalise_key_alias_dict(dict) when is_list(dict) do
cond do
Keyword.keyword?(dict) -> dict |> Enum.into(%{}) |> validate_key_alias_dict
true ->
new_error_result(m: "expected valid alias dictionary", v: dict)
end
end
defp normalise_key_alias_dict(dict) do
new_error_result(m: "expected valid alias dictionary", v: dict)
end
@spec validate_key_dict(any) :: {:ok, aliases_dict} | {:error, error}
defp validate_key_dict(dict)
defp validate_key_dict(dict) when is_map(dict) do
with true <- dict |> Map.keys |> Enum.all?(&is_atom/1) do
{:ok, dict}
else
false -> new_error_result(m: "expected valid key dictionary", v: dict)
end
end
@spec normalise_key_dict(any) :: {:ok, aliases_dict} | {:error, error}
defp normalise_key_dict(dict)
defp normalise_key_dict(dict) when is_map(dict) do
dict |> validate_key_dict
end
defp normalise_key_dict(dict) when is_list(dict) do
cond do
Keyword.keyword?(dict) -> dict |> Enum.into(%{})
true ->
new_error_result(m: "expected valid key dictionary", v: dict)
end
end
defp normalise_key_dict(dict) do
new_error_result(m: "expected valid key dictionary", v: dict)
end
@doc false
defp opts_index_normalise(opts, index)
defp opts_index_normalise(opts, index)
when is_list(opts) and is_integer(index) and index >= 0 do
{:ok, index}
end
defp opts_index_normalise(opts, index)
when is_list(opts) and is_integer(index) and index < 0 do
{:ok, length(opts) + index}
end
defp opts_index_normalise(opts, _index) when not is_list(opts) do
new_error_result(m: "opts invalid", v: opts)
end
defp opts_index_normalise(_opts, index) when not is_integer(index) do
new_error_result(m: "index invalid", v: index)
end
defp opts_index_validate(opts, index)
when is_list(opts) and is_integer(index) do
with {:ok, index} <- opts |> opts_index_normalise(index) do
index_max = length(opts) - 1
case index do
x when x >= 0 -> x
x -> index_max + x + 1
end
|> fn
ndx when ndx < 0 -> new_error_result(m: :index_too_small, v: ndx)
ndx when ndx > index_max -> new_error_result(m: :index_too_large, v: ndx)
ndx -> {:ok, ndx}
end.()
else
{:error, _} = result -> result
end
end
defp opts_index_validate(opts, _index) when not is_list(opts) do
new_error_result(m: "opts invalid", v: opts)
end
defp opts_index_validate(_opts, index) when not is_integer(index) do
new_error_result(m: "index invalid", v: index)
end
defp opts_indices_validate(opts, indices)
defp opts_indices_validate(opts, nil) do
{:ok, opts |> Enum.with_index |> Enum.map(&(elem(&1,1)))}
end
defp opts_indices_validate(opts, indices) when is_list(opts) do
indices
|> List.wrap
|> Enum.reduce({[],[]},
fn index, {valid_indices,invalid_indices} ->
case opts |> opts_index_validate(index) do
{:ok, index} -> {[index | valid_indices], invalid_indices}
{:error, _} -> {valid_indices, [index | invalid_indices]}
end
end)
|> case do
{valid_indices, []} -> {:ok, valid_indices |> Enum.reverse}
{_valid_indices, invalid_indices} ->
case invalid_indices |> length do
1 -> new_error_result(m: "index invalid", v: invalid_indices |> hd)
_ -> new_error_result(m: "indices invalid", v: invalid_indices |> Enum.reverse)
end
end
end
@doc ~S"""
`opts_normalise/` expects a *derivable opts* and returns `{:ok, opts}`.
Any other argument causes `{:error, error}` to be returned.
## Examples
iex> [] |> opts_normalise
{:ok, []}
iex> %{a: 1, b: 2, c: 3} |> opts_normalise
{:ok, [a: 1, b: 2, c: 3]}
iex> %{"a" => 1, :b => 2, :c => 3} |> opts_normalise
{:error, %KeyError{key: "a", term: %{:b => 2, :c => 3, "a" => 1}}}
iex> 42 |> opts_normalise
{:error, %ArgumentError{message: "expected valid derivable opts; got: 42"}}
iex> [a: nil, b: [:b1], c: [:c1, :c2, :c3]] |> opts_normalise
{:ok, [a: nil, b: [:b1], c: [:c1, :c2, :c3]]}
"""
@spec opts_normalise(any) :: {:ok, opts} | {:error, error}
def opts_normalise(value) do
cond do
Keyword.keyword?(value) -> {:ok, value}
is_map(value) ->
value
|> Map.to_list
|> fn tuples ->
tuples
|> Keyword.keyword?
|> case do
true -> {:ok, tuples}
_ ->
tuples
|> Keyword.keys
|> Enum.reject(&is_atom/1)
|> new_key_error_result(value)
end
end.()
true -> new_error_result(m: "expected valid derivable opts", v: value)
end
end
@doc ~S"""
`opts_normalise!/1` calls `opts_normalise/1` and if the result is `{:ok, opts}` returns `opts`.
## Examples
iex> [] |> opts_normalise!
[]
iex> %{a: 1, b: 2, c: 3} |> opts_normalise!
[a: 1, b: 2, c: 3]
iex> %{"a" => 1, :b => 2, :c => 3} |> opts_normalise!
** (KeyError) key "a" not found in: %{:b => 2, :c => 3, "a" => 1}
iex> 42 |> opts_normalise!
** (ArgumentError) expected valid derivable opts; got: 42
iex> [a: nil, b: [:b1], c: [:c1, :c2, :c3]] |> opts_normalise!
[a: nil, b: [:b1], c: [:c1, :c2, :c3]]
"""
@spec opts_normalise!(any) :: opts | no_return
def opts_normalise!(opts) do
case opts |> opts_normalise do
{:ok, opts} -> opts
{:error, error} -> raise error
end
end
@doc ~S"""
`opts_normalise_map/` expects a *derivable opts* as argument.
If the argument is a `Map`, with `Atom` keys, it returns `{:ok, argument}` directly.
If the argument is a `Keyword`, with `Atom` keys, it returns `{:ok, argument |> Enum.into(%{})}`.
Any other argument causes `{:error, error}` to be returned.
## Examples
iex> [] |> opts_normalise_map
{:ok, %{}}
iex> [a: nil, b: [:b1], c: [:c1, :c2, :c3]] |> opts_normalise_map
{:ok, %{a: nil, b: [:b1], c: [:c1, :c2, :c3]}}
iex> %{a: 1, b: 2, c: 3} |> opts_normalise_map
{:ok, %{a: 1, b: 2, c: 3}}
iex> %{"a" => 1, :b => 2, :c => 3} |> opts_normalise_map
{:error, %KeyError{key: ["a"], term: %{:b => 2, :c => 3, "a" => 1}}}
iex> 42 |> opts_normalise_map
{:error, %ArgumentError{message: "expected valid derivable opts; got: 42"}}
"""
@spec opts_normalise_map(any) :: {:ok, opts} | {:error, error}
def opts_normalise_map(value) do
cond do
Keyword.keyword?(value) -> {:ok, value |> Enum.into(%{})}
is_map(value) ->
with {:ok, _} <- value |> normalise_key_spec do
{:ok, value}
else
{:error, %KeyError{} = error} -> {:error, struct!(error, term: value)}
{:error, _} = result -> result
end
true -> new_error_result(m: "expected valid derivable opts", v: value)
end
end
@doc ~S"""
`opts_normalise_map!/1` call `opts_normalise_map/1` and if the result is `{:ok, map}` returns `map`.
## Examples
iex> [] |> opts_normalise_map!
%{}
iex> [a: 1, b: 2, c: 3] |> opts_normalise_map!
%{a: 1, b: 2, c: 3}
iex> %{a: 1, b: 2, c: 3} |> opts_normalise_map!
%{a: 1, b: 2, c: 3}
iex> %{"a" => 1, :b => 2, :c => 3} |> opts_normalise_map!
** (KeyError) key ["a"] not found in: %{:b => 2, :c => 3, "a" => 1}
iex> 42 |> opts_normalise_map!
** (ArgumentError) expected valid derivable opts; got: 42
"""
@spec opts_normalise_map!(any) :: opts | no_return
def opts_normalise_map!(opts) do
case opts |> opts_normalise_map do
{:ok, map} -> map
{:error, %KeyError{} = error} -> raise struct!(error, term: opts)
{:error, error} -> raise error
end
end
@doc ~S"""
`opts_validate/1` returns `{:ok, opts}` if the argument is an *opts*.
Any other argument causes `{:error, error}` to be returned.
## Examples
iex> [] |> opts_validate
{:ok, []}
iex> %{a: 1, b: 2, c: 3} |> opts_validate
{:error, %ArgumentError{message: "validate opts failed; got: %{a: 1, b: 2, c: 3}"}}
iex> %{"a" => 1, :b => 2, :c => 3} |> opts_validate
{:error, %ArgumentError{message: "validate opts failed; got: %{:b => 2, :c => 3, \"a\" => 1}"}}
iex> 42 |> opts_validate
{:error, %ArgumentError{message: "validate opts failed; got: 42"}}
iex> [a: nil, b: [:b1], c: [:c1, :c2, :c3]] |> opts_validate
{:ok, [a: nil, b: [:b1], c: [:c1, :c2, :c3]]}
"""
@spec opts_validate(any) :: {:ok, opts} | {:error, error}
def opts_validate(value) do
case Keyword.keyword?(value) do
true -> {:ok, value}
_ -> new_error_result(m: "validate opts failed", v: value)
end
end
@doc ~S"""
`opts_validate!/1` calls `opts_validate/1` and, if the result is `{:ok, opts}`, returns `opts`.
## Examples
iex> [] |> opts_validate!
[]
iex> %{a: 1, b: 2, c: 3} |> opts_validate!
** (ArgumentError) validate opts failed; got: %{a: 1, b: 2, c: 3}
iex> %{"a" => 1, :b => 2, :c => 3} |> opts_validate!
** (ArgumentError) validate opts failed; got: %{:b => 2, :c => 3, "a" => 1}
iex> 42 |> opts_validate!
** (ArgumentError) validate opts failed; got: 42
iex> [a: nil, b: [:b1], c: [:c1, :c2, :c3]] |> opts_validate!
[a: nil, b: [:b1], c: [:c1, :c2, :c3]]
"""
@spec opts_validate!(opts) :: opts | no_return
def opts_validate!(opts) do
case opts |> opts_validate do
{:ok, opts} -> opts
{:error, error} -> raise error
end
end
@doc ~S"""
`opts_create_aliases_tuples/1` takes an *opts* where the keys are the canonical key names, and their values are zero (nil), one or more aliases for the canonical key.
A `Keyword` is returned where each key is an alias and its value the canonical key.
The canonical key also has an entry for itself with the same value.
## Examples
iex> [a: nil, b: [:b1], c: [:c1, :c2, :c3]] |> opts_create_aliases_tuples
[a: :a, b: :b, b1: :b, c: :c, c1: :c, c2: :c, c3: :c]
"""
@spec opts_create_aliases_tuples(aliases_kvs) :: aliases_tuples
def opts_create_aliases_tuples(aliases) do
aliases
|> Enum.map(fn
{k,nil} -> {k,k}
{k,a} ->
[k | a |> List.wrap]
|> Enum.uniq
|> Enum.map(fn a -> {a,k} end)
end)
|> List.flatten
end
@doc ~S"""
`opts_create_aliases_dict/1` does the same job as `opts_create_aliases_tuples/1` but returns a *key alias dict*.
## Examples
iex> [a: nil, b: [:b1], c: [:c1, :c2, :c3]] |> opts_create_aliases_dict
%{a: :a, b: :b, b1: :b, c: :c, c1: :c, c2: :c, c3: :c}
"""
@spec opts_create_aliases_dict(aliases_kvs) :: aliases_dict
def opts_create_aliases_dict(aliases) do
aliases
|> opts_create_aliases_tuples
|> Enum.into(%{})
end
@doc ~S"""
`opts_create_defstruct/2` takes an *opts*, together with a defaults map, and returns an *opts* where each value if the value of the key in the defaults map (with default `nil`).
`opts_create_defstruct/2` creates an argument suitable for use with `Kernel.defstruct/1`
The defaults map must contain *only* keys that are also in the opts list; any unknown keys will raise a `KeyError.`
## Examples
iex> [a: 1, b: :two, c: "tre", d: nil] |> opts_create_defstruct(%{a: 42, b: "two"})
[a: 42, b: "two", c: nil, d: nil]
iex> [a: 1, b: :two, c: "tre", d: nil] |> opts_create_defstruct(%{a: 42, b: "two", x: 1})
** (KeyError) key [:x] not found in: [a: 1, b: :two, c: "tre", d: nil]
"""
@spec opts_create_defstruct(opts, defaults_map) :: opts
def opts_create_defstruct(struct_kvs, defaults_map \\ %{})
def opts_create_defstruct(struct_kvs, defaults_map)
when is_map(defaults_map) and (map_size(defaults_map) == 0) do
struct_kvs |> Enum.map(fn {k,_v} -> {k,nil} end)
end
def opts_create_defstruct(struct_kvs, defaults_map)
when is_map(defaults_map) do
# do not allow keys in the default that aren't in the struct_kvs
# too dangerous as hard to spot e.g default with wrong name
struct_map = struct_kvs |> Map.new(fn {k,_v} -> {k, nil} end)
defaults_map
# get rid of known keys
|> Enum.reject(fn {k,_v} -> struct_map |> Map.has_key?(k) end)
|> Keyword.keys
|> case do
# no unknown keys
[] -> nil
unknown_keys ->
raise KeyError, key: unknown_keys, term: struct_kvs
end
struct_kvs
|> Enum.map(fn {k,_v} -> {k, defaults_map |> Map.get(k)} end)
end
@doc ~S"""
`opts_crue_defstruct/2` takes a *derivable opts*, together with a defaults map, and returns `{:ok, opts}` where each value is the value of the key in the defaults map (with default `nil`).
`opts_crue_defstruct/2` creates an argument suitable for use with `Kernel.defstruct/1`
The defaults map must contain *only* keys that are also in the opts list; any unknown keys will cause `{:error, error}`, where `error` is a `KeyError`, to be returned.
## Examples
iex> [a: 1, b: :two, c: "tre", d: nil] |> opts_crue_defstruct(%{a: 42, b: "two"})
{:ok, [a: 42, b: "two", c: nil, d: nil]}
iex> [a: 1, b: :two, c: "tre", d: nil] |> opts_crue_defstruct(%{a: 42, b: "two", x: 1})
{:error, %KeyError{key: :x, term: [a: 1, b: :two, c: "tre", d: nil]}}
"""
@spec opts_crue_defstruct(opts, defaults_map) :: {:ok, opts} | {:error, error}
def opts_crue_defstruct(struct_kvs, defaults_map \\ %{})
def opts_crue_defstruct(struct_kvs, defaults_map)
when is_map(defaults_map) and (map_size(defaults_map) == 0) do
{:ok, struct_kvs |> Enum.map(fn {k,_v} -> {k,nil} end)}
end
def opts_crue_defstruct(struct_kvs, defaults_map)
when is_map(defaults_map) do
with {:ok, struct_kvs} <- struct_kvs |> opts_normalise do
# do not allow keys in the default that aren't in the struct_kvs
# too dangerous as hard to spot e.g default with wrong name
struct_map = struct_kvs |> Map.new(fn {k,_v} -> {k, nil} end)
defaults_map
# get rid of known keys
|> Enum.reject(fn {k,_v} -> struct_map |> Map.has_key?(k) end)
|> Keyword.keys
|> case do
# no unknown keys
[] -> {:ok, struct_kvs |> Enum.map(fn {k,_v} -> {k, defaults_map |> Map.get(k)} end)}
unknown_keys -> unknown_keys |> new_key_error_result(struct_kvs)
end
else
{:error, _} = result -> result
end
end
@doc ~S"""
`opts_crue_defstruct!/2` calls `opts_crue_defstruct/2` and if the result is `{:ok, opts}` returns `opts`.
## Examples
iex> [a: 1, b: :two, c: "tre", d: nil] |> opts_crue_defstruct!(%{a: 42, b: "two"})
[a: 42, b: "two", c: nil, d: nil]
iex> [a: 1, b: :two, c: "tre", d: nil] |> opts_crue_defstruct!(%{a: 42, b: "two", x: 1})
** (KeyError) key :x not found in: [a: 1, b: :two, c: "tre", d: nil]
"""
@spec opts_crue_defstruct!(opts, defaults_map) :: opts | no_return
def opts_crue_defstruct!(struct_kvs, defaults_map \\ %{})
def opts_crue_defstruct!(opts, defaults_map) do
case opts_crue_defstruct(opts, defaults_map) do
{:ok, opts} -> opts
{:error, error} -> raise error
end
end
@doc ~S"""
`opts_avoir_keys/2` takes an *opts* and a *key spec*.
If all of the keys are present in the `opts`, its returns `{:ok, opts}`.
If there are any missing keys, `{:error, error}`, where `error` is a `KeyError`, will be returned.
## Examples
iex> [a: 1, b: 2, c: 3] |> opts_avoir_keys([:a, :b, :c])
{:ok, [a: 1, b: 2, c: 3]}
iex> [a: 1, b: 2, c: 3] |> opts_avoir_keys(%{a: 1, b: 2, c: 3})
{:ok, [a: 1, b: 2, c: 3]}
iex> [a: 1, b: 2, c: 3] |> opts_avoir_keys([:a, :b, :d, :a])
{:error, %KeyError{key: :d, term: [a: 1, b: 2, c: 3]}}
iex> [a: 1, b: 2, c: 3] |> opts_avoir_keys(%{x: nil, y: nil, z: nil})
{:error, %KeyError{key: [:x, :y, :z], term: [a: 1, b: 2, c: 3]}}
"""
@spec opts_avoir_keys(any, any) :: {:ok, opts} | {:error, error}
def opts_avoir_keys(opts, keys)
def opts_avoir_keys(opts, keys) do
with {:ok, opts_keys} <- opts |> normalise_key_spec,
{:ok, wanted_keys} <- keys |> normalise_key_spec do
wanted_keys -- opts_keys
|> case do
# none missing
[] -> {:ok, opts}
missing_keys -> missing_keys |> new_key_error_result(opts)
end
else
{:error, _} = result -> result
end
end
@doc ~S"""
`opts_avoir_keys?/2` calls `opts_avoir_keys/2` and if the result is `{:ok, _}`, returns `true`, else `false`.
## Examples
iex> [a: 1, b: 2, c: 3] |> opts_avoir_keys?([:a, :b, :c])
true
iex> [a: 1, b: 2, c: 3] |> opts_avoir_keys?(%{a: 1, b: 2, c: 3})
true
iex> [a: 1, b: 2, c: 3] |> opts_avoir_keys?([:a, :b, :d, :a])
false
iex> [a: 1, b: 2, c: 3] |> opts_avoir_keys?(%{x: nil, y: nil, z: nil})
false
"""
@spec opts_avoir_keys?(any, any) :: true | false
def opts_avoir_keys?(opts, keys) do
case opts_avoir_keys(opts, keys) do
{:ok, _} -> true
_ -> false
end
end
@doc ~S"""
`opts_avoir_keys!/2` calls `opts_avoir_keys/2` and if the result is `{:ok, opts}`, returns `opts`.
## Examples
iex> [a: 1, b: 2, c: 3] |> opts_avoir_keys!([:a, :b, :c])
[a: 1, b: 2, c: 3]
iex> [a: 1, b: 2, c: 3] |> opts_avoir_keys!(%{a: 1, b: 2, c: 3})
[a: 1, b: 2, c: 3]
iex> [a: 1, b: 2, c: 3] |> opts_avoir_keys!([:a, :b, :d, :a])
** (KeyError) key :d not found in: [a: 1, b: 2, c: 3]
iex> [a: 1, b: 2, c: 3] |> opts_avoir_keys!(%{x: nil, y: nil, z: nil})
** (KeyError) key [:x, :y, :z] not found in: [a: 1, b: 2, c: 3]
"""
@spec opts_avoir_keys!(any, any) :: opts | no_return
def opts_avoir_keys!(opts, keys) do
case opts_avoir_keys(opts, keys) do
{:ok, opts} -> opts
{:error, error} -> raise error
end
end
@doc ~S"""
`opts_has_keys/2` takes an *opts*, together with a list or dictionary (map) of wanted `keys`.
If all of the `keys` are present in the `opts`, its returns `{:ok, opts}`.
If there are any missing keys, `{:error, {present_opts, missing_keys}}` is returned, where the
`present_opts` include *only* the tuples for the wanted keys (i.e. result of `Keyword.take/2` for the wanted keys).
## Examples
iex> [a: 1, b: 2, c: 3] |> opts_has_keys([:a, :b, :c])
{:ok, [a: 1, b: 2, c: 3]}
iex> [a: 1, b: 2, c: 3] |> opts_has_keys(%{a: 1, b: 2, c: 3})
{:ok, [a: 1, b: 2, c: 3]}
iex> [a: 1, b: 2, c: 3] |> opts_has_keys([:a, :b, :d, :a])
{:error, {[a: 1, b: 2], [:d]}}
iex> [a: 1, b: 2, c: 3] |> opts_has_keys(%{x: nil, y: nil, z: nil})
{:error, {[], [:x, :y, :z]}}
"""
@spec opts_has_keys(opts, keys) :: {:ok, opts} | {:error, {opts,opts}}
@spec opts_has_keys(opts, dict) :: {:ok, opts} | {:error, {opts,opts}}
def opts_has_keys(opts, keys_or_dict)
def opts_has_keys(opts, dict) when is_map(dict) do
opts
|> opts_has_keys(dict |> Map.keys)
end
def opts_has_keys(opts, keys) when is_list(keys) do
opts_keys = opts |> Keyword.keys |> Enum.uniq
wanted_keys = keys |> Enum.uniq
wanted_keys -- opts_keys
|> case do
# none missing
[] -> {:ok, opts}
missing_keys ->
wanted_tuples = opts |> opts_take_keys(wanted_keys)
{:error, {wanted_tuples, missing_keys}}
end
end
@doc ~S"""
`opts_has_keys?/2` calls `opts_has_keys/2` and if the result is `{:ok, _}`, returns `true`, else `false`.
## Examples
iex> [a: 1, b: 2, c: 3] |> opts_has_keys?([:a, :b, :c])
true
iex> [a: 1, b: 2, c: 3] |> opts_has_keys?(%{a: 1, b: 2, c: 3})
true
iex> [a: 1, b: 2, c: 3] |> opts_has_keys?([:a, :b, :d, :a])
false
iex> [a: 1, b: 2, c: 3] |> opts_has_keys?(%{x: nil, y: nil, z: nil})
false
"""
@spec opts_has_keys?(opts, keys) :: true | false
@spec opts_has_keys?(opts, dict) :: true | false
def opts_has_keys?(opts, keys) do
case opts_has_keys(opts, keys) do
{:ok, _} -> true
_ -> false
end
end
@doc ~S"""
`opts_has_keys!/2` calls `opts_has_keys/2` and if the result is `{:ok, opts}`, returns `opts`.
## Examples
iex> [a: 1, b: 2, c: 3] |> opts_has_keys!([:a, :b, :c])
[a: 1, b: 2, c: 3]
iex> [a: 1, b: 2, c: 3] |> opts_has_keys!(%{a: 1, b: 2, c: 3})
[a: 1, b: 2, c: 3]
iex> [a: 1, b: 2, c: 3] |> opts_has_keys!([:a, :b, :d, :a])
** (KeyError) key [:d] not found in: [a: 1, b: 2, c: 3]
iex> [a: 1, b: 2, c: 3] |> opts_has_keys!(%{x: nil, y: nil, z: nil})
** (KeyError) key [:x, :y, :z] not found in: [a: 1, b: 2, c: 3]
"""
@spec opts_has_keys!(opts, keys) :: opts | no_return
@spec opts_has_keys!(opts, dict) :: opts | no_return
def opts_has_keys!(opts, keys) do
case opts_has_keys(opts, keys) do
{:ok, x} -> x
{:error, {_present_tuples, missing_keys}} ->
raise KeyError, key: missing_keys, term: opts
end
end
@doc ~S"""
`opts_canon_keys!/2` takes an *opts*, together with a lookup dictionary and replaces each key with its canonical value from the dictionary. Unknown keys raise a `KeyError`.
## Examples
iex> [a: 1, b: 2, c: 3] |> opts_canon_keys!(%{a: :x, b: :y, c: :z})
[x: 1, y: 2, z: 3]
iex> [x: 1, y: 3, z: 3] |> opts_canon_keys!(%{a: 1, b: 2, c: 3})
** (KeyError) key :x not found in: %{a: 1, b: 2, c: 3}
"""
@spec opts_canon_keys!(opts, dict) :: opts | no_return
def opts_canon_keys!(opts, dict) when is_map(dict) do
opts |> Enum.map(fn {k,v} -> {dict |> Map.fetch!(k), v} end)
end
@doc ~S"""
`opts_canon_keys/2` takes an *opts*, together with either a dictionary (map) or (keyword) list of aliases.
If a dictionary is provided, each key in the `opts` is replaced with its (canonical) value from the dictionary, returning `{:ok, transformed_opts}`.
If a (keyword) list of aliases is provided, the aliases are first converted into a dictionary by `opts_create_aliases_dict/1` and the dictionary used as described above.
If there are any unknown keys, `{:error, {known_opts, unknown_opts}}` is returned.
## Examples
iex> [a: 1, b: 2, c: 3] |> opts_canon_keys(%{a: :x, b: :y, c: :z})
{:ok, [x: 1, y: 2, z: 3]}
iex> [a: 11, p: 1, b: 22, q: 2, c: 33, r: 3] |> opts_canon_keys(%{a: :x, b: :y, c: :z})
{:error, {[x: 11, y: 22, z: 33], [p: 1, q: 2, r: 3]}}
iex> [a: 1, b: 2, c: 3] |> opts_canon_keys([a_canon: :a, b_canon: [:b], c_canon: [:c, :cc]])
{:ok, [a_canon: 1, b_canon: 2, c_canon: 3]}
iex> [a: 1, b: 2, c: 3] |> opts_canon_keys([a_canon: :a, b_canon: nil, c_canon: [:c, :cc]])
{:error, {[a_canon: 1, c_canon: 3], [b: 2]}}
"""
@spec opts_canon_keys(opts, dict) :: {:ok, opts} | {:error, {opts, opts}}
@spec opts_canon_keys(opts, opts) :: {:ok, opts} | {:error, {opts, opts}}
def opts_canon_keys(opts, dict) when is_map(dict) do
opts
# split into known and unknown keys
|> Enum.split_with(fn {k,_v} -> Map.has_key?(dict, k) end)
|> case do
# no unknown keys
{known_tuples, []} ->
{:ok, opts_canon_keys!(known_tuples, dict)}
{known_tuples, unknown_tuples} ->
{:error, {opts_canon_keys!(known_tuples, dict), unknown_tuples}}
end
end
def opts_canon_keys(opts, aliases) when is_list(aliases) do
opts
|> opts_canon_keys(aliases |> opts_create_aliases_dict)
end
@doc ~S"""
`opts_canonical_keys/2` takes a *derivable opts*, together with a *key alias dict*.
Each key in the `opts` is replaced with its (canonical) value from the dictionary, returning `{:ok, canon_opts}`.
If there are any unknown keys, `{:error, error}`, where `error` is a `KeyError`, will be returned.
## Examples
iex> [a: 1, b: 2, c: 3] |> opts_canonical_keys(%{a: :x, b: :y, c: :z})
{:ok, [x: 1, y: 2, z: 3]}
iex> [a: 1, b: 2, c: 3] |> opts_canonical_keys([a: :x, b: :y, c: :z])
{:ok, [x: 1, y: 2, z: 3]}
iex> [a: 11, p: 1, b: 22, q: 2, c: 33, r: 3] |> opts_canonical_keys(%{a: :x, b: :y, c: :z})
{:error, %KeyError{key: [:p, :q, :r], term: %{a: :x, b: :y, c: :z}}}
iex> [a: 1, b: 2, c: 3] |> opts_canonical_keys([a_canon: :a, b_canon: [:b], c_canon: [:c, :cc]])
{:error, %ArgumentError{message: "expected valid key alias dictionary; got: %{a_canon: :a, b_canon: [:b], c_canon: [:c, :cc]}"}}
"""
@spec opts_canonical_keys(any, any) :: {:ok, opts} | {:error, error}
def opts_canonical_keys(opts, dict) do
with {:ok, opts} <- opts |> opts_normalise,
{:ok, dict} <- dict |> normalise_key_alias_dict do
opts
# reject known_keys
|> Enum.reject(fn {k,_v} -> Map.has_key?(dict, k) end)
|> case do
# no unknown keys
[] ->
canon_tuples = opts
|> Enum.map(fn{k,v} -> {Map.get(dict,k), v} end)
{:ok, canon_tuples}
unknown_tuples -> unknown_tuples |> new_key_error_result(dict)
end
else
{:error, _} = result -> result
end
end
@doc ~S"""
`opts_canonical_keys!/2` calls `opts_canonical_keys/2` and if the result is `{:ok, opts}` returns `opts`.
## Examples
iex> [a: 1, b: 2, c: 3] |> opts_canonical_keys!(%{a: :x, b: :y, c: :z})
[x: 1, y: 2, z: 3]
iex> [a: 1, b: 2, c: 3] |> opts_canonical_keys!([a: :x, b: :y, c: :z])
[x: 1, y: 2, z: 3]
iex> [x: 1, y: 3, z: 3] |> opts_canonical_keys!(%{a: 1, b: 2, c: 3})
** (ArgumentError) expected valid key alias dictionary; got: %{a: 1, b: 2, c: 3}
"""
@spec opts_canonical_keys!(any, any) :: opts | no_return
def opts_canonical_keys!(opts, dict) do
with {:ok, opts} <- opts |> opts_canonical_keys(dict) do
opts
else
{:error, error} -> raise error
end
end
@doc ~S"""
`opts_sort_keys/` takes a *derivable opts*, together with a list of sort keys, and returns the opts sorted in the sort keys order. Duplicate keys follow one after another.
Any keys found but not given in the sort keys follow the sorted keys in the returned opts.
Any key in the sort list not found in the opts is ignored.
## Examples
iex> [a: 1, b: 2, c: 3, d: 4] |> opts_sort_keys
[a: 1, b: 2, c: 3, d: 4]
iex> [a: 1, b: 2, c: 3, d: 4] |> opts_sort_keys([:c, :a])
[c: 3, a: 1, b: 2, d: 4]
iex> [] |> opts_sort_keys([:c, :a])
[]
iex> [a: 11, b: 2, c: 3, a: 12, d: 4] |> opts_sort_keys([:c, :a])
[c: 3, a: 11, a: 12, b: 2, d: 4]
iex> [a: 11, b: 21, c: 3, a: 12, d: 4, b: 22] |> opts_sort_keys([:d, :x, :b, :z])
[d: 4, b: 21, b: 22, a: 11, c: 3, a: 12]
"""
@spec opts_sort_keys(any, any) :: {:ok, opts} | {:error, error}
def opts_sort_keys(opts, keys \\ [])
def opts_sort_keys([], _keys) do
[]
end
def opts_sort_keys(opts, keys) do
sort_keys = keys |> Enum.uniq
sort_dict = sort_keys |> Map.new(fn k -> {k,nil} end)
# partition the opts into sort and other keys
{sorted_tuples, remain_tuples} = opts
|> Enum.split_with(fn {k,_v} -> Map.has_key?(sort_dict, k) end)
# collect the sorted_tuples for same key
sort_keys
|> Enum.flat_map(fn k ->
sorted_tuples
|> Keyword.get_values(k)
|> Enum.map(fn v -> {k,v} end)
end)
|> Kernel.++(remain_tuples)
end
@doc ~S"""
`opts_take_keys!/1` takes an *opts*, together with a *key list* and returns the *opts* with just the supplied keys.
It any of the keys are not found, raises a `KeyError` citing the missing keys.
## Examples
iex> [a: 1, b: 2, c: 3] |> opts_take_keys!([:c, :a])
[a: 1, c: 3]
iex> [a: 1, b: 2, c: 3] |> opts_take_keys!([:d, :a])
** (KeyError) key [:d] not found in: [a: 1, b: 2, c: 3]
"""
@spec opts_take_keys!(opts, keys) :: opts
def opts_take_keys!(opts, keys \\ [])
def opts_take_keys!([], _keys) do
[]
end
def opts_take_keys!(opts, []) when is_list(opts) do
[]
end
def opts_take_keys!(opts, keys) when is_list(keys) do
opts
|> opts_take_keys(keys)
# check all keys present
|> opts_has_keys(keys)
|> case do
{:ok, new_opts} -> new_opts
{:error, {_present_opts, missing_keys}} ->
raise KeyError, key: missing_keys, term: opts
end
end
@doc ~S"""
`opts_drop_keys!/1` takes an *opts*, together with a *key list* and returns the *opts* without the supplied keys.
It any of the keys are not found, raises a `KeyError` citing the missing keys.
## Examples
iex> [a: 1, b: 2, c: 3] |> opts_drop_keys!([:b])
[a: 1, c: 3]
iex> [a: 11, b: 21, c: 3, b: 22, a: 12] |> opts_drop_keys!([:b])
[a: 11, c: 3, a: 12]
iex> [a: 1, b: 2, c: 3] |> opts_drop_keys!([:d, :a])
** (KeyError) key [:d] not found in: [a: 1, b: 2, c: 3]
"""
@spec opts_drop_keys!(opts, keys) :: opts
def opts_drop_keys!(opts, keys \\ [])
def opts_drop_keys!([], _keys) do
[]
end
def opts_drop_keys!(opts, []) when is_list(opts) do
[]
end
def opts_drop_keys!(opts, keys) when is_list(keys) do
opts
|> opts_has_keys(keys)
|> case do
{:ok, _} -> opts |> opts_drop_keys(keys)
{:error, {_present_opts, missing_keys}} ->
raise KeyError, key: missing_keys, term: opts
end
end
@doc ~S"""
`opts_filter/2` takes a *derivable opts*, together with a *tuple predicate* and returns `{:ok, opts}` where `opts` has all the 2tuples the *tuple predicate* return `true` for.
## Examples
iex> [a: 1, b: 2, c: 3] |> opts_filter(&(&1))
{:ok, [a: 1, b: 2, c: 3]}
iex> [a: 1, b: 2, c: 3] |> opts_filter(
...> fn
...> {:a,_} -> true
...> _ -> false
...> end)
{:ok, [a: 1]}
iex> %{a: 1, b: 2, c: 3} |> opts_filter(
...> fn
...> {:a,_} -> false
...> _ -> true
...> end)
{:ok, [b: 2, c: 3]}
iex> {:error, error} = [a: 1, b: 2, c: 3] |> opts_filter(:not_a_function)
...> match?(%ArgumentError{message: "expected valid tuple predicate; got: :not_a_function"}, error)
true
iex> {:error, error} = :not_opts |> opts_filter(&(&1))
...> match?(%ArgumentError{message: "expected valid derivable opts; got: :not_opts"}, error)
true
"""
@spec opts_filter(any, any) :: {:ok, opts} | {:error, error}
def opts_filter(opts, fun_pred) when is_function(fun_pred,1) do
with {:ok, opts} <- opts |> opts_normalise do
opts = opts
|> Enum.filter(fun_pred)
{:ok, opts}
else
{:error, _} = result -> result
end
end
def opts_filter(_opts, fun_pred) do
new_error_result(m: "expected valid tuple predicate", v: fun_pred)
end
@doc ~S"""
`opts_filter!/2` calls `opts_filter/2` and if the result is `{:ok, opts}` returns `opts`.
## Examples
iex> [a: 1, b: 2, c: 3] |> opts_filter!(&(&1))
[a: 1, b: 2, c: 3]
iex> [a: 1, b: 2, c: 3] |> opts_filter!(
...> fn
...> {:a,_} -> true
...> _ -> false
...> end)
[a: 1]
iex> %{a: 1, b: 2, c: 3} |> opts_filter!(
...> fn
...> {:a,_} -> false
...> _ -> true
...> end)
[b: 2, c: 3]
iex> [a: 1, b: 2, c: 3] |> opts_filter!(:not_a_function)
** (ArgumentError) expected valid tuple predicate; got: :not_a_function
iex> :not_opts |> opts_filter!(&(&1))
** (ArgumentError) expected valid derivable opts; got: :not_opts
"""
@spec opts_filter!(any, any) :: opts | no_return
def opts_filter!(opts, dict) do
with {:ok, opts} <- opts |> opts_filter(dict) do
opts
else
{:error, error} -> raise error
end
end
@doc ~S"""
`opts_reject/2` takes a *derivable opts*, together with a *tuple predicate* and returns `{:ok, opts}` where `opts` has all the 2tuples the *tuple predicate* returns `false` for.
## Examples
iex> [a: 1, b: 2, c: 3] |> opts_reject(&(&1))
{:ok, []}
iex> [a: 1, b: 2, c: 3] |> opts_reject(
...> fn
...> {:a,_} -> true
...> _ -> false
...> end)
{:ok, [b: 2, c: 3]}
iex> %{a: 1, b: 2, c: 3} |> opts_reject(
...> fn
...> {:a,_} -> false
...> _ -> true
...> end)
{:ok, [a: 1]}
iex> {:error, error} = [a: 1, b: 2, c: 3] |> opts_reject(:not_a_function)
...> match?(%ArgumentError{message: "expected valid tuple predicate; got: :not_a_function"}, error)
true
iex> {:error, error} = :not_opts |> opts_reject(&(&1))
...> match?(%ArgumentError{message: "expected valid derivable opts; got: :not_opts"}, error)
true
"""
@spec opts_reject(any, any) :: {:ok, opts} | {:error, error}
def opts_reject(opts, fun_pred) when is_function(fun_pred,1) do
with {:ok, opts} <- opts |> opts_normalise do
opts = opts
|> Enum.reject(fun_pred)
{:ok, opts}
else
{:error, _} = result -> result
end
end
def opts_reject(_opts, fun_pred) do
new_error_result(m: "expected valid tuple predicate", v: fun_pred)
end
@doc ~S"""
`opts_reject!/2` calls `opts_reject/2` and if the result is `{:ok, opts}` returns `opts`.
## Examples
iex> [a: 1, b: 2, c: 3] |> opts_reject!(&(&1))
[]
iex> [a: 1, b: 2, c: 3] |> opts_reject!(
...> fn
...> {:a,_} -> true
...> _ -> false
...> end)
[b: 2, c: 3]
iex> %{a: 1, b: 2, c: 3} |> opts_reject!(
...> fn
...> {:a,_} -> false
...> _ -> true
...> end)
[a: 1]
iex> [a: 1, b: 2, c: 3] |> opts_reject!(:not_a_function)
** (ArgumentError) expected valid tuple predicate; got: :not_a_function
iex> :not_opts |> opts_reject!(&(&1))
** (ArgumentError) expected valid derivable opts; got: :not_opts
"""
@spec opts_reject!(any, any) :: opts | no_return
def opts_reject!(opts, dict) do
with {:ok, opts} <- opts |> opts_reject(dict) do
opts
else
{:error, error} -> raise error
end
end
@doc ~S"""
`opts_predicate/2` takes a *derivable opts*, together with a *tuple predicate* and returns `{:ok, opts}` if all the 2tuples pass the *tuple predicate*.
If any of the *opts* fail the *tuple predicate*, {:error, error} is returned where `error` will be a `KeyError` whose `key` field contains all the keys that failed.
## Examples
iex> [a: 1, b: 2, c: 3] |> opts_predicate(&(&1))
{:ok, [a: 1, b: 2, c: 3]}
iex> [a: 1, b: 2, c: 3] |> opts_predicate(fn {_k,v} -> v |> is_integer end)
{:ok, [a: 1, b: 2, c: 3]}
iex> [a: 1, b: 2, c: 3] |> opts_predicate(
...> fn
...> {:a,_} -> true
...> _ -> false
...> end)
{:error, %KeyError{key: [:b, :c], term: [a: 1, b: 2, c: 3]}}
iex> {:error, error} = [a: 1, b: 2, c: 3] |> opts_predicate(:not_a_function)
...> match?(%ArgumentError{message: "expected valid tuple predicate; got: :not_a_function"}, error)
true
iex> {:error, error} = :not_opts |> opts_predicate(&(&1))
...> match?(%ArgumentError{message: "expected valid derivable opts; got: :not_opts"}, error)
true
"""
@spec opts_predicate(any, any) :: {:ok, opts} | {:error, error}
def opts_predicate(opts, fun_pred) when is_function(fun_pred,1) do
with {:ok, norm_opts} <- opts |> opts_normalise do
norm_opts
|> Enum.split_with(fun_pred)
|> case do
{pass_opts, []} -> {:ok, pass_opts}
{_pass_opts, fail_opts} ->
# build a KeyError with failing keys
fail_keys = fail_opts |> Keyword.keys |> Enum.uniq
{:error, %KeyError{key: fail_keys, term: opts}}
end
else
{:error, _} = result -> result
end
end
def opts_predicate(_opts, fun_pred) do
new_error_result(m: "expected valid tuple predicate", v: fun_pred)
end
@doc ~S"""
`opts_predicate!/2` calls `opts_predicate/2` and if the result is `{:ok, opts}` returns `opts`.
## Examples
iex> [a: 1, b: 2, c: 3] |> opts_predicate(&(&1))
{:ok, [a: 1, b: 2, c: 3]}
iex> [a: 1, b: 2, c: 3] |> opts_predicate(fn {_k,v} -> v |> is_integer end)
{:ok, [a: 1, b: 2, c: 3]}
iex> [a: 1, b: 2, c: 3] |> opts_predicate(
...> fn
...> {:a,_} -> true
...> _ -> false
...> end)
{:error, %KeyError{key: [:b, :c], term: [a: 1, b: 2, c: 3]}}
iex> [a: 1, b: 2, c: 3] |> opts_predicate!(:not_a_function)
** (ArgumentError) expected valid tuple predicate; got: :not_a_function
iex> :not_opts |> opts_predicate!(&(&1))
** (ArgumentError) expected valid derivable opts; got: :not_opts
"""
@spec opts_predicate!(any, any) :: opts | no_return
def opts_predicate!(opts, dict) do
with {:ok, opts} <- opts |> opts_predicate(dict) do
opts
else
{:error, error} -> raise error
end
end
@doc ~S"""
`opts_filter_keys/2` takes a *derivable opts* and a *key spec* and returns `{:ok, opts}` where `opts` has all keys from the original *derivable opts* that appear in the *key spec*.
## Examples
iex> [a: 1, b: 2, c: 3] |> opts_filter_keys([:a, :b, :c])
{:ok, [a: 1, b: 2, c: 3]}
iex> [a: 1, b: 2, c: 3] |> opts_filter_keys([b: :want_b, c: :and_c])
{:ok, [b: 2, c: 3]}
iex> [a: 1, b: 2, c: 3] |> opts_filter_keys(%{a: 42, b: nil})
{:ok, [a: 1, b: 2]}
iex> [a: 1, b: 2, c: 3] |> opts_filter_keys(:not_a_key_spec)
{:error, %ArgumentError{message: "expected enum; got: :not_a_key_spec"}}
iex> :not_opts |> opts_filter_keys([:a, :b, :c])
{:error, %ArgumentError{message: "expected valid derivable opts; got: :not_opts"}}
"""
@spec opts_filter_keys(any, any) :: {:ok, opts} | {:error, error}
def opts_filter_keys(opts, keys)
def opts_filter_keys(opts, keys) do
with {:ok, filter_keys} <- keys |> normalise_key_spec do
filter_map = filter_keys |> Map.new(fn k -> {k, nil} end)
filter_pred = fn {k,_v} -> filter_map |> Map.has_key?(k) end
with {:ok, _filter_opts} = result <- opts |> opts_filter(filter_pred) do
result
else
{:error, _} = result -> result
end
else
{:error, _} = result -> result
end
end
@doc ~S"""
`opts_filter_keys!/2` calls `opts_filter_keys/2` and if the result is `{:ok, opts}`, returns `opts`.
## Examples
iex> [a: 1, b: 2, c: 3] |> opts_filter_keys!([:a, :b, :c])
[a: 1, b: 2, c: 3]
iex> [a: 1, b: 2, c: 3] |> opts_filter_keys!([b: :want_b, c: :and_c])
[b: 2, c: 3]
iex> [a: 1, b: 2, c: 3] |> opts_filter_keys!(%{a: 42, b: nil})
[a: 1, b: 2]
iex> [a: 1, b: 2, c: 3] |> opts_filter_keys!(:not_a_key_spec)
** (ArgumentError) expected enum; got: :not_a_key_spec
iex> :not_opts |> opts_filter_keys!([:a, :b, :c])
** (ArgumentError) expected valid derivable opts; got: :not_opts
"""
@spec opts_filter_keys!(any, any) :: opts | no_return
def opts_filter_keys!(opts, keys) do
case opts_filter_keys(opts, keys) do
{:ok, opts} -> opts
{:error, error} -> raise error
end
end
@doc ~S"""
`opts_reject_keys/2` takes a *derivable opts* and a *key spec* and returns `{:ok, opts}` where `opts` has all keys from the original *derivable opts* that **do not** appear in the *key spec*.
## Examples
iex> [a: 1, b: 2, c: 3] |> opts_reject_keys([:a, :b, :c])
{:ok, []}
iex> [a: 1, b: 2, c: 3] |> opts_reject_keys([b: "b value", c: :dont_want_this_key])
{:ok, [a: 1]}
iex> [a: 1, b: 2, c: 3] |> opts_reject_keys(%{a: nil, b: nil})
{:ok, [c: 3]}
iex> [a: 1, b: 2, c: 3] |> opts_reject_keys(:not_a_key_spec)
{:error, %ArgumentError{message: "expected enum; got: :not_a_key_spec"}}
iex> :not_opts |> opts_reject_keys([:a, :b, :c])
{:error, %ArgumentError{message: "expected valid derivable opts; got: :not_opts"}}
"""
@spec opts_reject_keys(any, any) :: {:ok, opts} | {:error, error}
def opts_reject_keys(opts, keys)
def opts_reject_keys(opts, keys) do
with {:ok, reject_keys} <- keys |> normalise_key_spec do
reject_map = reject_keys |> Map.new(fn k -> {k, nil} end)
reject_pred = fn {k,_v} -> reject_map |> Map.has_key?(k) end
with {:ok, _reject_opts} = result <- opts |> opts_reject(reject_pred) do
result
else
{:error, _} = result -> result
end
else
{:error, _} = result -> result
end
end
@doc ~S"""
`opts_reject_keys!/2` calls `opts_reject_keys/2` and if the result is `{:ok, opts}`, returns `opts`.
## Examples
iex> [a: 1, b: 2, c: 3] |> opts_reject_keys!([:a, :b, :c])
[]
iex> [a: 1, b: 2, c: 3] |> opts_reject_keys!([b: "b value", c: :dont_want_this_key])
[a: 1]
iex> [a: 1, b: 2, c: 3] |> opts_reject_keys!(%{a: nil, b: nil})
[c: 3]
iex> [a: 1, b: 2, c: 3] |> opts_reject_keys!(:not_a_key_spec)
** (ArgumentError) expected enum; got: :not_a_key_spec
iex> :not_opts |> opts_reject_keys!([:a, :b, :c])
** (ArgumentError) expected valid derivable opts; got: :not_opts
"""
@spec opts_reject_keys!(any, any) :: opts | no_return
def opts_reject_keys!(opts, keys) do
case opts_reject_keys(opts, keys) do
{:ok, opts} -> opts
{:error, error} -> raise error
end
end
@doc ~S"""
`opts_fetch_key_values/2` takes a *derivable opts* and a *key* and returns the values of the *key* as `{:ok, values}` where `values` will be a list.
One of more indices can be provided to select the values at specific indices; the default is to return all values from `Keyword.get_values/2`.
Note indices *must* be relative to the result of `Keyword.get_values/2` **not** the indices of the original *opts*
Values are returned in the same order as the indices. Indices may be repeated. Indices are validated; asking for an unknown/impossible index will cause an error.
## Examples
The default is to return *all* the values for a *key* i.e same as `Keyword.get_values/2`:
iex> [a: 1, b: 2, c: 3] |> opts_fetch_key_values(:a)
{:ok, [1]}
This examples show multiple values being returned:
iex> [a: 11, b: 2, a: 12, c: 3, a: 13] |> opts_fetch_key_values(:a)
{:ok, [11, 12, 13]}
Here the last value for the key is returned:
iex> [a: 11, b: 2, a: 12, c: 3, a: 13] |> opts_fetch_key_values(:a, -1)
{:ok, [13]}
Here only the first value is wanted:
iex> [a: 11, b: 2, a: 12, c: 3, a: 13] |> opts_fetch_key_values(:a, 0)
{:ok, [11]}
Values at different indices:
iex> [a: 11, b: 2, a: 12, c: 3, a: 13] |> opts_fetch_key_values(:a, [1,-1])
{:ok, [12, 13]}
Note order of the values is same order as the indices:
iex> [a: 11, b: 2, a: 12, c: 3, a: 13] |> opts_fetch_key_values(:a, [-2,0,-1])
{:ok, [12, 11, 13]}
The same index/indices can be repeated:
iex> [a: 11, b: 2, a: 12, c: 3, a: 13] |> opts_fetch_key_values(:a, [-2,0,-2,-1,0])
{:ok, [12, 11, 12, 13, 11]}
Indices are validated:
iex> [a: 11, b: 2, a: 12, c: 3, a: 13] |> opts_fetch_key_values(:a, 99)
{:error, %ArgumentError{message: "index invalid; got: 99"}}
iex> [a: 11, b: 2, a: 12, c: 3, a: 13] |> opts_fetch_key_values(:a, :not_an_index)
{:error, %ArgumentError{message: "index invalid; got: :not_an_index"}}
"""
@spec opts_fetch_key_values(any, any) :: {:ok, opts} | {:error, error}
def opts_fetch_key_values(opts, key, indicies \\ nil)
def opts_fetch_key_values(opts, key, nil) do
with {:ok, norm_opts} <- opts |> opts_normalise,
true <- norm_opts |> Keyword.has_key?(key) do
{:ok, norm_opts |> Keyword.get_values(key)}
else
false -> new_key_error_result(key, opts)
{:error, _} = result -> result
end
end
def opts_fetch_key_values(opts, key, indices) do
with {:ok, norm_opts} <- opts |> opts_normalise,
true <- norm_opts |> Keyword.has_key?(key) do
key_values = norm_opts |> Keyword.get_values(key)
with {:ok, indices} <- key_values |> opts_indices_validate(indices) do
indices
|> List.wrap
|> Enum.reduce({[],[]},
fn index, {known_values,missing_indices} ->
with {:ok, value} <- key_values |> Enum.fetch(index) do
{[value | known_values], missing_indices}
else
:error -> {known_values, [index | missing_indices]}
end
end)
|> case do
{known_values, []} -> {:ok, known_values |> Enum.reverse}
{_known_values, missing_indices} ->
new_key_error_result(missing_indices, key_values)
end
else
{:error, _} = result -> result
end
else
false -> new_key_error_result(key, opts)
{:error, _} = result -> result
end
end
@doc ~S"""
`opts_fetch_key_values!/2` takes a *derivable opts* and a *key* and returns the values of the *key* as `{:ok, values}`.
One of more indices can be provided to select the values at specific indices; the default is to return all values from `Keyword.get_values/2`.
Note indices *must* be relative to the result of `Keyword.get_values/2` **not** the indices of the original *opts*
Values are returned in the order they are given in the indices. Indices may be repeated.
## Examples
The default is to return *all* the values for a *key*:
iex> [a: 1, b: 2, c: 3] |> opts_fetch_key_values!(:a)
[1]
This examples show multiple values being returned:
iex> [a: 11, b: 2, a: 12, c: 3, a: 13] |> opts_fetch_key_values!(:a)
[11, 12, 13]
Here the last value for the key is returned:
iex> [a: 11, b: 2, a: 12, c: 3, a: 13] |> opts_fetch_key_values!(:a, -1)
[13]
Here the first value is wanted:
iex> [a: 11, b: 2, a: 12, c: 3, a: 13] |> opts_fetch_key_values!(:a, 0)
[11]
Values at different indices:
iex> [a: 11, b: 2, a: 12, c: 3, a: 13] |> opts_fetch_key_values!(:a, [1,-1])
[12, 13]
Note order of the values is same as order of the indices
iex> [a: 11, b: 2, a: 12, c: 3, a: 13] |> opts_fetch_key_values!(:a, [-2,0,-1])
[12, 11, 13]
The same index/indices can be requested more than once:
iex> [a: 11, b: 2, a: 12, c: 3, a: 13] |> opts_fetch_key_values!(:a, [-2,0,-2,-1,0])
[12, 11, 12, 13, 11]
Indices are validated:
iex> [a: 11, b: 2, a: 12, c: 3, a: 13] |> opts_fetch_key_values!(:a, :not_an_index)
** (ArgumentError) index invalid; got: :not_an_index
"""
@spec opts_fetch_key_values!(any, any, any) :: list | no_return
def opts_fetch_key_values!(opts, key, indices \\ nil)
def opts_fetch_key_values!(opts, key, indices) do
case opts_fetch_key_values(opts, key, indices) do
{:ok, values} -> values
{:error, error} -> raise error
end
end
@doc ~S"""
`canon_keys!/2` takes a *key list* together with a lookup dictionary and replaces each key with its canonical value from the dictionary. Unknown keys raise a `KeyError`.
## Examples
iex> [:a, :b, :c] |> canon_keys!(%{a: 1, b: 2, c: 3})
[1,2,3]
iex> [:x] |> canon_keys!(%{a: 1, b: 2, c: 3})
** (KeyError) key :x not found in: %{a: 1, b: 2, c: 3}
"""
@spec canon_keys!(alias_keys, dict) :: alias_keys | no_return
def canon_keys!(keys, dict) when is_map(dict) do
keys |> Enum.map(fn k -> dict |> Map.fetch!(k) end)
end
@doc ~S"""
`canon_keys/2` takes a *key list* together with a lookup dictionary and replaces each key with its canonical value from the dictionary, returning `{:ok, canon_keys}`.
If there are any unknown keys, `{:error, {canon_known_keys, unknown_keys}}` will be returned.
## Examples
iex> [:a, :b, :c] |> canon_keys(%{a: 1, b: 2, c: 3})
{:ok, [1,2,3]}
iex> [:a, :x, :b, :y, :c, :z] |> canon_keys(%{a: 1, b: 2, c: 3})
{:error, {[1, 2, 3], [:x, :y, :z]}}
"""
@spec canon_keys(alias_keys, dict) :: {:ok, alias_keys} | {:error, error}
def canon_keys(keys, dict) when is_map(dict) do
keys
# split into known and unknown keys
|> Enum.split_with(fn k -> Map.has_key?(dict, k) end)
|> case do
# no unknown keys
{known_keys, []} ->
{:ok, known_keys |> canon_keys!(dict)}
{known_keys, unknown_keys} ->
{:error, {known_keys |> canon_keys!(dict), unknown_keys}}
end
end
@doc ~S"""
`canonical_keys/2` takes a *key list* and *key alias dict* and replaces each key with its canonical value from the dictionary, returning `{:ok, canonical_keys}`.
If there are any unknown keys `{:error, error}`, where `error` is a `KeyError`, will be returned.
## Examples
iex> [:a, :b, :c] |> canonical_keys(%{a: :p, b: :q, c: :r})
{:ok, [:p,:q,:r]}
iex> [:a, :b, :c] |> canonical_keys(%{a: 1, b: 2, c: 3})
{:ok, [1,2,3]}
iex> [:a, :x, :b, :y, :c, :z] |> canonical_keys(%{a: 1, b: 2, c: 3})
{:error, %KeyError{key: [:x, :y, :z], term: %{a: 1, b: 2, c: 3}}}
"""
@spec canonical_keys(alias_keys, any) :: {:ok, alias_keys} | {:error, error}
def canonical_keys(keys, dict) do
with {:ok, keys} <- keys |> normalise_key_list,
{:ok, dict} <- dict |> normalise_key_dict do
keys
|> Enum.reject(fn k -> Map.has_key?(dict, k) end)
|> case do
# no unknown keys
[] ->
canon_keys = keys |> Enum.map(fn k -> dict |> Map.get(k) end)
{:ok, canon_keys}
unknown_keys ->
unknown_keys |> new_key_error_result(dict)
end
else
{:error, _} = result -> result
end
end
@doc ~S"""
`canonical_keys!/2` calls `canonical_keys/2` and if the result is `{:ok, canonical_keys}` returns `canonical_keys`.
## Examples
iex> [:a, :b, :c] |> canonical_keys!(%{a: :p, b: :q, c: :r})
[:p,:q,:r]
iex> [:a, :b, :c] |> canonical_keys!(%{a: 1, b: 2, c: 3})
[1,2,3]
iex> [:a, :x, :b, :y, :c, :z] |> canonical_keys!(%{a: 1, b: 2, c: 3})
** (KeyError) key [:x, :y, :z] not found in: %{a: 1, b: 2, c: 3}
"""
@spec canonical_keys!(alias_keys, dict) :: alias_keys | no_return
def canonical_keys!(keys, dict) do
with {:ok, keys} <- keys |> canonical_keys(dict) do
keys
else
{:error, error} -> raise error
end
end
@doc ~S"""
`canonical_key/2` takes a key together with a *key dict* and replaces the key with its canonical value from the dictionary, returning `{:ok, canonical_key}`.
If the key is unknown, `{:error, error}`, `error` is a `KeyError`, will be returned.
## Examples
iex> :b |> canonical_key(%{a: :p, b: :q, c: :r})
{:ok, :q}
iex> :a |> canonical_key(%{a: 1, b: 2, c: 3})
{:ok, 1}
iex> :x |> canonical_key(%{a: 1, b: 2, c: 3})
{:error, %KeyError{key: :x, term: %{a: 1, b: 2, c: 3}}}
"""
@spec canonical_key(alias_key, any) :: {:ok, alias_key} | {:error, error}
def canonical_key(key, dict) do
with {:ok, dict} <- dict |> normalise_key_dict,
{:ok, keys} <- [key] |> canonical_keys(dict) do
{:ok, keys |> hd}
else
{:error, %KeyError{} = error} -> {:error, error |> struct!(key: key)}
{:error, _} = result -> result
end
end
@doc ~S"""
`canonical_key!/2` calls `canonical_key/2` and if the result is `{:ok, canonical_key}` returns `canonical_key`.
## Examples
iex> :a |> canonical_key!(%{a: 1, b: 2, c: 3})
1
iex> :b |> canonical_key!(%{a: :p, b: :q, c: :r})
:q
iex> :x |> canonical_key!(%{a: 1, b: 2, c: 3})
** (KeyError) key :x not found in: %{a: 1, b: 2, c: 3}
"""
@spec canonical_key!(alias_key, dict) :: alias_key | no_return
def canonical_key!(key, dict) do
with {:ok, key} <- key |> canonical_key(dict) do
key
else
{:error, error} -> raise error
end
end
@doc ~S"""
`maybe_canon_keys/2` takes a *key list* together with a lookup dictionary and, if the key is in the dictionary, replaces it with its value. Unknown keys are passed through unchanged.
## Examples
iex> [:a, :b, :c] |> maybe_canon_keys(%{a: 1, b: 2, c: 3})
[1, 2, 3]
iex> [:x, :a] |> maybe_canon_keys(%{a: 1, b: 2, c: 3})
[:x, 1]
"""
@spec maybe_canon_keys(alias_keys, dict) :: alias_keys
def maybe_canon_keys(keys, dict) when is_map(dict) do
keys
|> Enum.map(fn k ->
case dict |> Map.has_key?(k) do
true -> dict |> Map.fetch!(k)
_ -> k
end
end)
end
@doc ~S"""
`list_wrap_flat_just/1` wraps a value (if not already a list), flattens and removes `nils` at the *first / top* level.
## Examples
iex> [{:a, 1}, nil, [{:b1, 12}, nil, {:b2, [nil, 22, nil]}], nil, {:c, 3}] |> list_wrap_flat_just
[a: 1, b1: 12, b2: [nil, 22, nil], c: 3]
iex> [[[nil, 42, nil]]] |> list_wrap_flat_just
[42]
"""
@spec list_wrap_flat_just(any) :: [any]
def list_wrap_flat_just(value) do
value
|> List.wrap
|> List.flatten
|> Enum.reject(&is_nil/1)
end
@doc ~S"""
`list_wrap_flat_just_uniq/1` wraps a value (if not already a list), flattens, removes `nils` at
the *first / top* level, and deletes duplicates (using `Enum.uniq/1`)
## Examples
iex> [{:a, 1}, nil, [{:b1, 12}, nil, {:b2, [nil, 22, nil]}], nil, {:c, 3}, {:a, 1}, {:b1, 12}] |> list_wrap_flat_just_uniq
[a: 1, b1: 12, b2: [nil, 22, nil], c: 3]
iex> [nil, [42, [42, 42, nil]], 42] |> list_wrap_flat_just_uniq
[42]
"""
@spec list_wrap_flat_just_uniq(any) :: [any]
def list_wrap_flat_just_uniq(value) do
value
|> List.wrap
|> List.flatten
|> Enum.reject(&is_nil/1)
|> Enum.uniq
end
defp normalise_error_message(value)
defp normalise_error_message(value) when is_binary(value) do
value
end
defp normalise_error_message(value) when is_atom(value) do
value |> to_string
end
defp normalise_error_message(value) do
value |> inspect
end
defp normalise_error_value(value)
defp normalise_error_value(value) do
"got: #{value |> inspect}"
end
defp new_error_result(opts)
defp new_error_result(opts) do
message = [
m: &normalise_error_message/1,
v: &normalise_error_value/1,
]
|> Enum.reduce([],fn {k,fun}, texts ->
opts
|> Keyword.has_key?(k)
|> case do
true ->
text = opts |> Keyword.get(k) |> fun.()
[text | texts]
_ -> texts
end
end)
|> Enum.reverse
|> Enum.join("; ")
{:error, %ArgumentError{message: message}}
end
defp new_key_error(values, term) do
cond do
Keyword.keyword?(values) -> values |> Keyword.keys
is_list(values) -> values
true -> raise ArgumentError, message: "expected opts or keys; got: #{inspect values}"
end
|> Enum.uniq
|> case do
[key] -> %KeyError{key: key, term: term}
keys -> %KeyError{key: keys, term: term}
end
end
defp new_key_error_result(values, term) do
{:error, new_key_error(values, term)}
end
end