./ct_report/coverage/mongoose_config_parser.COVER.html

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.
Line Hits Source