1 |
|
%% @doc Config parsing and processing for the TOML format |
2 |
|
-module(mongoose_config_parser_toml). |
3 |
|
|
4 |
|
-behaviour(mongoose_config_parser). |
5 |
|
|
6 |
|
-export([parse_file/1]). |
7 |
|
|
8 |
|
-ifdef(TEST). |
9 |
|
-export([process/1, |
10 |
|
extract_errors/1]). |
11 |
|
-endif. |
12 |
|
|
13 |
|
-include("mongoose_config_spec.hrl"). |
14 |
|
|
15 |
|
%% Input: TOML parsed by tomerl |
16 |
|
-type toml_key() :: binary(). |
17 |
|
-type toml_value() :: tomerl:value(). |
18 |
|
-type toml_section() :: tomerl:section(). |
19 |
|
|
20 |
|
%% Output: list of config records, containing key-value pairs |
21 |
|
-type option_value() :: atom() | binary() | string() | float(). % parsed leaf value |
22 |
|
-type config_part() :: term(). % any part of a top-level option value, may contain config errors |
23 |
|
-type top_level_config() :: {mongoose_config:key(), mongoose_config:value()}. |
24 |
|
-type config_error() :: #{class := error, what := atom(), text := string(), any() => any()}. |
25 |
|
-type config() :: top_level_config() | config_error(). |
26 |
|
|
27 |
|
-type list_processor() :: fun((path(), [config_part()]) -> config_part()) |
28 |
|
| fun(([config_part()]) -> config_part()). |
29 |
|
|
30 |
|
-type processor() :: fun((path(), config_part()) -> config_part()) |
31 |
|
| fun((config_part()) -> config_part()). |
32 |
|
|
33 |
|
-type step() :: |
34 |
|
parse % Recursive processing (section/list) or type conversion (leaf option) |
35 |
|
|
36 |
|
| validate % Value check with one of the predefined validators |
37 |
|
|
38 |
|
| format_items % Optional formatting of section/list items as a map |
39 |
|
|
40 |
|
| process % Optional processing of the value with a custom function |
41 |
|
|
42 |
|
| wrap. % Wrapping the value into a list, which will be concatenated |
43 |
|
% with other items of the parent node. |
44 |
|
% In case of a KV pair the key is also added here. |
45 |
|
|
46 |
|
%% Path from the currently processed config node to the root |
47 |
|
%% - toml_key(): key in a toml_section() |
48 |
|
%% - item: item in a list |
49 |
|
%% - {host, Host}: item in the list of hosts in host_config |
50 |
|
-type path() :: [toml_key() | item | {host, jid:server()}]. |
51 |
|
|
52 |
|
-export_type([toml_key/0, toml_value/0, toml_section/0, |
53 |
|
option_value/0, config/0, config_error/0, config_part/0, |
54 |
|
list_processor/0, processor/0]). |
55 |
|
|
56 |
|
-spec parse_file(FileName :: string()) -> mongoose_config_parser:state(). |
57 |
|
parse_file(FileName) -> |
58 |
80 |
case tomerl:read_file(FileName) of |
59 |
|
{ok, Content} -> |
60 |
80 |
process(Content); |
61 |
|
{error, Error} -> |
62 |
:-( |
error(config_error([#{what => toml_parsing_failed, text => Error}])) |
63 |
|
end. |
64 |
|
|
65 |
|
-spec process(toml_section()) -> mongoose_config_parser:state(). |
66 |
|
process(Content) -> |
67 |
80 |
Config = parse(Content), |
68 |
80 |
Hosts = proplists:get_value(hosts, Config, []), |
69 |
80 |
HostTypes = proplists:get_value(host_types, Config, []), |
70 |
80 |
case extract_errors(Config) of |
71 |
|
[] -> |
72 |
80 |
build_state(Hosts, HostTypes, Config); |
73 |
|
Errors -> |
74 |
:-( |
error(config_error(Errors)) |
75 |
|
end. |
76 |
|
|
77 |
|
config_error(Errors) -> |
78 |
:-( |
{config_error, "Could not read the TOML configuration file", Errors}. |
79 |
|
|
80 |
|
-spec parse(toml_section()) -> [config()]. |
81 |
|
parse(Content) -> |
82 |
80 |
handle([], Content, mongoose_config_spec:root()). |
83 |
|
|
84 |
|
%% TODO replace with binary_to_existing_atom where possible, prevent atom leak |
85 |
52715 |
b2a(B) -> binary_to_atom(B, utf8). |
86 |
|
|
87 |
|
-spec ensure_keys([toml_key()], toml_section()) -> any(). |
88 |
|
ensure_keys(Keys, Section) -> |
89 |
9668 |
case lists:filter(fun(Key) -> not maps:is_key(Key, Section) end, Keys) of |
90 |
9668 |
[] -> ok; |
91 |
:-( |
MissingKeys -> error(#{what => missing_mandatory_keys, missing_keys => MissingKeys}) |
92 |
|
end. |
93 |
|
|
94 |
|
-spec parse_section(path(), toml_section(), mongoose_config_spec:config_section()) -> |
95 |
|
[config_part()]. |
96 |
|
parse_section(Path, M, #section{items = Items, defaults = Defaults}) -> |
97 |
9668 |
FilteredDefaults = maps:filter(fun(K, _V) -> not maps:is_key(K, M) end, Defaults), |
98 |
9668 |
M1 = maps:merge(get_always_included(Items), M), |
99 |
9668 |
ProcessedConfig = maps:map(fun(K, V) -> handle([K|Path], V, get_spec_for_key(K, Items)) end, M1), |
100 |
9668 |
ProcessedDefaults = maps:map(fun(K, V) -> handle_default([K|Path], V, maps:get(K, Items)) end, |
101 |
|
FilteredDefaults), |
102 |
9668 |
lists:flatmap(fun({_K, ConfigParts}) -> ConfigParts end, |
103 |
|
lists:keysort(1, maps:to_list(maps:merge(ProcessedDefaults, ProcessedConfig)))). |
104 |
|
|
105 |
|
-spec get_spec_for_key(toml_key(), map()) -> mongoose_config_spec:config_node(). |
106 |
|
get_spec_for_key(Key, Items) -> |
107 |
24256 |
case maps:is_key(Key, Items) of |
108 |
|
true -> |
109 |
21186 |
maps:get(Key, Items); |
110 |
|
false -> |
111 |
3070 |
case maps:find(default, Items) of |
112 |
3070 |
{ok, Spec} -> Spec; |
113 |
:-( |
error -> error(#{what => unexpected_key, key => Key}) |
114 |
|
end |
115 |
|
end. |
116 |
|
|
117 |
|
get_always_included(Items) -> |
118 |
9668 |
maps:from_list([{K, #{}} || {K, #section{include = always}} <- maps:to_list(Items)]). |
119 |
|
|
120 |
|
-spec parse_list(path(), [toml_value()], mongoose_config_spec:config_list()) -> [config_part()]. |
121 |
|
parse_list(Path, L, #list{items = ItemSpec}) -> |
122 |
4050 |
lists:flatmap(fun(Elem) -> |
123 |
6265 |
Key = item_key(Path, Elem), |
124 |
6265 |
handle([Key|Path], Elem, ItemSpec) |
125 |
|
end, L). |
126 |
|
|
127 |
|
-spec handle(path(), toml_value(), mongoose_config_spec:config_node()) -> [config_part()]. |
128 |
|
handle(Path, Value, Spec) -> |
129 |
30601 |
handle(Path, Value, Spec, [parse, validate, format_items, process, wrap]). |
130 |
|
|
131 |
|
-spec handle_default(path(), toml_value(), mongoose_config_spec:config_node()) -> [config_part()]. |
132 |
|
handle_default(Path, Value, Spec) -> |
133 |
2241 |
handle(Path, Value, Spec, [wrap]). |
134 |
|
|
135 |
|
-spec handle(path(), toml_value(), mongoose_config_spec:config_node(), [step()]) -> [config_part()]. |
136 |
|
handle(Path, Value, Spec, Steps) -> |
137 |
32842 |
lists:foldl(fun(_, [#{what := _, class := error}|_] = Errors) -> |
138 |
:-( |
Errors; |
139 |
|
(Step, Acc) -> |
140 |
155246 |
try_step(Step, Path, Value, Acc, Spec) |
141 |
|
end, Value, Steps). |
142 |
|
|
143 |
|
-spec handle_step(step(), path(), toml_value(), mongoose_config_spec:config_node()) -> |
144 |
|
config_part(). |
145 |
|
handle_step(parse, Path, Value, Spec) -> |
146 |
30601 |
ParsedValue = case Spec of |
147 |
|
#section{} when is_map(Value) -> |
148 |
9668 |
check_required_keys(Spec, Value), |
149 |
9668 |
validate_keys(Spec, Value), |
150 |
9668 |
parse_section(Path, Value, Spec); |
151 |
|
#list{} when is_list(Value) -> |
152 |
4050 |
parse_list(Path, Value, Spec); |
153 |
|
#option{type = Type} when not is_list(Value), not is_map(Value) -> |
154 |
16883 |
convert(Value, Type) |
155 |
|
end, |
156 |
30601 |
case extract_errors(ParsedValue) of |
157 |
30601 |
[] -> ParsedValue; |
158 |
:-( |
Errors -> Errors |
159 |
|
end; |
160 |
|
handle_step(format_items, _Path, Items, Spec) -> |
161 |
30601 |
format_items(Items, format_items_spec(Spec)); |
162 |
|
handle_step(validate, _Path, ParsedValue, Spec) -> |
163 |
30601 |
validate(ParsedValue, Spec), |
164 |
30601 |
ParsedValue; |
165 |
|
handle_step(process, Path, ParsedValue, Spec) -> |
166 |
30601 |
process(Path, ParsedValue, process_spec(Spec)); |
167 |
|
handle_step(wrap, Path, ProcessedValue, Spec) -> |
168 |
32842 |
wrap(Path, ProcessedValue, wrap_spec(Spec)). |
169 |
|
|
170 |
|
-spec check_required_keys(mongoose_config_spec:config_section(), toml_section()) -> any(). |
171 |
|
check_required_keys(#section{required = all, items = Items}, Section) -> |
172 |
2480 |
ensure_keys(maps:keys(Items), Section); |
173 |
|
check_required_keys(#section{required = Required}, Section) -> |
174 |
7188 |
ensure_keys(Required, Section). |
175 |
|
|
176 |
|
-spec validate_keys(mongoose_config_spec:config_section(), toml_section()) -> any(). |
177 |
|
validate_keys(#section{validate_keys = Validator}, Section) -> |
178 |
9668 |
lists:foreach(fun(Key) -> |
179 |
24075 |
mongoose_config_validator:validate(b2a(Key), atom, Validator) |
180 |
|
end, maps:keys(Section)). |
181 |
|
|
182 |
|
-spec format_items_spec(mongoose_config_spec:config_node()) -> mongoose_config_spec:format_items(). |
183 |
9668 |
format_items_spec(#section{format_items = FormatItems}) -> FormatItems; |
184 |
4050 |
format_items_spec(#list{format_items = FormatItems}) -> FormatItems; |
185 |
16883 |
format_items_spec(#option{}) -> none. |
186 |
|
|
187 |
|
-spec format_items(config_part(), mongoose_config_spec:format_items()) -> config_part(). |
188 |
|
format_items(KVs, map) -> |
189 |
4307 |
Keys = lists:map(fun({K, _}) -> K end, KVs), |
190 |
4307 |
mongoose_config_validator:validate_list(Keys, unique), |
191 |
4307 |
maps:from_list(KVs); |
192 |
|
format_items(Value, none) -> |
193 |
26294 |
Value. |
194 |
|
|
195 |
|
-spec validate(config_part(), mongoose_config_spec:config_node()) -> any(). |
196 |
|
validate(Value, #section{validate = Validator}) -> |
197 |
9668 |
mongoose_config_validator:validate_section(Value, Validator); |
198 |
|
validate(Value, #list{validate = Validator}) -> |
199 |
4050 |
mongoose_config_validator:validate_list(Value, Validator); |
200 |
|
validate(Value, #option{type = Type, validate = Validator}) -> |
201 |
16883 |
mongoose_config_validator:validate(Value, Type, Validator). |
202 |
|
|
203 |
|
-spec process_spec(mongoose_config_spec:config_section() | |
204 |
|
mongoose_config_spec:config_list()) -> undefined | list_processor(); |
205 |
|
(mongoose_config_spec:config_option()) -> undefined | processor(). |
206 |
9668 |
process_spec(#section{process = Process}) -> Process; |
207 |
4050 |
process_spec(#list{process = Process}) -> Process; |
208 |
16883 |
process_spec(#option{process = Process}) -> Process. |
209 |
|
|
210 |
|
-spec process(path(), config_part(), undefined | processor()) -> config_part(). |
211 |
26244 |
process(_Path, V, undefined) -> V; |
212 |
1826 |
process(_Path, V, F) when is_function(F, 1) -> F(V); |
213 |
2531 |
process(Path, V, F) when is_function(F, 2) -> F(Path, V). |
214 |
|
|
215 |
|
-spec convert(toml_value(), mongoose_config_spec:option_type()) -> option_value(). |
216 |
390 |
convert(V, boolean) when is_boolean(V) -> V; |
217 |
1102 |
convert(V, binary) when is_binary(V) -> V; |
218 |
5309 |
convert(V, string) -> binary_to_list(V); |
219 |
4975 |
convert(V, atom) -> b2a(V); |
220 |
80 |
convert(<<"infinity">>, int_or_infinity) -> infinity; %% TODO maybe use TOML '+inf' |
221 |
590 |
convert(V, int_or_infinity) when is_integer(V) -> V; |
222 |
240 |
convert(V, int_or_atom) when is_integer(V) -> V; |
223 |
1520 |
convert(V, int_or_atom) -> b2a(V); |
224 |
2677 |
convert(V, integer) when is_integer(V) -> V; |
225 |
:-( |
convert(V, float) when is_float(V) -> V. |
226 |
|
|
227 |
|
-spec wrap_spec(mongoose_config_spec:config_node()) -> mongoose_config_spec:wrapper(). |
228 |
9748 |
wrap_spec(#section{wrap = Wrap}) -> Wrap; |
229 |
4516 |
wrap_spec(#list{wrap = Wrap}) -> Wrap; |
230 |
18578 |
wrap_spec(#option{wrap = Wrap}) -> Wrap. |
231 |
|
|
232 |
|
-spec wrap(path(), config_part(), mongoose_config_spec:wrapper()) -> [config_part()]. |
233 |
|
wrap([Key|_] = Path, V, host_config) -> |
234 |
632 |
wrap(Path, V, {host_config, b2a(Key)}); |
235 |
|
wrap([Key|_] = Path, V, global_config) -> |
236 |
1360 |
wrap(Path, V, {global_config, b2a(Key)}); |
237 |
|
wrap(Path, V, {host_config, Key}) -> |
238 |
712 |
[{{Key, get_host(Path)}, V}]; |
239 |
|
wrap(Path, V, {global_config, Key}) -> |
240 |
1721 |
global = get_host(Path), |
241 |
1721 |
[{Key, V}]; |
242 |
|
wrap([item|_] = Path, V, default) -> |
243 |
6134 |
wrap(Path, V, item); |
244 |
|
wrap([Key|_] = Path, V, default) -> |
245 |
20153 |
wrap(Path, V, {kv, b2a(Key)}); |
246 |
|
wrap(_Path, V, {kv, Key}) -> |
247 |
21629 |
[{Key, V}]; |
248 |
|
wrap(_Path, V, item) -> |
249 |
6214 |
[V]; |
250 |
|
wrap(_Path, _V, remove) -> |
251 |
131 |
[]; |
252 |
|
wrap([Key|_], V, prepend_key) -> |
253 |
:-( |
L = [b2a(Key) | tuple_to_list(V)], |
254 |
:-( |
[list_to_tuple(L)]; |
255 |
|
wrap(_Path, V, none) when is_list(V) -> |
256 |
2435 |
V. |
257 |
|
|
258 |
|
-spec get_host(path()) -> jid:server() | global. |
259 |
|
get_host(Path) -> |
260 |
2433 |
case lists:reverse(Path) of |
261 |
232 |
[<<"host_config">>, {host, Host} | _] -> Host; |
262 |
2201 |
_ -> global |
263 |
|
end. |
264 |
|
|
265 |
|
-spec try_step(step(), path(), toml_value(), term(), |
266 |
|
mongoose_config_spec:config_node()) -> config_part(). |
267 |
|
try_step(Step, Path, Value, Acc, Spec) -> |
268 |
155246 |
try |
269 |
155246 |
handle_step(Step, Path, Acc, Spec) |
270 |
|
catch error:Reason:Stacktrace -> |
271 |
:-( |
BasicFields = #{what => toml_processing_failed, |
272 |
|
class => error, |
273 |
|
stacktrace => Stacktrace, |
274 |
|
text => "TOML configuration error: " ++ error_text(Step), |
275 |
|
toml_path => path_to_string(Path), |
276 |
|
toml_value => Value}, |
277 |
:-( |
ErrorFields = error_fields(Reason), |
278 |
:-( |
[maps:merge(BasicFields, ErrorFields)] |
279 |
|
end. |
280 |
|
|
281 |
|
-spec error_text(step()) -> string(). |
282 |
:-( |
error_text(parse) -> "Malformed node"; |
283 |
:-( |
error_text(validate) -> "Invalid node value"; |
284 |
:-( |
error_text(format_items) -> "List or section has invalid key-value pairs"; |
285 |
:-( |
error_text(process) -> "Node could not be processed"; |
286 |
:-( |
error_text(wrap) -> "Node could not be wrapped in a config option". |
287 |
|
|
288 |
|
-spec error_fields(any()) -> map(). |
289 |
:-( |
error_fields(#{what := Reason} = M) -> maps:remove(what, M#{reason => Reason}); |
290 |
:-( |
error_fields(Reason) -> #{reason => Reason}. |
291 |
|
|
292 |
|
-spec path_to_string(path()) -> string(). |
293 |
|
path_to_string(Path) -> |
294 |
:-( |
Items = lists:flatmap(fun node_to_string/1, lists:reverse(Path)), |
295 |
:-( |
string:join(Items, "."). |
296 |
|
|
297 |
:-( |
node_to_string(item) -> []; |
298 |
:-( |
node_to_string({host, _}) -> []; |
299 |
:-( |
node_to_string(Node) -> [binary_to_list(Node)]. |
300 |
|
|
301 |
|
-spec item_key(path(), toml_value()) -> {host, jid:server()} | item. |
302 |
101 |
item_key([<<"host_config">>], #{<<"host_type">> := Host}) -> {host, Host}; |
303 |
30 |
item_key([<<"host_config">>], #{<<"host">> := Host}) -> {host, Host}; |
304 |
6134 |
item_key(_, _) -> item. |
305 |
|
|
306 |
|
%% Processing of the parsed options |
307 |
|
|
308 |
|
-spec build_state([jid:server()], [jid:server()], [top_level_config()]) -> |
309 |
|
mongoose_config_parser:state(). |
310 |
|
build_state(Hosts, HostTypes, Opts) -> |
311 |
80 |
lists:foldl(fun(F, StateIn) -> F(StateIn) end, |
312 |
|
mongoose_config_parser:new_state(), |
313 |
80 |
[fun(S) -> mongoose_config_parser:set_hosts(Hosts, S) end, |
314 |
80 |
fun(S) -> mongoose_config_parser:set_host_types(HostTypes, S) end, |
315 |
80 |
fun(S) -> mongoose_config_parser:set_opts(Opts, S) end, |
316 |
|
fun mongoose_config_parser:unfold_globals/1, |
317 |
|
fun mongoose_config_parser:post_process_modules/1]). |
318 |
|
|
319 |
|
%% Any nested config_part() may be a config_error() - this function extracts them all recursively |
320 |
|
-spec extract_errors([config()]) -> [config_error()]. |
321 |
|
extract_errors(Config) -> |
322 |
30681 |
extract(fun(#{what := _, class := error}) -> true; |
323 |
904247 |
(_) -> false |
324 |
|
end, Config). |
325 |
|
|
326 |
|
-spec extract(fun((config_part()) -> boolean()), config_part()) -> [config_part()]. |
327 |
|
extract(Pred, Data) -> |
328 |
904247 |
case Pred(Data) of |
329 |
:-( |
true -> [Data]; |
330 |
904247 |
false -> extract_items(Pred, Data) |
331 |
|
end. |
332 |
|
|
333 |
|
-spec extract_items(fun((config_part()) -> boolean()), config_part()) -> [config_part()]. |
334 |
236275 |
extract_items(Pred, L) when is_list(L) -> lists:flatmap(fun(El) -> extract(Pred, El) end, L); |
335 |
129036 |
extract_items(Pred, T) when is_tuple(T) -> extract_items(Pred, tuple_to_list(T)); |
336 |
16922 |
extract_items(Pred, M) when is_map(M) -> extract_items(Pred, maps:to_list(M)); |
337 |
667972 |
extract_items(_, _) -> []. |