Current section
Files
Jump to
Current section
Files
lib/binding_protocol.ex
defprotocol ExMatch.BindingProtocol do
@moduledoc false
@fallback_to_any true
@spec diff(t, any, any) :: [any] | {any, any}
def diff(left, right, opts)
@spec escape(t) :: any
def escape(self)
@spec value(t) :: any
def value(self)
end
defimpl ExMatch.BindingProtocol, for: Any do
@moduledoc false
def diff(left, right, opts) do
case ExMatch.Protocol.diff(left, right, opts) do
nil ->
[]
{left_diff, right_diff} ->
{Macro.escape(left_diff), right_diff}
end
end
# def diff(value, value, _), do: []
# 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 ->
# []
# {left_map, right_map} ->
# {
# Map.put(left_map, :__struct__, struct),
# Map.put(right_map, :__struct__, struct)
# }
# end
# end
# def diff(left, right = %_{}, opts) do
# case ExMatch.Protocol.diff(right, left, opts) do
# nil -> []
# {right_result, left_result} -> {left_result, right_result}
# end
# end
# def diff(left, right, _),
# do: {escape(left), right}
def escape(self),
do: Macro.escape(self)
def value(self),
do: self
end
defmodule ExMatch.Expr do
@moduledoc false
defstruct [:ast, :value]
# pin variable
def parse({:^, _, [{var_name, _, module} = var_item]} = ast)
when is_atom(var_name) and is_atom(module),
do: parse(ast, var_item)
# remote function/macro call
def parse({{:., _, [{:__aliases__, _, [module_alias | _]}, fn_name]}, _, args} = ast)
when is_atom(module_alias) and is_atom(fn_name) and is_list(args),
do: parse(ast, ast)
# local/imported function/macro call
def parse({fn_name, _, args} = ast) when is_atom(fn_name) and is_list(args) do
if Macro.special_form?(fn_name, length(args)) do
raise "Special form #{fn_name}/#{length(args)} is not yet supported in ExMatch"
end
parse(ast, ast)
end
defp parse(ast, value) do
self =
quote do
%ExMatch.Expr{
ast: unquote(Macro.escape(ast)),
value: unquote(value)
}
end
{[], self}
end
defimpl ExMatch.BindingProtocol do
@moduledoc false
def diff(left, right, opts) do
%ExMatch.Expr{ast: ast, value: value} = left
case ExMatch.Protocol.diff(value, right, opts) do
{^value, right_diff} ->
{escape(left), right_diff}
{left_diff, right_diff} ->
left_diff = {:=~, [], [ast, Macro.escape(left_diff)]}
{left_diff, right_diff}
nil ->
[]
end
end
def escape(%ExMatch.Expr{ast: ast, value: value}) do
code =
ast
|> Code.quoted_to_algebra()
|> Inspect.Algebra.format(:infinity)
|> IO.iodata_to_binary()
if code == inspect(value) do
ast
else
{:=, [], [ast, Macro.escape(value)]}
end
end
def value(%ExMatch.Expr{value: value}),
do: value
end
end
defmodule ExMatch.Var do
@moduledoc false
defstruct [:ast]
def parse({var, _, nil} = ast) when is_atom(var) do
self =
quote do
%ExMatch.Var{ast: unquote(Macro.escape(ast))}
end
{[ast], self}
end
defimpl ExMatch.BindingProtocol do
@moduledoc false
def diff(_left, right, _opts) do
[right]
end
def escape(%ExMatch.Var{ast: ast}),
do: ast
def value(_self),
do: raise(ArgumentError, "Bindings don't represent values")
end
end
defmodule ExMatch.List do
@moduledoc false
defstruct [:items]
def parse(list, parse_ast) do
{bindings, parsed} = parse_items(list, [], [], parse_ast)
self =
quote do
%ExMatch.List{items: unquote(parsed)}
end
{bindings, self}
end
def parse_items([item | list], bindings, parsed, parse_ast) do
{item_bindings, item_parsed} = parse_ast.(item)
bindings = item_bindings ++ bindings
parsed = [item_parsed | parsed]
parse_items(list, bindings, parsed, parse_ast)
end
def parse_items([], bindings, parsed, _) do
{bindings, Enum.reverse(parsed)}
end
def diff([item | items], bindings, left_diffs, right_diffs, right, opts) do
case right do
[right_item | right] ->
case ExMatch.BindingProtocol.diff(item, right_item, opts) do
new_bindings when is_list(new_bindings) ->
bindings = new_bindings ++ bindings
diff(items, bindings, left_diffs, right_diffs, right, opts)
{left_diff, right_diff} ->
left_diffs = [left_diff | left_diffs]
right_diffs = [right_diff | right_diffs]
diff(items, bindings, left_diffs, right_diffs, right, opts)
end
[] ->
items = escape_items([item | items])
{Enum.reverse(left_diffs, items), Enum.reverse(right_diffs)}
end
end
def diff([], bindings, [], [], _right, _opts), do: bindings
def diff([], _bindings, left_diffs, right_diffs, right, _opts) do
{Enum.reverse(left_diffs), Enum.reverse(right_diffs, right)}
end
def escape_items(items) do
Enum.map(items, &ExMatch.BindingProtocol.escape/1)
end
def value(items) do
Enum.map(items, &ExMatch.BindingProtocol.value/1)
end
defimpl ExMatch.BindingProtocol do
@moduledoc false
def diff(left, right, opts) when is_list(right) do
%ExMatch.List{items: items} = left
ExMatch.List.diff(items, [], [], [], right, opts)
end
def diff(left, right, _) do
{escape(left), right}
end
def escape(%ExMatch.List{items: items}),
do: ExMatch.List.escape_items(items)
def value(%ExMatch.List{items: items}),
do: ExMatch.List.value(items)
end
end
defmodule ExMatch.Tuple do
@moduledoc false
defstruct [:items]
def parse({:{}, _, items}, parse_ast), do: parse_items(items, parse_ast)
def parse({item1, item2}, parse_ast), do: parse_items([item1, item2], parse_ast)
defp parse_items(items, parse_ast) do
{bindings, parsed} = ExMatch.List.parse_items(items, [], [], parse_ast)
self =
quote do
%ExMatch.Tuple{items: unquote(parsed)}
end
{bindings, self}
end
defimpl ExMatch.BindingProtocol do
@moduledoc false
def diff(left, right, opts) when is_tuple(right) do
%ExMatch.Tuple{items: items} = left
case ExMatch.List.diff(items, [], [], [], Tuple.to_list(right), opts) do
{left_diffs, right_diffs} ->
right_diffs = List.to_tuple(right_diffs)
{{:{}, [], left_diffs}, right_diffs}
bindings ->
bindings
end
end
def diff(left, right, _opts) do
{escape(left), right}
end
def escape(%ExMatch.Tuple{items: [i1, i2]}),
do: {ExMatch.BindingProtocol.escape(i1), ExMatch.BindingProtocol.escape(i2)}
def escape(%ExMatch.Tuple{items: items}),
do: {:{}, [], ExMatch.List.escape_items(items)}
def value(%ExMatch.Tuple{items: items}),
do:
items
|> ExMatch.List.value()
|> List.to_tuple()
end
end
defmodule ExMatch.Map do
@moduledoc false
@enforce_keys [:partial, :fields]
defstruct @enforce_keys
def parse({:%{}, _, fields}, parse_ast) do
{partial, bindings, parsed} = parse_fields(fields, parse_ast)
self =
quote do
%ExMatch.Map{
partial: unquote(partial),
fields: unquote(parsed)
}
end
{bindings, self}
end
def parse_fields(fields, parse_ast) do
{partial, bindings, parsed, _parse_ast} =
Enum.reduce(fields, {false, [], [], parse_ast}, &parse_field/2)
{partial, bindings, Enum.reverse(parsed)}
end
defp parse_field({:..., _, nil}, {_partial, bindings, parsed, parse_ast}) do
{true, bindings, parsed, parse_ast}
end
defp parse_field({key, value}, {partial, bindings, parsed, parse_ast}) do
{value_bindings, value_parsed} = parse_ast.(value)
parsed = [{key, value_parsed} | parsed]
bindings = value_bindings ++ bindings
{partial, bindings, parsed, parse_ast}
end
def diff_items(fields, right, opts) do
{bindings, left_diffs, right_diffs, right, _opts} =
Enum.reduce(fields, {[], [], %{}, right, opts}, &diff_item/2)
{bindings, Enum.reverse(left_diffs), right_diffs, right}
end
defp diff_item({key, field}, {bindings, left_diffs, right_diffs, right, opts}) do
case right do
%{^key => right_value} ->
right = Map.delete(right, key)
case ExMatch.BindingProtocol.diff(field, right_value, opts) do
{left_diff, right_diff} ->
left_diffs = [{ExMatch.BindingProtocol.escape(key), left_diff} | left_diffs]
right_diffs = Map.put(right_diffs, key, right_diff)
{bindings, left_diffs, right_diffs, right, opts}
new_bindings ->
bindings = new_bindings ++ bindings
{bindings, left_diffs, right_diffs, right, opts}
end
_ ->
left_diff = {
ExMatch.BindingProtocol.escape(key),
ExMatch.BindingProtocol.escape(field)
}
left_diffs = [left_diff | left_diffs]
{bindings, left_diffs, right_diffs, right, opts}
end
end
def field_values(fields),
do:
Enum.map(fields, fn {key, value} ->
{
ExMatch.BindingProtocol.value(key),
ExMatch.BindingProtocol.value(value)
}
end)
defimpl ExMatch.BindingProtocol do
@moduledoc false
def diff(left, right, opts) when is_map(right) do
%ExMatch.Map{partial: partial, fields: fields} = left
case ExMatch.Map.diff_items(fields, right, opts) do
{bindings, left_diffs, right_diffs, right}
when left_diffs == [] and
right_diffs == %{} and
(partial or right == %{}) ->
bindings
{_bindings, left_diffs, right_diffs, right} ->
right_diffs =
if partial do
right_diffs
else
Map.merge(right_diffs, right)
end
left_diffs = {:%{}, [], left_diffs}
{left_diffs, right_diffs}
end
end
def diff(left, right, _opts) do
{escape(left), right}
end
def escape(%ExMatch.Map{fields: fields} = left) do
fields =
Enum.map(fields, fn {key, value} ->
{
ExMatch.BindingProtocol.escape(key),
ExMatch.BindingProtocol.escape(value)
}
end)
{:%{}, [], fields ++ and_partial_ast(left)}
end
defp and_partial_ast(%ExMatch.Map{partial: partial}) do
if partial do
[quote(do: ...)]
else
[]
end
end
def value(%ExMatch.Map{partial: true}),
do: raise(ArgumentError, "partial map doesn't represent a value")
def value(%ExMatch.Map{fields: fields}),
do: fields |> ExMatch.Map.field_values() |> Map.new()
end
end
defmodule ExMatch.Struct do
@moduledoc false
defmodule WithValue do
defstruct [:module, :fields, :value]
defimpl ExMatch.BindingProtocol do
@moduledoc false
def escape(%WithValue{module: module, fields: fields}),
do: ExMatch.Struct.escape(module, fields, false)
def value(%WithValue{value: value}),
do: value
def diff(left, right, opts) do
%WithValue{module: module, fields: fields, value: value} = left
case ExMatch.Protocol.diff(value, right, opts) do
nil -> []
{_, _} -> ExMatch.Struct.diff(module, fields, false, right, opts)
end
end
end
end
defmodule NoValue do
defstruct [:module, :fields, :partial]
defimpl ExMatch.BindingProtocol do
@moduledoc false
def escape(%NoValue{module: module, fields: fields, partial: partial}),
do: ExMatch.Struct.escape(module, fields, partial)
def value(%NoValue{}),
do: raise("This struct doesn't have value")
def diff(left, right, opts) do
%NoValue{module: module, fields: fields, partial: partial} = left
ExMatch.Struct.diff(module, fields, partial, right, opts)
end
end
end
def parse(
{:%, _, [module, {:%{}, _, fields}]},
parse_ast
)
when is_list(fields) do
{partial, bindings, parsed} = ExMatch.Map.parse_fields(fields, parse_ast)
self =
quote do
ExMatch.Struct.new(
unquote(module),
unquote(parsed),
unquote(partial)
)
end
{bindings, self}
end
def new(module, fields, partial) do
if partial do
raise ArgumentError
end
value = struct!(module, ExMatch.Map.field_values(fields))
fields =
value
|> Map.from_struct()
|> Enum.map(fn {key, value} ->
{key, Macro.escape(value)}
end)
|> Keyword.merge(fields, fn _, _, field -> field end)
%WithValue{
module: module,
fields: fields,
value: value
}
rescue
ArgumentError ->
%NoValue{
module: module,
fields: fields,
partial: partial
}
end
def diff(module, fields, partial, %rstruct{} = right, opts) do
map = %ExMatch.Map{fields: fields, partial: partial}
right_map = Map.from_struct(right)
case ExMatch.BindingProtocol.ExMatch.Map.diff(map, right_map, opts) do
{left_diff, right_diff} ->
right_diff = Map.put(right_diff, :__struct__, rstruct)
try do
_ = inspect(right_diff, safe: false)
{{:%, [], [module, left_diff]}, right_diff}
rescue
_ ->
{escape(module, fields, partial), right}
end
bindings ->
bindings
end
end
def diff(module, fields, partial, right, _opts) do
{escape(module, fields, partial), right}
end
def escape(module, fields, partial) do
map = %ExMatch.Map{
partial: partial,
fields: fields
}
map = ExMatch.BindingProtocol.ExMatch.Map.escape(map)
{:%, [], [module, map]}
end
end