/
recon_map.erl
208 lines (184 loc) · 6.6 KB
/
recon_map.erl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
%%%-------------------------------------------------------------------
%%% @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.