Current section
Files
Jump to
Current section
Files
lib/harnais/list/list.ex
defmodule Harnais.List do
@moduledoc ~S"""
Functions for Testing Lists.
## Errors
Errors are managed by `Harnais.Error`.
The doctests include examples where the exception message
(`Exception.message/1`) is shown. Other doctests export the
exception (`Harnais.Error.export/1`) and show the exception field
breakdown.
"""
require Plymio.Fontais.Option
use Plymio.Codi
use Harnais.Attribute
use Harnais.Error.Attribute
@codi_opts [
{@plymio_codi_key_vekil, Plymio.Vekil.Codi.__vekil__()}
]
import Plymio.Fontais.Option,
only: [
opts_create_aliases_dict: 1,
opts_canonical_keys: 2
]
import Harnais.Error,
only: [
new_error_result: 1,
new_errors_result: 1
]
import Plymio.Funcio.Enum.Map.Collate,
only: [
map_concurrent_collate2_enum: 2
]
@type opts :: Harnais.opts()
@type error :: Harnais.error()
@harnais_list_error_message_list_invalid "list invalid"
@harnais_list_error_message_list_compare_failed "list compare failed"
@harnais_list_compare_worker_kvs_aliases [
{@harnais_key_compare_values, nil},
{@harnais_key_transform_list, nil}
]
@harnais_list_compare_worker_dict_aliases @harnais_list_compare_worker_kvs_aliases
|> opts_create_aliases_dict
@doc false
def opts_canonical_compare_worker_opts(opts, dict \\ @harnais_list_compare_worker_dict_aliases) do
opts |> opts_canonical_keys(dict)
end
defp list_fetch_index(list, index)
when is_list(list) and is_integer(index) do
case index > length(list) - 1 do
false ->
{:ok, Enum.at(list, index)}
true ->
new_error_result(m: "list #{inspect(list)} index invalid", v: index)
end
end
defp list_fetch_index!(list, index)
when is_list(list) and is_integer(index) do
case list_fetch_index(list, index) do
{:ok, value} ->
value
{:error, error} ->
raise error
end
end
defp list_compare_worker(list1, list2, opts)
defp list_compare_worker(list1, list2, opts)
when is_list(list1) and is_list(list2) do
with {:ok, opts} <- opts |> opts_canonical_compare_worker_opts do
fun_compare_values =
opts
|> Keyword.get(:compare_values, fn _key, v1, v2 -> v1 == v2 end)
{transform_list1, transform_list2} =
opts
|> Keyword.get(:transform_list)
|> case do
fun when is_function(fun) -> {fun.(list1), fun.(list2)}
_ -> {list1, list2}
end
index1_max = length(transform_list1) - 1
index2_max = length(transform_list2) - 1
[index1_max, index2_max]
|> Enum.max()
|> (fn
max when max < 0 -> []
max -> Range.new(0, max)
end).()
|> map_concurrent_collate2_enum(fn ndx ->
transform_list1
|> list_fetch_index(ndx)
|> case do
{:ok, v1} ->
transform_list2
|> list_fetch_index(ndx)
|> case do
{:ok, v2} ->
fun_compare_values.(ndx, v1, v2)
|> case do
true ->
@plymio_fontais_the_unset_value
x when x in [nil, false] ->
new_error_result(
t: :value,
m: @harnais_list_error_message_list_compare_failed,
r: @harnais_error_reason_mismatch,
i: ndx,
v1: v1,
v2: v2
)
{:ok, _} ->
@plymio_fontais_the_unset_value
{:error, %{__exception__: true}} = result ->
result
end
_ ->
new_error_result(
t: @harnais_error_value_field_type_value,
m: @harnais_list_error_message_list_compare_failed,
r: @harnais_error_reason_missing,
i: ndx,
v1: v1,
v2: @harnais_error_status_value_no_value
)
end
_ ->
new_error_result(
t: @harnais_error_value_field_type_value,
m: @harnais_list_error_message_list_compare_failed,
r: @harnais_error_reason_missing,
i: ndx,
v1: @harnais_error_status_value_no_value,
v2: list_fetch_index!(transform_list2, ndx)
)
end
end)
|> case do
{:error, %{__exception__: true}} = result -> result
{:ok, _} -> {:ok, list1}
end
else
{:error, %{__exception__: true}} = result -> result
end
end
@doc ~S"""
`harnais_list_compare/3` takes two values, each expected to be a
`List`, and optional *opts*, and compares them, concurrently, element by element.
If the compare succeeds `{:ok, first_argument}` is returned, else `{:error, error}`.
The default is to use `Kernel.==/2` to compare the elements at the same index of each
list. A `falsy` result will add a new error result.
The compare function can be overriden using the `:compare_values` key
together with a function of arity 3 which is passed the `index` (zero offset), `value1` and
`value2` and should return `true`, `false`, `nil`, `{:ok, value}`
or `{error, error}`.
The `:transform_list` option can be used to e.g. sort each list
before comparsion. The value must be a function of arity 1; it will
be passed each list in turn. (Note, if the compare succeeds, the
value in the `{:ok, value}` result is the original, un-transformed first argument.)
## Examples
Some simple sucessful compares:
iex> harnais_list_compare([], [])
{:ok, []}
iex> harnais_list_compare([1, 2, 3], [1, 2, 3])
{:ok, [1, 2, 3]}
iex> harnais_list_compare([a: 1, b: 2, c: 3], [a: 1, b: 2, c: 3])
{:ok, [a: 1, b: 2, c: 3]}
The next two examples demonstrate an element mismatch (`3` v `4`) at
index `2`. One shows the exception message and the other shows the
exception export:
iex> {:error, error} = harnais_list_compare([1, 2, 3], [1, 2, 4])
...> error |> Exception.message
"list compare failed, reason=:mismatch, type=:value, location=2, value1=3, value2=4"
iex> {:error, error} = harnais_list_compare([1, 2, 3], [1, 2, 4])
...> error |> Harnais.Error.export
{:ok, [error: [[m: "list compare failed", r: :mismatch, t: :value, l: 2, v1: 3, v2: 4]]]}
These two demonstrate unequal length lists:
iex> {:error, error} = harnais_list_compare([1, 2, 3], [1, 2])
...> error |> Exception.message
"list compare failed, reason=:missing, type=:value, location=2, value1=3, value2=:no_value"
iex> {:error, error} = harnais_list_compare([1, 2], [1, 2, 3])
...> error |> Harnais.Error.export
{:ok, [error: [[m: "list compare failed", r: :missing, t: :value, l: 2, v1: :no_value, v2: 3]]]}
Here the lists are sorted before the compare, ensuring a sucessful
compare. Note the returned list is the original 1st argument.
iex> harnais_list_compare([2, 1, 3], [3, 2, 1], transform_list: &Enum.sort/1)
{:ok, [2, 1, 3]}
The next two have a `:compare_values` function. The first one always
returns `true` even if `:value1` and `:value2` are different.
iex> harnais_list_compare([2, 1, 3], [3, 2, 1], compare_values: fn _k,_v1,_v2 -> true end)
{:ok, [2, 1, 3]}
The next one can return `{:error, error}` as well as `true`.
iex> {:error, error} = harnais_list_compare([2, 1, 3], [3, 2, 1],
...> compare_values: fn
...> k,_v1,_v2 when k < 2 -> true
...> _,v1,v2 -> Harnais.Error.new_error_result(
...> r: :override, t: :special, l: :the_one, v1: v1, v2: v2)
...> end)
...> error |> Harnais.Error.export
{:ok, [error: [[r: :override, t: :special, l: :the_one, v1: 3, v2: 1]]]}
First argument is not a list:
iex> {:error, error} = harnais_list_compare(42, [b: 2])
...> error |> Harnais.Error.export
{:ok, [error: [[m: "list compare failed", r: :not_list, t: :arg, l: 0, v: 42]]]}
Second argument is not a list:
iex> {:error, error} = harnais_list_compare([a: 1], 42)
...> error |> Exception.message
"list compare failed, reason=:not_list, type=:arg, location=1, got: 42"
Comparing `Keyword` lists works as expected:
iex> {:error, error} = harnais_list_compare([a: 1], [b: 2])
...> error |> Harnais.Error.export
{:ok, [error: [[m: "list compare failed", r: :mismatch, t: :value, l: 0, v1: {:a, 1}, v2: {:b, 2}]]]}
Examples for the bang function:
iex> harnais_list_compare!([1, 2, 3], [1, 2, 3])
[1, 2, 3]
iex> harnais_list_compare!([a: 1], [a: 1])
[a: 1]
iex> harnais_list_compare!([1, 21, 3], [1, 22, 3])
** (Harnais.Error) list compare failed, reason=:mismatch, type=:value, location=1, value1=21, value2=22
iex> harnais_list_compare!([1, 2, 3], [1, 2])
** (Harnais.Error) list compare failed, reason=:missing, type=:value, location=2, value1=3, value2=:no_value
Examples for the query function:
iex> harnais_list_compare?([1, 2, 3], [1, 2, 3])
true
iex> harnais_list_compare?([a: 1], [a: 1])
true
iex> harnais_list_compare?([1, 21, 3], [1, 22, 3])
false
iex> harnais_list_compare?([1, 2, 3], [1, 2])
false
"""
@since "0.1.0"
@spec harnais_list_compare(any, any, opts) :: {:ok, list} | {:error, error}
def harnais_list_compare(list1, list2, opts \\ [])
def harnais_list_compare(list1, list2, opts) do
# build errors incrementally
[list1, list2]
|> Stream.with_index()
|> Enum.reduce([], fn
{list, _ndx}, errors when is_list(list) ->
errors
{value, ndx}, errors ->
[
new_error_result(
t: @harnais_error_value_field_type_arg,
m: @harnais_list_error_message_list_compare_failed,
r: @harnais_error_reason_not_list,
i: ndx,
v: value
)
| errors
]
end)
|> case do
# no errors so far
[] ->
list_compare_worker(list1, list2, opts)
# error already
errors ->
errors |> Enum.reverse() |> Enum.map(&elem(&1, 1)) |> new_errors_result
end
end
@doc ~S"""
`harnais_list/1` tests whether argument is a `List` and,
if true, returns `{:ok, argument}` else `{:error, errors}`.
## Examples
Some simple succesful tests:
iex> harnais_list([1, 2, 3])
{:ok, [1, 2, 3]}
iex> harnais_list([1, :two, "tre"])
{:ok, [1, :two, "tre"]}
iex> harnais_list([a: 1, b: 2, c: 3])
{:ok, [a: 1, b: 2, c: 3]}
The next two examples have an non-list argument (`42`). The first
show the exception message and the second the exception export:
iex> {:error, error} = harnais_list(42)
...> error |> Exception.message
"list invalid, got: 42"
iex> {:error, error} = harnais_list(42)
...> error |> Harnais.Error.export
{:ok, [error: [[m: "list invalid", r: :not_list, t: :arg, v: 42]]]}
Examples for the bang function:
iex> harnais_list!([1, 2, 3])
[1, 2, 3]
iex> harnais_list!([1, :two, "tre"])
[1, :two, "tre"]
iex> harnais_list!([a: 1])
[a: 1]
iex> harnais_list!(42)
** (Harnais.Error) list invalid, got: 42
iex> harnais_list!(%{a: 1})
** (Harnais.Error) list invalid, got: %{a: 1}
Examples for the query function:
iex> harnais_list?([a: 1, b: 2, c: 3])
true
iex> harnais_list?(42)
false
iex> harnais_list?(%{a: 1})
false
"""
@since "0.1.0"
@spec harnais_list(any) :: {:ok, list} | {:error, error}
def harnais_list(value)
def harnais_list(value) when is_list(value) do
{:ok, value}
end
def harnais_list(value) do
new_error_result(
message_config: [:message, :value],
t: @harnais_error_value_field_type_arg,
m: @harnais_list_error_message_list_invalid,
r: @harnais_error_reason_not_list,
v: value
)
end
@quote_result_list_no_return quote(do: list | no_return)
[
# there is a bug in codi v0.3.0 that gets the doc wrong when `:as` is given.
delegate: [
name: :harnais_list?,
as: :is_list,
to: Kernel,
args: :list,
since: "0.1.0",
result: :boolean,
doc: "Delegated to `Kernel.is_list/1`"
],
bang: [as: :harnais_list, args: :list, since: "0.1.0", result: @quote_result_list_no_return],
bang: [
as: :harnais_list_compare,
args: [:list1, :list2],
since: "0.1.0",
result: @quote_result_list_no_return
],
query: [as: :harnais_list_compare, args: [:list1, :list2], since: "0.1.0", result: true],
bang: [
as: :harnais_list_compare,
args: [:list1, :list2, :opts],
since: "0.1.0",
result: @quote_result_list_no_return
],
query: [
as: :harnais_list_compare,
args: [:list1, :list2, :opts],
since: "0.1.0",
result: true
]
]
|> Enum.flat_map(fn {pattern, opts} ->
[pattern: [pattern: pattern] ++ opts]
end)
|> CODI.reify_codi(@codi_opts)
end