blob: 3689df1130d6aeb1637d607bb0d619bb1d1118eb [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([is_active/0]).
-export([import/1, clear/1, clear/0, list/0, get_list/0, limit/3]).
-export([format_tuple/1]).
-ifdef(TEST).
-export([lookup_record/2]).
-endif.
% basic types
-type field() :: atom().
-type record_name() :: atom().
% compound
-type limit() :: all | none | field() | [field()].
-type listentry() :: {module(), record_name(), [field()], limit()}.
-type import_result() :: {imported, module(), record_name(), arity()}
| {overwritten, module(), record_name(), arity()}
| {ignored, module(), record_name(), arity(), module()}.
%% @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 the new
%% definition is ignored (returned info tells you from which module the existing definition was imported).
%% 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
-spec import(module() | [module()]) -> import_result() | [import_result()].
import(Modules) when is_list(Modules) ->
lists:foldl(fun import/2, [], Modules);
import(Module) ->
import(Module, []).
%% @doc quickly check if we want to do any record formatting
-spec is_active() -> boolean().
is_active() ->
case whereis(recon_ets) of
undefined -> false;
_ -> true
end.
%% @doc remove definitions imported from a module.
clear(Module) ->
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() ->
maybe_kill(recon_ets),
ok.
%% @doc prints out all "known" (imported) record definitions and their limit settings.
%% Printout 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 = join(",", Fnames),
io:format("~p: #~p(~p){~s} ~p~n",
[Module, Name, length(Fields), Flds, Limits])
end,
io:format("Module: #Name(Size){<Fields>} Limits~n==========~n", []),
lists:foreach(F, get_list()).
%% @doc returns a list of active record definitions
-spec get_list() -> [listentry()].
get_list() ->
ensure_table_exists(),
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, 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(records_table_name(), {Key, Fields, Mod, Limit}),
ok
end.
%% @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),
format_tuple(First, Tuple).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% PRIVATE
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
make_list_entry({{Name, _}, Fields, Module, Limits}) ->
FmtLimit = case Limits of
[] -> none;
Other -> Other
end,
{Module, Name, 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(records_table_name(), rec_info(Rec, Module)),
{imported, Module, Name, Arity};
[{_, _, Module, _}] ->
ets:insert(records_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(records_table_name(), {RecName, FieldCount}).
%% @private
ensure_table_exists() ->
case ets:info(records_table_name()) of
undefined ->
case whereis(recon_ets) of
undefined ->
Parent = self(),
Ref = make_ref(),
%% attach to the currently running session
{Pid, MonRef} = spawn_monitor(fun() ->
register(recon_ets, self()),
ets:new(records_table_name(), [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.
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(records_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),
["{", join(", ", [recon_trace:format_trace_output(true, 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),
List = lists:zip(Fields, Values),
LimitedList = apply_limits(List, Limits),
["#", atom_to_list(Name), "{",
join(", ", [format_kv(Key, Val) || {Key, Val} <- LimitedList]),
"}"];
_ ->
format_default(Rec)
end.
format_kv(Key, Val) ->
%% Some messy mutually recursive calls we can't avoid
[recon_trace:format_trace_output(true, Key), "=", recon_trace:format_trace_output(true, Val)].
apply_limits(List, none) -> List;
apply_limits(_List, all) -> [];
apply_limits(List, Field) when is_atom(Field) ->
[{Field, proplists:get_value(Field, List)}, {more, '...'}];
apply_limits(List, Limits) ->
lists:filter(fun({K, _}) -> lists:member(K, Limits) end, List) ++ [{more, '...'}].
%%%%%%%%%%%%%%%
%%% 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.
-ifdef(OTP_RELEASE).
-spec join(term(), [term()]) -> [term()].
join(Sep, List) ->
lists:join(Sep, List).
-else.
-spec join(string(), [string()]) -> string().
join(Sep, List) ->
string:join(List, Sep).
-endif.