Skip to content
Permalink
Browse files
Merge pull request #71 from bartekgorny/format_maps
Trim output for maps
  • Loading branch information
ferd committed May 7, 2019
2 parents 75d70c7 + b1ccc7b commit 40f39a2b8f5efdce93ccb48841e325ca56808c56
Showing 3 changed files with 243 additions and 25 deletions.
@@ -0,0 +1,200 @@
%%%-------------------------------------------------------------------
%%% @author bartlomiej.gorny@erlang-solutions.com
%%% @doc
%%% This module handles formatting maps.
%% It allows for trimming output to selected fields, or to nothing at all. It also adds a label
%% to a printout.
%% To set up a limit for a map, you need to give recon a way to tell the map you want to
%% trim from all the other maps, so you have to provide something like a 'type definition'.
%% It can be either another map which is compared to the arg, or a fun.
%%% @end
%%%-------------------------------------------------------------------
-module(recon_map).
-author("bartlomiej.gorny@erlang-solutions.com").
%% API

-export([limit/3]).
-export([list/0]).
-export([process_map/1]).
-export([is_active/0]).
-export([clear/0]).
-export([remove/1, rename/2]).

-type map_label() :: atom().
-type pattern() :: map() | function().
-type limit() :: all | none | atom() | binary() | [any()].

%% @doc quickly check if we want to do any record formatting
-spec is_active() -> boolean().
is_active() ->
case whereis(recon_ets_maps) of
undefined -> false;
_ -> true
end.

%% @doc remove all imported definitions, destroy the table, clean up
clear() ->
maybe_kill(recon_ets_maps),
ok.

%% @doc Limit output to selected keys of a map (can be 'none', 'all', a key or a list of keys).
%% Pattern selects maps to process: a "pattern" is just a map, and if all key/value pairs of a pattern
%% are present in a map (in other words, the pattern is a subset), then we say the map matches
%% and we process it accordingly (apply the limit).
%%
%% Patterns are applied in alphabetical order, until a match is found.
%%
%% Instead of a pattern you can also provide a function which will take a map and return a boolean.
%% @end
-spec limit(map_label(), pattern(), limit()) -> ok | {error, any()}.
limit(Label, #{} = Pattern, Limit) when is_atom(Label) ->
store_pattern(Label, Pattern, Limit);
limit(Label, Pattern, Limit) when is_atom(Label), is_function(Pattern) ->
store_pattern(Label, Pattern, Limit).

list() ->
ensure_table_exists(),
io:format("~nmap definitions and limits:~n"),
list(lists:sort(ets:tab2list(patterns_table_name()))).

remove(Label) ->
ensure_table_exists(),
ets:delete(patterns_table_name(), Label).

rename(Name, NewName) ->
ensure_table_exists(),
case ets:lookup(patterns_table_name(), Name) of
[{Name, Pattern, Limit}] ->
ets:delete(patterns_table_name(), Name),
ets:insert(patterns_table_name(), {NewName, Pattern, Limit}),
renamed;
[] ->
missing
end.

list([]) ->
io:format("~n"),
ok;
list([{Label, Pattern, Limit} | Rest]) ->
io:format("~p: ~p -> ~p~n", [Label, Pattern, Limit]),
list(Rest).

%% @doc given a map, scans saved patterns for one that matches; if found, returns a label
%% and a map with limits applied; otherwise returns 'none' and original map.
%% Pattern can be:
%% <ul>
%% <li> a map - then each key in pattern is checked for equality with the map in question</li>
%% <li> a fun(map()) -> boolean()</li>
%% </ul>
-spec process_map(map()) -> map() | {atom(), map()}.
process_map(M) ->
process_map(M, ets:tab2list(patterns_table_name())).

process_map(M, []) ->
M;
process_map(M, [{Label, Pattern, Limit} | Rest]) ->
case map_matches(M, Pattern) of
true ->
{Label, apply_map_limits(Limit, M)};
false ->
process_map(M, Rest)
end.

map_matches(#{} = M, Pattern) when is_function(Pattern) ->
Pattern(M);
map_matches(_, []) ->
true;
map_matches(M, [{K, V} | Rest]) ->
case maps:is_key(K, M) of
true ->
case maps:get(K, M) of
V ->
map_matches(M, Rest);
_ ->
false
end;
false ->
false
end.

apply_map_limits(none, M) ->
M;
apply_map_limits(all, _) ->
#{};
apply_map_limits(Fields, M) ->
maps:with(Fields, M).

patterns_table_name() -> recon_map_patterns.

store_pattern(Label, Pattern, Limit) ->
ensure_table_exists(),
ets:insert(patterns_table_name(), {Label, prepare_pattern(Pattern), prepare_limit(Limit)}),
ok.

prepare_limit(all) -> all;
prepare_limit(none) -> none;
prepare_limit(Limit) when is_binary(Limit) -> [Limit];
prepare_limit(Limit) when is_atom(Limit) -> [Limit];
prepare_limit(Limit) when is_list(Limit) -> Limit.

prepare_pattern(Pattern) when is_function(Pattern) -> Pattern;
prepare_pattern(Pattern) when is_map(Pattern) -> maps:to_list(Pattern).


ensure_table_exists() ->
case ets:info(patterns_table_name()) of
undefined ->
case whereis(recon_ets_maps) of
undefined ->
Parent = self(),
Ref = make_ref(),
%% attach to the currently running session
{Pid, MonRef} = spawn_monitor(fun() ->
register(recon_ets_maps, self()),
ets:new(patterns_table_name(), [ordered_set, public, named_table]),
Parent ! Ref,
ets_keeper()
end),
receive
Ref ->
erlang:demonitor(MonRef, [flush]),
Pid;
{'DOWN', MonRef, _, _, Reason} ->
error(Reason)
end;
Pid ->
Pid
end;
Pid ->
Pid
end.

ets_keeper() ->
receive
stop -> ok;
_ -> ets_keeper()
end.

%%%%%%%%%%%%%%%
%%% HELPERS %%%
%%%%%%%%%%%%%%%

maybe_kill(Name) ->
case whereis(Name) of
undefined ->
ok;
Pid ->
unlink(Pid),
exit(Pid, kill),
wait_for_death(Pid, Name)
end.

wait_for_death(Pid, Name) ->
case is_process_alive(Pid) orelse whereis(Name) =:= Pid of
true ->
timer:sleep(10),
wait_for_death(Pid, Name);
false ->
ok
end.

@@ -16,6 +16,7 @@

-export([is_active/0]).
-export([import/1, format_tuple/1, clear/1, clear/0, list/0, get_list/0, limit/3]).
-export([ensure_table_exists/0]).

-ifdef(TEST).
-export([lookup_record/2]).
@@ -54,7 +55,7 @@ is_active() ->

%% @doc remove definitions imported from a module.
clear(Module) ->
lists:map(fun(R) -> rem_for_module(R, Module) end, ets:tab2list(ets_table_name())).
lists:map(fun(R) -> rem_for_module(R, Module) end, ets:tab2list(records_table_name())).

%% @doc remove all imported definitions, destroy the table, clean up
clear() ->
@@ -80,20 +81,20 @@ list() ->
-spec get_list() -> [listentry()].
get_list() ->
ensure_table_exists(),
Lst = lists:map(fun make_list_entry/1, ets:tab2list(ets_table_name())),
Lst = lists:map(fun make_list_entry/1, ets:tab2list(records_table_name())),
lists:sort(Lst).

%% @doc Limit output to selected fields of a record (can be 'none', 'all', a field or a list of fields).
%% Limit set to 'none' means there is no limit, and all fields are displayed; limit 'all' means that
%% all fields are squashed and only record name will be shown.
%% @end
-spec limit(record_name(), arity(), limit()) -> ok | {error, record_unknown}.
limit(Name, Arity, Limit) ->
-spec limit(record_name(), arity(), limit()) -> ok | {error, any()}.
limit(Name, Arity, Limit) when is_atom(Name), is_integer(Arity) ->
case lookup_record(Name, Arity) of
[] ->
{error, record_unknown};
[{Key, Fields, Mod, _}] ->
ets:insert(ets_table_name(), {Key, Fields, Mod, Limit}),
ets:insert(records_table_name(), {Key, Fields, Mod, Limit}),
ok
end.

@@ -127,10 +128,10 @@ store_record(Rec, Module, ResultList) ->
Arity = length(Fields),
Result = case lookup_record(Name, Arity) of
[] ->
ets:insert(ets_table_name(), rec_info(Rec, Module)),
ets:insert(records_table_name(), rec_info(Rec, Module)),
{imported, Module, Name, Arity};
[{_, _, Module, _}] ->
ets:insert(ets_table_name(), rec_info(Rec, Module)),
ets:insert(records_table_name(), rec_info(Rec, Module)),
{overwritten, Module, Name, Arity};
[{_, _, Mod, _}] ->
{ignored, Module, Name, Arity, Mod}
@@ -148,10 +149,10 @@ get_record(_, Acc) -> Acc.
%% @private
lookup_record(RecName, FieldCount) ->
ensure_table_exists(),
ets:lookup(ets_table_name(), {RecName, FieldCount}).
ets:lookup(records_table_name(), {RecName, FieldCount}).

ensure_table_exists() ->
case ets:info(ets_table_name()) of
case ets:info(records_table_name()) of
undefined ->
case whereis(recon_ets) of
undefined ->
@@ -160,7 +161,7 @@ ensure_table_exists() ->
%% attach to the currently running session
{Pid, MonRef} = spawn_monitor(fun() ->
register(recon_ets, self()),
ets:new(ets_table_name(), [set, public, named_table]),
ets:new(records_table_name(), [set, public, named_table]),
Parent ! Ref,
ets_keeper()
end),
@@ -178,13 +179,13 @@ ensure_table_exists() ->
Pid
end.

ets_table_name() -> recon_record_definitions.
records_table_name() -> recon_record_definitions.

rec_info({Name, Fields}, Module) ->
{{Name, length(Fields)}, field_names(Fields), Module, none}.

rem_for_module({_, _, Module, _} = Rec, Module) ->
ets:delete_object(ets_table_name(), Rec);
ets:delete_object(records_table_name(), Rec);
rem_for_module(_, _) ->
ok.

@@ -185,7 +185,7 @@
-export([format/1]).

%% Internal exports
-export([count_tracer/1, rate_tracer/2, formatter/5, format_trace_output/2]).
-export([count_tracer/1, rate_tracer/2, formatter/5, format_trace_output/1, format_trace_output/2]).

-type matchspec() :: [{[term()], [term()], [term()]}].
-type shellfun() :: fun((_) -> term()).
@@ -607,36 +607,53 @@ to_hms(_) ->
format_args(Arity) when is_integer(Arity) ->
[$/, integer_to_list(Arity)];
format_args(Args) when is_list(Args) ->
Active = recon_rec:is_active(),
[$(, join(", ", [format_trace_output(Active, Arg) || Arg <- Args]), $)].
[$(, join(", ", [format_trace_output(Arg) || Arg <- Args]), $)].


%% @doc formats call arguments and return values - most types are just printed out, except for
%% tuples recognised as records, which mimic the source code syntax
%% @end
format_trace_output(Args) ->
format_trace_output(recon_rec:is_active(), Args).
format_trace_output(recon_rec:is_active(), recon_map:is_active(), Args).

format_trace_output(true, Args) when is_tuple(Args) ->
recon_rec:format_tuple(Args);
format_trace_output(true, Args) when is_list(Args) ->
format_trace_output(Recs, Args) ->
format_trace_output(Recs, recon_map:is_active(), Args).

format_trace_output(true, _, Args) when is_tuple(Args) ->
case sets:is_set(Args) of
true ->
["set:", format_trace_output(true, sets:to_list(Args))];
false ->
recon_rec:format_tuple(Args)
end;
format_trace_output(true, Maps, Args) when is_list(Args) ->
case io_lib:printable_list(Args) of
true ->
io_lib:format("~p", [Args]);
false ->
L = lists:map(fun(A) -> format_trace_output(true, A) end, Args),
L = lists:map(fun(A) -> format_trace_output(true, Maps, A) end, Args),
[$[, join(", ", L), $]]
end;
format_trace_output(true, Args) when is_map(Args) ->
format_trace_output(Recs, true, Args) when is_map(Args) ->
{Label, Map} = case recon_map:process_map(Args) of
{L, M} -> {atom_to_list(L), M};
M -> {"", M}
end,
ItemList = maps:to_list(Map),
[Label,
"#{",
join(", ", [format_kv(Recs, true, Key, Val) || {Key, Val} <- ItemList]),
"}"];
format_trace_output(Recs, false, Args) when is_map(Args) ->
ItemList = maps:to_list(Args),
["#{",
join(", ", [format_kv(Key, Val) || {Key, Val} <- ItemList]),
join(", ", [format_kv(Recs, false, Key, Val) || {Key, Val} <- ItemList]),
"}"];
format_trace_output(_, Args) ->
format_trace_output(_, _, Args) ->
io_lib:format("~p", [Args]).

format_kv(Key, Val) ->
[format_trace_output(true, Key), "=", format_trace_output(true, Val)].
format_kv(Recs, Maps, Key, Val) ->
[format_trace_output(Recs, Maps, Key), "=>", format_trace_output(Recs, Maps, Val)].

%%%%%%%%%%%%%%%
%%% HELPERS %%%

0 comments on commit 40f39a2

Please sign in to comment.