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