/
diff.ex
148 lines (115 loc) · 3.6 KB
/
diff.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
defmodule ExAudit.Diff do
@type addition :: {:added, term}
@type removal :: {:removed, term}
@type change :: {:changed, changes}
@type primitive_change :: {:primitive_change, removed :: term, added :: term}
@type list_addition :: {:added_to_list, index :: integer, term}
@type list_removal :: {:removed_from_list, index :: integer, term}
@type list_change :: {:changed_in_list, index :: integer, changes}
@type no_change :: :not_changed
@type changes ::
addition
| removal
| change
| list_addition
| list_removal
| list_change
| no_change
| primitive_change
| %{any: changes}
| [changes]
@undefined :"$undefined"
@doc """
Creates a patch that can be used to go from a to b with the ExAudit.Patch.patch function
"""
@spec diff(term, term) :: changes
def diff(a, b)
def diff(a, a) do
:not_changed
end
def diff(%{__struct__: a_struct} = a, %{__struct__: b_struct} = b) do
if primitive_struct?(a_struct) and primitive_struct?(b_struct) do
{:primitive_change, a, b}
else
diff(Map.from_struct(a), Map.from_struct(b))
end
end
def diff(%{} = a, %{} = b) do
all_keys =
(Map.keys(a) ++ Map.keys(b))
|> Enum.uniq()
changes =
Enum.map(all_keys, fn key ->
value_a = Map.get(a, key, @undefined)
value_b = Map.get(b, key, @undefined)
case {value_a, value_b} do
{a, a} ->
nil
{@undefined, b} ->
{key, {:added, b}}
{a, @undefined} ->
{key, {:removed, a}}
{a, b} ->
{key, {:changed, diff(a, b)}}
end
end)
|> Enum.reject(&is_nil/1)
case length(changes) do
0 -> :not_changed
_ -> Enum.into(changes, %{})
end
end
def diff(a, b) when is_list(a) and is_list(b) do
indexes = 0..:erlang.max(length(a) - 1, length(b) - 1)
changes =
for i <- indexes, into: [] do
value_a = Enum.at(a, i, @undefined)
value_b = Enum.at(b, i, @undefined)
case {value_a, value_b} do
{a, a} ->
nil
{@undefined, b} ->
{:added_to_list, i, b}
{a, @undefined} ->
{:removed_from_list, i, a}
{a, b} ->
{:changed_in_list, i, diff(a, b)}
end
end
changes = Enum.reject(changes, &is_nil/1)
case length(changes) do
0 -> :not_changed
_ -> changes
end
end
def diff(a, b) do
{:primitive_change, a, b}
end
@doc """
Reverts a patch so that it can undo a change
"""
@spec reverse(changes) :: changes
def reverse(:not_changed), do: :not_changed
def reverse({:primitive_change, a, b}), do: {:primitive_change, b, a}
def reverse({:added, a}), do: {:removed, a}
def reverse({:removed, a}), do: {:added, a}
def reverse({:changed, changes}), do: {:changed, reverse(changes)}
def reverse({:added_to_list, index, value}), do: {:removed_from_list, index, value}
def reverse({:removed_from_list, index, value}), do: {:added_to_list, index, value}
def reverse({:changed_in_list, index, changes}), do: {:changed_in_list, index, reverse(changes)}
def reverse(changes) when is_map(changes) do
changes
|> Enum.map(fn {key, change} -> {key, reverse(change)} end)
|> Enum.into(%{})
end
def reverse(changes) when is_list(changes) do
changes
|> Enum.reverse()
|> Enum.map(&reverse/1)
end
## PRIVATE
defp primitive_struct?(type) do
primitive_structs = Application.get_env(:ex_audit, :primitive_structs, [])
type in primitive_structs
end
end