Current section
Files
Jump to
Current section
Files
src/metamon@diff.erl
-module(metamon@diff).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/metamon/diff.gleam").
-export([diff/2, diff_string/2, render/1]).
-export_type([diff/0, indexed_diff/0, segment/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Structural diff for failure reports. Compares two values\n"
" element-by-element and produces a tree that the formatter can\n"
" render compactly.\n"
"\n"
" The implementation is type-erased: it inspects the dynamic shape\n"
" of each side via `string.inspect`. This is sufficient for the\n"
" failure-report use case (we only need to *display* the diff, not\n"
" reason about it programmatically), and avoids requiring users to\n"
" supply per-type formatters.\n"
).
-type diff() :: {same, binary()} |
{differ, binary(), binary()} |
{list_diff, list(indexed_diff())} |
{tuple_diff, list(indexed_diff())} |
{string_diff, binary(), binary(), list(segment())}.
-type indexed_diff() :: {indexed_diff, integer(), diff()}.
-type segment() :: {common, binary()} | {removed, binary()} | {added, binary()}.
-file("src/metamon/diff.gleam", 70).
-spec is_list_inspect(binary()) -> boolean().
is_list_inspect(S) ->
gleam_stdlib:string_starts_with(S, <<"["/utf8>>) andalso gleam_stdlib:string_ends_with(
S,
<<"]"/utf8>>
).
-file("src/metamon/diff.gleam", 74).
-spec is_tuple_inspect(binary()) -> boolean().
is_tuple_inspect(S) ->
gleam_stdlib:string_starts_with(S, <<"#("/utf8>>) andalso gleam_stdlib:string_ends_with(
S,
<<")"/utf8>>
).
-file("src/metamon/diff.gleam", 94).
-spec drop_tuple_prefix(binary()) -> binary().
drop_tuple_prefix(Inspected) ->
case gleam_stdlib:string_starts_with(Inspected, <<"#("/utf8>>) of
true ->
<<"("/utf8, (gleam@string:drop_start(Inspected, 2))/binary>>;
false ->
Inspected
end.
-file("src/metamon/diff.gleam", 101).
-spec zip_items(list(binary()), list(binary()), integer()) -> list(indexed_diff()).
zip_items(Left, Right, Index) ->
case {Left, Right} of
{[], []} ->
[];
{[], [R | Rs]} ->
[{indexed_diff, Index, {differ, <<"·"/utf8>>, R}} |
zip_items([], Rs, Index + 1)];
{[L | Ls], []} ->
[{indexed_diff, Index, {differ, L, <<"·"/utf8>>}} |
zip_items(Ls, [], Index + 1)];
{[L@1 | Ls@1], [R@1 | Rs@1]} ->
Inner = case L@1 =:= R@1 of
true ->
{same, L@1};
false ->
{differ, L@1, R@1}
end,
[{indexed_diff, Index, Inner} | zip_items(Ls@1, Rs@1, Index + 1)]
end.
-file("src/metamon/diff.gleam", 174).
-spec finish_split(binary(), list(binary())) -> list(binary()).
finish_split(Acc, Results) ->
case gleam@string:trim(Acc) of
<<""/utf8>> ->
lists:reverse(Results);
Trimmed ->
lists:reverse([Trimmed | Results])
end.
-file("src/metamon/diff.gleam", 154).
-spec walk_and_split(list(binary()), integer(), binary(), list(binary())) -> list(binary()).
walk_and_split(Graphemes, Depth, Acc, Results) ->
case Graphemes of
[] ->
finish_split(Acc, Results);
[First | Rest] ->
case {First, Depth} of
{<<"["/utf8>>, _} ->
walk_and_split(
Rest,
Depth + 1,
<<Acc/binary, First/binary>>,
Results
);
{<<"]"/utf8>>, D} when D > 0 ->
walk_and_split(
Rest,
D - 1,
<<Acc/binary, First/binary>>,
Results
);
{<<"("/utf8>>, _} ->
walk_and_split(
Rest,
Depth + 1,
<<Acc/binary, First/binary>>,
Results
);
{<<")"/utf8>>, D@1} when D@1 > 0 ->
walk_and_split(
Rest,
D@1 - 1,
<<Acc/binary, First/binary>>,
Results
);
{<<","/utf8>>, 0} ->
walk_and_split(
Rest,
0,
<<""/utf8>>,
[gleam@string:trim(Acc) | Results]
);
{_, _} ->
walk_and_split(
Rest,
Depth,
<<Acc/binary, First/binary>>,
Results
)
end
end.
-file("src/metamon/diff.gleam", 126).
-spec split_top_level(binary()) -> list(binary()).
split_top_level(Inspected) ->
Inner = case {gleam_stdlib:string_starts_with(Inspected, <<"["/utf8>>),
gleam_stdlib:string_ends_with(Inspected, <<"]"/utf8>>)} of
{true, true} ->
_pipe = Inspected,
_pipe@1 = gleam@string:drop_start(_pipe, 1),
gleam@string:drop_end(_pipe@1, 1);
{_, _} ->
case {gleam_stdlib:string_starts_with(Inspected, <<"("/utf8>>),
gleam_stdlib:string_ends_with(Inspected, <<")"/utf8>>)} of
{true, true} ->
_pipe@2 = Inspected,
_pipe@3 = gleam@string:drop_start(_pipe@2, 1),
gleam@string:drop_end(_pipe@3, 1);
{_, _} ->
Inspected
end
end,
walk_and_split(gleam@string:to_graphemes(Inner), 0, <<""/utf8>>, []).
-file("src/metamon/diff.gleam", 78).
-spec list_diff_from_inspect(binary(), binary()) -> diff().
list_diff_from_inspect(Left, Right) ->
L_items = split_top_level(Left),
R_items = split_top_level(Right),
{list_diff, zip_items(L_items, R_items, 0)}.
-file("src/metamon/diff.gleam", 84).
-spec tuple_diff_from_inspect(binary(), binary()) -> diff().
tuple_diff_from_inspect(Left, Right) ->
L_inner = drop_tuple_prefix(Left),
R_inner = drop_tuple_prefix(Right),
L_items = split_top_level(L_inner),
R_items = split_top_level(R_inner),
{tuple_diff, zip_items(L_items, R_items, 0)}.
-file("src/metamon/diff.gleam", 55).
-spec shape_aware_diff(GFJ, GFJ) -> diff().
shape_aware_diff(Left, Right) ->
L_str = gleam@string:inspect(Left),
R_str = gleam@string:inspect(Right),
case {is_list_inspect(L_str),
is_list_inspect(R_str),
is_tuple_inspect(L_str),
is_tuple_inspect(R_str)} of
{true, true, _, _} ->
list_diff_from_inspect(L_str, R_str);
{_, _, true, true} ->
tuple_diff_from_inspect(L_str, R_str);
{_, _, _, _} ->
{differ, L_str, R_str}
end.
-file("src/metamon/diff.gleam", 48).
?DOC(
" Compute a structural diff between two values. Equal inputs collapse\n"
" to `Same`; otherwise the result depends on whether\n"
" `string.inspect` exposes list-shaped or string-shaped output.\n"
).
-spec diff(GFI, GFI) -> diff().
diff(Left, Right) ->
case Left =:= Right of
true ->
{same, gleam@string:inspect(Left)};
false ->
shape_aware_diff(Left, Right)
end.
-file("src/metamon/diff.gleam", 234).
-spec render_string_diff(list(segment()), binary()) -> binary().
render_string_diff(Segments, Indent) ->
_pipe = gleam@list:map(Segments, fun(Segment) -> case Segment of
{common, Text} ->
<<<<Indent/binary, " "/utf8>>/binary, Text/binary>>;
{removed, Text@1} ->
<<<<Indent/binary, "- "/utf8>>/binary, Text@1/binary>>;
{added, Text@2} ->
<<<<Indent/binary, "+ "/utf8>>/binary, Text@2/binary>>
end end),
gleam@string:join(_pipe, <<"\n"/utf8>>).
-file("src/metamon/diff.gleam", 278).
-spec empty_row(list(binary())) -> list(integer()).
empty_row(Right) ->
[0 | gleam@list:map(Right, fun(_) -> 0 end)].
-file("src/metamon/diff.gleam", 310).
-spec max_int(integer(), integer()) -> integer().
max_int(A, B) ->
case A >= B of
true ->
A;
false ->
B
end.
-file("src/metamon/diff.gleam", 282).
-spec build_row(
binary(),
list(binary()),
list(integer()),
list(integer()),
integer()
) -> list(integer()).
build_row(L, Remaining, Prev_row, Acc, Current_left_value) ->
case {Remaining, Prev_row} of
{[], _} ->
lists:reverse([Current_left_value | Acc]);
{[R | Rs], [_ | Rest_prev]} ->
Above = case Rest_prev of
[V | _] ->
V;
[] ->
0
end,
Diagonal = case Prev_row of
[V@1 | _] ->
V@1;
[] ->
0
end,
Value = case L =:= R of
true ->
Diagonal + 1;
false ->
max_int(Above, Current_left_value)
end,
build_row(L, Rs, Rest_prev, [Current_left_value | Acc], Value);
{_, []} ->
lists:reverse([Current_left_value | Acc])
end.
-file("src/metamon/diff.gleam", 257).
-spec build_rows(list(binary()), list(binary()), list(list(integer()))) -> list(list(integer())).
build_rows(Remaining_left, Right, Acc) ->
case Remaining_left of
[] ->
Row_for_empty = gleam@list:map(Right, fun(_) -> 0 end),
lists:reverse([[0 | Row_for_empty] | Acc]);
[First | Rest] ->
Prev_row = case Acc of
[] ->
empty_row(Right);
[Most_recent | _] ->
Most_recent
end,
Row = build_row(First, Right, Prev_row, [], 0),
build_rows(Rest, Right, [Row | Acc])
end.
-file("src/metamon/diff.gleam", 253).
-spec build_lcs(list(binary()), list(binary())) -> list(list(integer())).
build_lcs(Left, Right) ->
build_rows(Left, Right, []).
-file("src/metamon/diff.gleam", 317).
-spec backtrack_diff(list(binary()), list(binary()), list(list(integer()))) -> list(segment()).
backtrack_diff(Left, Right, _) ->
case {Left, Right} of
{[], []} ->
[];
{[], [R | Rs]} ->
[{added, R} | backtrack_diff([], Rs, [])];
{[L | Ls], []} ->
[{removed, L} | backtrack_diff(Ls, [], [])];
{[L@1 | Ls@1], [R@1 | Rs@1]} ->
case L@1 =:= R@1 of
true ->
[{common, L@1} | backtrack_diff(Ls@1, Rs@1, [])];
false ->
[{removed, L@1},
{added, R@1} |
backtrack_diff(Ls@1, Rs@1, [])]
end
end.
-file("src/metamon/diff.gleam", 185).
?DOC(
" Multi-line string diff using a longest-common-subsequence (LCS) on\n"
" lines. Lines unique to the left side are shown as `Removed`, lines\n"
" unique to the right are shown as `Added`, lines present in both\n"
" are shown as `Common`.\n"
).
-spec diff_string(binary(), binary()) -> diff().
diff_string(Left, Right) ->
case Left =:= Right of
true ->
{same, Left};
false ->
L_lines = gleam@string:split(Left, <<"\n"/utf8>>),
R_lines = gleam@string:split(Right, <<"\n"/utf8>>),
Lcs_table = build_lcs(L_lines, R_lines),
Segments = backtrack_diff(L_lines, R_lines, Lcs_table),
{string_diff, Left, Right, Segments}
end.
-file("src/metamon/diff.gleam", 214).
-spec render_list_diff(list(indexed_diff()), binary(), binary()) -> binary().
render_list_diff(Items, Indent, Prefix) ->
Close = case Prefix of
<<"#."/utf8>> ->
<<""/utf8>>;
_ ->
<<"]"/utf8>>
end,
_pipe = gleam@list:map(
Items,
fun(Item) ->
<<<<<<<<<<Indent/binary, Prefix/binary>>/binary,
(erlang:integer_to_binary(erlang:element(2, Item)))/binary>>/binary,
Close/binary>>/binary,
"\n"/utf8>>/binary,
(render_with_indent(
erlang:element(3, Item),
<<Indent/binary, " "/utf8>>
))/binary>>
end
),
gleam@string:join(_pipe, <<"\n"/utf8>>).
-file("src/metamon/diff.gleam", 203).
-spec render_with_indent(diff(), binary()) -> binary().
render_with_indent(D, Indent) ->
case D of
{same, Rendered} ->
<<<<Indent/binary, "= "/utf8>>/binary, Rendered/binary>>;
{differ, Left, Right} ->
<<<<<<<<<<<<Indent/binary, "- "/utf8>>/binary, Left/binary>>/binary,
"\n"/utf8>>/binary,
Indent/binary>>/binary,
"+ "/utf8>>/binary,
Right/binary>>;
{list_diff, Items} ->
render_list_diff(Items, Indent, <<"[#"/utf8>>);
{tuple_diff, Items@1} ->
render_list_diff(Items@1, Indent, <<"#."/utf8>>);
{string_diff, _, _, Segments} ->
render_string_diff(Segments, Indent)
end.
-file("src/metamon/diff.gleam", 199).
?DOC(" Render a `Diff` as a human-readable multi-line string.\n").
-spec render(diff()) -> binary().
render(D) ->
render_with_indent(D, <<""/utf8>>).