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