1 |
|
%% @doc Parsing and processing of MongooseIM config files |
2 |
|
%% - parser backends: only 'toml' |
3 |
|
%% - config state management |
4 |
|
-module(mongoose_config_parser). |
5 |
|
|
6 |
|
%% parser API |
7 |
|
-export([parse_file/1]). |
8 |
|
|
9 |
|
%% state API |
10 |
|
-export([new_state/0, |
11 |
|
set_opts/2, |
12 |
|
set_hosts/2, |
13 |
|
set_host_types/2, |
14 |
|
get_opts/1]). |
15 |
|
|
16 |
|
%% config post-processing |
17 |
|
-export([unfold_globals/1, |
18 |
|
post_process_modules/1]). |
19 |
|
|
20 |
|
-callback parse_file(FileName :: string()) -> state(). |
21 |
|
|
22 |
|
-include("mongoose.hrl"). |
23 |
|
|
24 |
|
-export_type([state/0]). |
25 |
|
|
26 |
|
-record(state, {opts = [] :: opts(), |
27 |
|
hosts = [] :: [domain_name()], |
28 |
|
host_types = [] :: [mongooseim:host_type()]}). |
29 |
|
|
30 |
|
-type opts() :: [{mongoose_config:key(), mongoose_config:value()}]. |
31 |
|
-type domain_name() :: jid:server(). |
32 |
|
-type state() :: #state{}. |
33 |
|
|
34 |
|
%% Parser API |
35 |
|
|
36 |
|
-spec parse_file(FileName :: string()) -> state(). |
37 |
|
parse_file(FileName) -> |
38 |
73 |
ParserModule = parser_module(filename:extension(FileName)), |
39 |
73 |
try |
40 |
73 |
ParserModule:parse_file(FileName) |
41 |
|
catch |
42 |
|
error:{config_error, ExitMsg, Errors} -> |
43 |
:-( |
halt_with_msg(ExitMsg, Errors) |
44 |
|
end. |
45 |
|
|
46 |
|
%% Only the TOML format is supported |
47 |
73 |
parser_module(".toml") -> mongoose_config_parser_toml. |
48 |
|
|
49 |
|
%% State API |
50 |
|
|
51 |
|
-spec new_state() -> state(). |
52 |
|
new_state() -> |
53 |
73 |
#state{}. |
54 |
|
|
55 |
|
-spec set_opts(opts(), state()) -> state(). |
56 |
|
set_opts(Opts, State) -> |
57 |
73 |
State#state{opts = Opts}. |
58 |
|
|
59 |
|
-spec set_hosts([domain_name()], state()) -> state(). |
60 |
|
set_hosts(Hosts, State) -> |
61 |
73 |
State#state{hosts = Hosts}. |
62 |
|
|
63 |
|
-spec set_host_types([mongooseim:host_type()], state()) -> state(). |
64 |
|
set_host_types(HostTypes, State) -> |
65 |
73 |
State#state{host_types = HostTypes}. |
66 |
|
|
67 |
|
-spec get_opts(state()) -> opts(). |
68 |
|
get_opts(#state{opts = Opts}) -> |
69 |
146 |
Opts. |
70 |
|
|
71 |
|
%% Config post-processing |
72 |
|
|
73 |
|
%% @doc Repeat global options for each host type for easier lookup |
74 |
|
-spec unfold_globals(state()) -> state(). |
75 |
|
unfold_globals(Config = #state{opts = Opts, hosts = Hosts, host_types = HostTypes}) -> |
76 |
73 |
{HTOpts, SimpleOpts} = lists:partition(fun({K, _}) -> is_tuple(K) end, Opts), |
77 |
73 |
GroupedOpts = maps:to_list(group_opts(HTOpts)), |
78 |
73 |
AllHostTypes = Hosts ++ HostTypes, |
79 |
73 |
NewHTOpts = lists:flatmap(fun({K, M}) -> merge_opts(K, M, AllHostTypes) end, GroupedOpts), |
80 |
73 |
Config#state{opts = SimpleOpts ++ NewHTOpts}. |
81 |
|
|
82 |
|
%% @doc For each host type, merge the global value with the host-type value (if it exists) |
83 |
|
-spec merge_opts(atom(), #{mongooseim:host_type_or_global() => mongoose_config:value()}, |
84 |
|
[mongooseim:host_type()]) -> |
85 |
|
[{mongoose_config:host_type_key(), mongoose_config:value()}]. |
86 |
|
merge_opts(Key, Opts = #{global := GlobalValue}, AllHostTypes) -> |
87 |
438 |
Global = case keep_global_value(Key) of |
88 |
146 |
true -> [{{Key, global}, GlobalValue}]; |
89 |
292 |
false -> [] |
90 |
|
end, |
91 |
438 |
Global ++ [{{Key, HT}, merge_values(Key, GlobalValue, maps:get(HT, Opts, GlobalValue))} |
92 |
438 |
|| HT <- AllHostTypes]; |
93 |
|
merge_opts(Key, Opts, _AllHostTypes) -> |
94 |
:-( |
[{{Key, HT}, Val} || {HT, Val} <- maps:to_list(Opts)]. |
95 |
|
|
96 |
|
%% @doc Group host-type options by keys for easier processing (key by key) |
97 |
|
-spec group_opts([{mongoose_config:host_type_key(), mongoose_config:value()}]) -> |
98 |
|
#{atom() => #{mongooseim:host_type_or_global() => mongoose_config:value()}}. |
99 |
|
group_opts(HTOpts) -> |
100 |
73 |
lists:foldl(fun({{Key, HT}, Val}, Acc) -> |
101 |
663 |
maps:update_with(Key, fun(Opts) -> Opts#{HT => Val} end, #{HT => Val}, Acc) |
102 |
|
end, #{}, HTOpts). |
103 |
|
|
104 |
|
%% @doc Merge global options with host-type ones |
105 |
|
-spec merge_values(atom(), mongoose_config:value(), mongoose_config:value()) -> |
106 |
|
mongoose_config:value(). |
107 |
|
merge_values(acl, GlobalValue, HTValue) -> |
108 |
382 |
merge_with(fun(V1, V2) -> V1 ++ V2 end, GlobalValue, HTValue); |
109 |
|
merge_values(access, GlobalValue, HTValue) -> |
110 |
382 |
merge_with(fun acl:merge_access_rules/2, GlobalValue, HTValue); |
111 |
|
merge_values(_Key, _GlobalValue, HTValue) -> |
112 |
1528 |
HTValue. |
113 |
|
|
114 |
|
%% Use maps:merge_with/3 when dropping OTP 23 |
115 |
|
merge_with(F, GlobalMap, HTMap) -> |
116 |
764 |
maps:fold(fun(Key, HTVal, M) -> |
117 |
7640 |
maps:update_with(Key, fun(GVal) when GVal =:= HTVal -> GVal; |
118 |
:-( |
(GVal) -> F(GVal, HTVal) |
119 |
|
end, HTVal, M) |
120 |
|
end, GlobalMap, HTMap). |
121 |
|
|
122 |
|
%% @doc Global value is retained for access rules and acl as they can be matched on the global level |
123 |
|
-spec keep_global_value(atom()) -> boolean(). |
124 |
73 |
keep_global_value(acl) -> true; |
125 |
73 |
keep_global_value(access) -> true; |
126 |
292 |
keep_global_value(_) -> false. |
127 |
|
|
128 |
|
-spec post_process_modules(state()) -> state(). |
129 |
|
post_process_modules(State = #state{opts = Opts}) -> |
130 |
73 |
Opts2 = lists:map(fun post_process_modules_opt/1, Opts), |
131 |
73 |
State#state{opts = Opts2}. |
132 |
|
|
133 |
|
post_process_modules_opt({{modules, HostType}, Modules}) -> |
134 |
382 |
ModulesWithDeps = gen_mod_deps:resolve_deps(HostType, Modules), |
135 |
382 |
{{modules, HostType}, unfold_opts(ModulesWithDeps)}; |
136 |
|
post_process_modules_opt(Other) -> |
137 |
3697 |
Other. |
138 |
|
|
139 |
|
unfold_opts(Modules) -> |
140 |
382 |
maps:map(fun(_Mod, Opts) -> proplists:unfold(Opts) end, Modules). |
141 |
|
|
142 |
|
%% local functions |
143 |
|
|
144 |
|
-spec halt_with_msg(string(), [any()]) -> no_return(). |
145 |
|
-ifdef(TEST). |
146 |
|
halt_with_msg(ExitMsg, Errors) -> |
147 |
|
error({config_error, ExitMsg, Errors}). |
148 |
|
-else. |
149 |
|
halt_with_msg(ExitMsg, Errors) -> |
150 |
:-( |
[?LOG_ERROR(Error) || Error <- Errors], |
151 |
:-( |
mongoose_config_utils:exit_or_halt(ExitMsg). |
152 |
|
-endif. |