defprotocol ExMatch.Protocol do @fallback_to_any true @spec diff(t, any, (atom -> any)) :: nil | {left :: any, right :: any} def diff(left, right, get_opts) end defimpl ExMatch.Protocol, for: Any do def diff(value, value, _), do: nil def diff(left = %struct{}, right = %struct{}, get_opts) do case ExMatch.Protocol.Map.diff( left |> Map.from_struct(), right |> Map.from_struct(), get_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 = %_{}, get_opts) do with impl = ExMatch.Protocol.impl_for(right), false <- impl == ExMatch.Protocol.Any, {right_result, left_result} <- ExMatch.Protocol.diff(right, left, get_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], get_opts) do this_diff = ExMatch.Protocol.diff(left_value, right_value, get_opts) rest_diff = diff(left, right, get_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, _) do {left, right} end end defimpl ExMatch.Protocol, for: Tuple do def diff(left, right, get_opts) when is_tuple(right) do left = Tuple.to_list(left) right = Tuple.to_list(right) case ExMatch.Protocol.List.diff(left, right, get_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, get_opts) when is_map(right) do case diff_items(left, right, get_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, get_opts) do Enum.reduce(left, {%{}, %{}, right}, &diff_item(&1, &2, get_opts)) end defp diff_item({key, field}, {left_diffs, right_diffs, right}, get_opts) do case right do %{^key => right_value} -> right = Map.delete(right, key) case ExMatch.Protocol.diff(field, right_value, get_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} nil -> {left_diffs, right_diffs, right} end _ -> left_diffs = Map.put(left_diffs, key, field) {left_diffs, right_diffs, right} end end end defimpl ExMatch.Protocol, for: DateTime do def diff(left, right, get_opts) when is_binary(right) do opts = get_opts.(DateTime) || [] case :match_string in opts and DateTime.from_iso8601(right) do {:ok, right_date, _} -> diff_dates(left, right, right_date) _ -> {left, right} end end def diff(left, right = %DateTime{}, _) do diff_dates(left, right, right) end def diff(left, right, _) do {left, right} end defp 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, get_opts) do opts = get_opts.(Decimal) || [] if is_float(right) or (is_binary(right) and :match_string not in opts) or (is_integer(right) and :match_integer not in opts) do {left, right} else parse_and_diff(left, right) end end defp parse_and_diff(left, right) do Decimal.new(right) :eq = Decimal.compare(left, right) nil catch _, _ -> {left, right} end end end