%%%------------------------------------------------------------------- %%% @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, list/0, is_active/0, clear/0, remove/1, rename/2]). -export([process_map/1]). -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). %% @doc prints out all "known" map definitions and their limit settings. %% Printout tells a map's name, the matching fields required, and the limit options. %% @end list() -> ensure_table_exists(), io:format("~nmap definitions and limits:~n"), list(ets:tab2list(patterns_table_name())). %% @doc remove a given map entry -spec remove(map_label()) -> true. remove(Label) -> ensure_table_exists(), ets:delete(patterns_table_name(), Label). %% @doc rename a given map entry, which allows to to change priorities for %% matching. The first argument is the current name, and the second %% argument is the new name. -spec rename(map_label(), map_label()) -> renamed | missing. rename(Name, NewName) -> ensure_table_exists(), case ets:lookup(patterns_table_name(), Name) of [{Name, Pattern, Limit}] -> ets:insert(patterns_table_name(), {NewName, Pattern, Limit}), ets:delete(patterns_table_name(), Name), renamed; [] -> missing end. %% @doc prints out all "known" map filter definitions and their settings. %% Printout tells the map's label, the matching patterns, and the limit options %% @end list([]) -> io:format("~n"), ok; list([{Label, Pattern, Limit} | Rest]) -> io:format("~p: ~p -> ~p~n", [Label, Pattern, Limit]), list(Rest). %% @private 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: %% -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.