| %%%------------------------------------------------------------------- |
| %%% @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. |