Skip to content
Permalink
Browse files
setting limits for maps, with map type recognition
  • Loading branch information
bartekgorny committed Mar 6, 2019
1 parent 4e7c387 commit d03d78e6c68cc489481b0b0cd1a23523f04af2f8
Showing 3 changed files with 131 additions and 15 deletions.
@@ -0,0 +1,106 @@
%%%-------------------------------------------------------------------
%%% @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([patterns_table_name/0]).

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

%% @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.
%% @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);
limit(_, _, _) ->
{error, "Bad argument - the spec is limit(atom(), map(), limit())"}.

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

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:
%% - a map - then each key in pattern is check for equality with the map in question
%% - a fun(map()) -> boolean()
-spec process_map(map()) -> {atom(), map()}.
process_map(M) ->
process_map(M, ets:tab2list(patterns_table_name())).

process_map(M, []) ->
{none, 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(#{} = M, #{} = Pattern) ->
map_matches(M, maps:to_list(Pattern));
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:from_list(lists:foldl(fun(F, Flist) -> get_value_from_map(F, M, Flist) end, [], Fields)).

get_value_from_map(F, M, Flist) ->
case maps:is_key(F, M) of
true ->
[{F, maps:get(F, M)} | Flist];
false ->
Flist
end.

patterns_table_name() -> recon_map_patterns.

store_pattern(Label, Pattern, Limit) ->
recon_rec:ensure_table_exists(),
ets:insert(patterns_table_name(), {Label, Pattern, Limit}),
ok.
@@ -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,22 +81,24 @@ 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.
end;
limit(_, _, _) ->
{error, "Bad argument - the spec is limit(atom(), integer(), limit())"}.

%% @private if a tuple is a known record, formats is as "#recname{field=value}", otherwise returns
%% just a printout of a tuple.
@@ -129,10 +132,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}
@@ -150,10 +153,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 ->
@@ -162,7 +165,8 @@ 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]),
ets:new(recon_map:patterns_table_name(), [set, public, named_table]),
Parent ! Ref,
ets_keeper()
end),
@@ -180,13 +184,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.

@@ -628,8 +628,14 @@ format_trace_output(true, Args) when is_list(Args) ->
[$[, join(", ", L), $]]
end;
format_trace_output(true, Args) when is_map(Args) ->
ItemList = maps:to_list(Args),
["#{",
{Label, Map} = recon_map:process_map(Args),
Label1 = case Label of
none -> "";
A -> atom_to_list(A)
end,
ItemList = maps:to_list(Map),
[Label1,
"#{",
join(", ", [format_kv(Key, Val) || {Key, Val} <- ItemList]),
"}"];
format_trace_output(_, Args) ->

0 comments on commit d03d78e

Please sign in to comment.