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