Packages

This library is meant to provide an ergonomic way to match, compare and bind data in complex types such as maps and structs.

Current section

Files

Jump to
exmatch lib protocol.ex
Raw

lib/protocol.ex

defprotocol ExMatch.Protocol do
@fallback_to_any true
@spec diff(t, any, any) :: nil | {left :: any, right :: any}
def diff(left, right, opts)
end
defimpl ExMatch.Protocol, for: Any do
def diff(value, value, _), do: nil
def diff(left = %struct{}, right = %struct{}, opts) do
fields = Map.get(opts, struct, [])
drop = Enum.filter(fields, &is_atom(&1))
merge =
Enum.reduce(fields, %{}, fn
{key, value}, map -> Map.put(map, key, value)
_, map -> map
end)
case ExMatch.Protocol.Map.diff(
left |> Map.from_struct() |> Map.drop(drop) |> Map.merge(merge),
right |> Map.from_struct() |> Map.drop(drop),
opts
) do
nil ->
nil
{left_map, right_map} ->
{
Map.put(left_map, :__struct__, struct),
Map.put(right_map, :__struct__, struct)
}
end
end
def diff(left, right = %_{}, opts) do
with impl = ExMatch.Protocol.impl_for(right),
false <- impl == ExMatch.Protocol.Any,
{right_result, left_result} <- ExMatch.Protocol.diff(right, left, opts) do
{left_result, right_result}
else
nil -> nil
true -> {left, right}
end
end
def diff(left, right, _),
do: {left, right}
end
defimpl ExMatch.Protocol, for: List do
def diff([left_value | left], [right_value | right], opts) do
this_diff = ExMatch.Protocol.diff(left_value, right_value, opts)
rest_diff = diff(left, right, opts)
case {this_diff, rest_diff} do
{nil, nil} ->
nil
{nil, {left_results, right_results}} ->
{[:eq | left_results], [:eq | right_results]}
{{left_result, right_result}, nil} ->
{[left_result], [right_result]}
{{left_result, right_result}, {left_results, right_results}} ->
{[left_result | left_results], [right_result | right_results]}
end
end
def diff([], [], _), do: nil
def diff(left, right, _opts) do
{left, right}
end
end
defimpl ExMatch.Protocol, for: Tuple do
def diff(left, right, opts) when is_tuple(right) do
left = Tuple.to_list(left)
right = Tuple.to_list(right)
case ExMatch.Protocol.List.diff(left, right, opts) do
{left, right} ->
{List.to_tuple(left), List.to_tuple(right)}
nil ->
nil
end
end
def diff(left, right, _) do
{left, right}
end
end
defimpl ExMatch.Protocol, for: Map do
def diff(left, right, opts) when is_map(right) do
case diff_items(left, right, opts) do
{left_diffs, right_diffs, right}
when left_diffs == [] and right_diffs == %{} and right == %{} ->
nil
{left_diffs, right_diffs, right} ->
{Map.new(left_diffs), Map.merge(right, right_diffs)}
end
end
def diff(left, right, _opts) do
{left, right}
end
def diff_items(left, right, opts) do
{left_diffs, right_diffs, right, _opts} =
Enum.reduce(left, {%{}, %{}, right, opts}, &diff_item/2)
{left_diffs, right_diffs, right}
end
defp diff_item({key, field}, {left_diffs, right_diffs, right, opts}) do
case right do
%{^key => right_value} ->
right = Map.delete(right, key)
case ExMatch.Protocol.diff(field, right_value, opts) do
{left_diff, right_diff} ->
left_diffs = Map.put(left_diffs, key, left_diff)
right_diffs = Map.put(right_diffs, key, right_diff)
{left_diffs, right_diffs, right, opts}
nil ->
{left_diffs, right_diffs, right, opts}
end
_ ->
left_diffs = Map.put(left_diffs, key, field)
{left_diffs, right_diffs, right, opts}
end
end
end
defimpl ExMatch.Protocol, for: DateTime do
def diff(left, right = %DateTime{}, _) do
diff_dates(left, right, right)
end
def diff(left, right, _) when is_binary(right) do
case DateTime.from_iso8601(right) do
{:ok, right_date, _} ->
diff_dates(left, right, right_date)
_ ->
{left, right}
end
end
def diff_dates(left, right, right_date) do
case DateTime.compare(left, right_date) do
:eq -> nil
_ -> {left, right}
end
end
end
if Code.ensure_loaded?(Decimal) do
defimpl ExMatch.Protocol, for: Decimal do
require Decimal
def diff(left, right, _) do
# ignore floats
Decimal.new(right)
:eq = Decimal.compare(left, right)
nil
rescue
_ ->
{left, right}
end
end
end