| %%%------------------------------------------------------------------- |
| %%% @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: |
| %% <ul> |
| %% <li> a map - then each key in pattern is checked for equality with the map in question</li> |
| %% <li> a fun(map()) -> boolean()</li> |
| %% </ul> |
| -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. |
| |