blob: 3c14883a657354ed21e33b2ba6d40e16bfdb931d [file] [log] [blame]
%%%-------------------------------------------------------------------
%%% @author bartlomiej.gorny@erlang-solutions.com
%%% @doc
%%% This module handles formatting records for known record types.
%%% Record definitions are imported from modules by user. Definitions are
%%% distinguished by record name and its arity, if you have multiple records
%%% of the same name and size, you have to choose one of them and some of your
%% records may be wrongly labelled. You can manipulate your definition list by
%% using import/1 and clear/1, and check which definitions are in use by executing
%% list/0.
%%% @end
%%%-------------------------------------------------------------------
-module(recon_rec).
-author("bartlomiej.gorny@erlang-solutions.com").
%% API
-export([import/1, format_tuple/1, clear/1, clear/0, list/0, get_list/0, limit/3]).
-export([lookup_record/2]). %% for testing
%% @doc import record definitions from a module. If a record definition of the same name
%% and arity has already been imported from another module then a warning is issued and the new
%% definition is ignored. You have to choose one and possibly remove the old one using
%% clear/1. Supports importing multiple modules at once (by giving a list of atoms as
%% an argument).
%% @end
import(Modules) when is_list(Modules) ->
lists:foldl(fun import/2, [], Modules);
import(Module) ->
import(Module, []).
%% @private if a tuple is a known record, formats is as "#recname{field=value}", otherwise returns
%% just a printout of a tuple.
format_tuple(Tuple) ->
ensure_table_exists(),
First = element(1, Tuple),
lists:flatten(format_tuple(First, Tuple)).
%% @doc remove definitions imported from a module.
clear(Module) ->
lists:map(fun(R) -> rem_for_module(R, Module) end, ets:tab2list(ets_table_name())).
%% @doc remove all imported definitions, destroy the table, clean up
clear() ->
catch ets:delete_all_objects(ets_table_name()),
catch whereis(recon_ets) ! stop,
ok.
%% @doc prints out all "known" (imported) record definitions and their limit settings.
%% Print out tells module a record originates from, its name and a list of field names,
%% plus the record's arity (may be handy if handling big records) and a list of field it
%% limits its output to, if set.
%% @end
list() ->
F = fun({Module, Name, Fields, Limits}) ->
Fnames = lists:map(fun atom_to_list/1, Fields),
Flds = string:join(Fnames, ", "),
io:format("~p: #~p(~p){~s} ~p~n", [Module, Name, length(Fields), Flds, Limits])
end,
lists:foreach(F, get_list()).
get_list() ->
ensure_table_exists(),
Lst = lists:map(fun make_list_entry/1, ets:tab2list(ets_table_name())),
lists:sort(Lst).
%% @doc Limit output to selected fields of a record (set to 'all' to reset).
limit(Name, Arity, all) ->
limit(Name, Arity, []);
limit(Name, Arity, Field) when is_atom(Field) ->
limit(Name, Arity, [Field]);
limit(Name, Arity, FieldList) ->
case lookup_record(Name, Arity) of
[] ->
{error, record_unknown};
[{Key, Fields, Mod, _}] ->
ets:insert(ets_table_name(), {Key, Fields, Mod, FieldList}),
ok
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% PRIVATE
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
make_list_entry({{Name, _}, Fields, Module, Limits}) ->
FmtLimit = case Limits of
[] -> all;
Other -> Other
end,
{Module, Name, field_names(Fields), FmtLimit}.
import(Module, ResultList) ->
ensure_table_exists(),
lists:foldl(fun(Rec, Res) -> store_record(Rec, Module, Res) end,
ResultList,
get_record_defs(Module)).
store_record(Rec, Module, ResultList) ->
{Name, Fields} = Rec,
Arity = length(Fields),
Result = case lookup_record(Name, Arity) of
[] ->
ets:insert(ets_table_name(), rec_info(Rec, Module)),
{imported, Module, Name, Arity};
[{_, _, Module, _}] ->
ets:insert(ets_table_name(), rec_info(Rec, Module)),
{overwritten, Module, Name, Arity};
[{_, _, Mod, _}] ->
{ignored, Module, Name, Arity, Mod}
end,
[Result | ResultList].
get_record_defs(Module) ->
Path = code:which(Module),
{ok,{_,[{abstract_code,{_,AC}}]}} = beam_lib:chunks(Path, [abstract_code]),
lists:foldl(fun get_record/2, [], AC).
get_record({attribute, _, record, Rec}, Acc) -> [Rec | Acc];
get_record(_, Acc) -> Acc.
%% @private
lookup_record(RecName, FieldCount) ->
ensure_table_exists(),
ets:lookup(ets_table_name(), {RecName, FieldCount}).
ensure_table_exists() ->
case ets:info(ets_table_name()) of
undefined ->
Pid = case whereis(recon_ets) of
undefined ->
P = spawn(fun() -> ets_keeper() end),
register(recon_ets, P),
P;
P -> P
end,
ets:new(ets_table_name(), [set, public, named_table]),
ets:give_away(ets_table_name(), Pid, none);
_ -> ok
end.
ets_table_name() -> recon_record_definitions.
rec_info({Name, Fields}, Module) ->
{{Name, length(Fields)}, Fields, Module, []}.
rem_for_module({_, _, Module, _} = Rec, Module) ->
ets:delete_object(ets_table_name(), Rec);
rem_for_module(_, _) -> ok.
ets_keeper() ->
receive
stop -> ok;
_ -> ets_keeper()
end.
field_names(Fields) ->
lists:map(fun field_name/1, Fields).
field_name({record_field, _, {atom, _, Name}}) -> Name;
field_name({record_field, _, {atom, _, Name}, _Default}) -> Name;
field_name({typed_record_field, Field, _Type}) -> field_name(Field).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% FORMATTER
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
format_tuple(Name, Rec) when is_atom(Name) ->
case lookup_record(Name, size(Rec) - 1) of
[RecDef] -> format_record(Rec, RecDef);
_ ->
List = tuple_to_list(Rec),
"{" ++ string:join([recon_lib:format_trace_output(El) || El <- List], ", ") ++ "}"
end;
format_tuple(_, Tuple) ->
format_default(Tuple).
format_default(Val) ->
io_lib:format("~p", [Val]).
format_record(Rec, {{Name, Arity}, Fields, _, Limits}) ->
ExpectedLength = Arity + 1,
case tuple_size(Rec) of
ExpectedLength ->
[_ | Values] = tuple_to_list(Rec),
FieldNames = field_names(Fields),
List = lists:zip(FieldNames, Values),
LimitedList = apply_limits(List, Limits),
"#" ++ atom_to_list(Name) ++ "{"
++ string:join([format_kv(Key, Val) || {Key, Val} <- LimitedList], ", ") ++ "}";
_ ->
format_default(Rec)
end.
format_kv(Key, Val) ->
recon_lib:format_trace_output(Key) ++ "=" ++ recon_lib:format_trace_output(Val).
apply_limits(List, []) -> List;
apply_limits(List, Limits) ->
lists:filter(fun({K, _}) -> lists:member(K, Limits) end, List) ++ [{more, '...'}].