Make config file parsers pluggable

This commit is contained in:
Ulf Wiger 2025-03-13 15:48:01 +01:00
parent ec02bfed41
commit 29ebb5dfc9
4 changed files with 91 additions and 84 deletions

View File

@ -3,9 +3,8 @@
{plugins, [rebar3_hex]}.
{deps, [
{zj, {git, "https://gitlab.com/zxq9/zj.git", {tag, "1.1.2"}}}
, {yamerl, "0.10.0"}
, {setup, "2.1.2"}
{setup, {git, "https://github.com/uwiger/setup.git", {ref, "9675f9a"}}}
%% , {setup, "2.1.2"}
]}.
{profiles, [
@ -17,5 +16,5 @@
deprecated_function_calls, deprecated_functions]}.
{dialyzer, [ {warnings, [unknown]}
, {base_plt_apps, [erts, kernel, stdlib, yamerl, zj, setup]}
, {base_plt_apps, [erts, kernel, stdlib, setup]}
]}.

View File

@ -1,15 +1,4 @@
{"1.2.0",
[{<<"setup">>,{pkg,<<"setup">>,<<"2.1.2">>},0},
{<<"yamerl">>,{pkg,<<"yamerl">>,<<"0.10.0">>},0},
{<<"zj">>,
{git,"https://gitlab.com/zxq9/zj.git",
{ref,"090a43d23edc481695664f16763f147a78c45afc"}},
0}]}.
[
{pkg_hash,[
{<<"setup">>, <<"43C0BBFE9160DE7925BF2FC2FE4396A99DC4EE1B73F0DC46ACC3F10E27B07A9C">>},
{<<"yamerl">>, <<"4FF81FEE2F1F6A46F1700C0D880B24D193DDB74BD14EF42CB0BCF46E81EF2F8E">>}]},
{pkg_hash_ext,[
{<<"setup">>, <<"596713D48D8241DF31821C08A9F7BAAF3E7CDD042C8396BC956CC7AE056925DC">>},
{<<"yamerl">>, <<"346ADB2963F1051DC837A2364E4ACF6EB7D80097C0F53CBDC3046EC8EC4B4E6E">>}]}
].
[{<<"setup">>,
{git,"https://github.com/uwiger/setup.git",
{ref,"9675f9a25c2087e676660911987935caeff9fef8"}},
0}].

View File

@ -7,7 +7,7 @@
[
kernel
, stdlib
, zj
, setup
]},
{env, []},
{modules, []},

View File

@ -48,6 +48,7 @@
, gmconfig_env/1
, gmconfig_env/2
, set_gmconfig_env/1
, default_gmconfig_env/0
]).
-ifdef(TEST).
@ -62,11 +63,19 @@
-include_lib("kernel/include/logger.hrl").
-type extension() :: string(). %% file extension (without leading dot)
-type decoder_return() :: config_tree()
| {ok, config_tree()}
| {error, any()}.
-type decoder_fun() :: fun( (binary()) -> decoder_return() ).
-type gmconfig() :: #{ os_env_prefix => string()
, config_file_basename => string() | 'undefined'
, config_file_os_env => string() | 'undefined'
, config_file_search_path => [string() | fun(() -> string())]
, system_suffix => string()
, system_suffix => string()
, config_formats => #{ extension() => decoder_fun() }
, schema => string() | map() | fun(() -> map())}.
-type basic_type() :: number() | binary() | boolean().
@ -115,7 +124,7 @@ set_gmconfig_env(Env) when is_map(Env) ->
-spec gmconfig_env() -> gmconfig().
gmconfig_env() ->
persistent_term:get({?MODULE, gmconfig_env}, default_gmconfig()).
persistent_term:get({?MODULE, gmconfig_env}, default_gmconfig_env()).
-spec gmconfig_env(atom()) -> any().
gmconfig_env(Key) ->
@ -142,11 +151,13 @@ search_path(Kind) ->
end, Path0)
end.
default_gmconfig() ->
default_gmconfig_env() ->
#{ os_env_prefix => "GM"
, config_file_basename => "gmconfig"
, config_file_os_env => undefined
, config_file_search_path => ["."]
, config_formats => #{ "json" => fun json_decode/1
, "eterm" => fun eterm_consult/1 }
, system_defaults_search_path => [fun setup:data_dir/0]
, system_suffix => "" }.
@ -552,8 +563,8 @@ coerce_type(Key, Value, Schema) ->
<<"integer">> -> to_integer(Value);
<<"string">> -> to_string(Value);
<<"boolean">> -> to_bool(Value);
<<"array">> -> json_decode(list_to_binary(Value));
<<"object">> -> json_decode(list_to_binary(Value))
<<"array">> -> json:decode(list_to_binary(Value));
<<"object">> -> json:decode(list_to_binary(Value))
end;
_ ->
error({unknown_key, Key})
@ -642,10 +653,21 @@ search_default_config() ->
Basename ->
Dirs = search_path(),
SystemSuffix = get_system_suffix(),
Fname = Basename ++ SystemSuffix ++ ".{json,yaml}",
ExtPattern = extension_pattern(),
Fname = Basename ++ SystemSuffix ++ ExtPattern,
search_for_config_file(Dirs, Fname)
end.
config_formats() ->
gmconfig_env(config_formats).
extension_pattern() ->
Exts = maps:keys(config_formats()),
lists:flatten([".{", intersperse(Exts),"}"]).
intersperse([H|T]) ->
[H | [[",", X] || X <- T]].
search_for_config_file(Dirs, FileWildcard) ->
lists:foldl(
fun(D0, undefined) ->
@ -665,13 +687,16 @@ to_list_string(S) when is_list(S) ->
binary_to_list(iolist_to_binary(S)).
do_load_user_config(F, Action, Mode) ->
case {filename:extension(F), Action} of
{".json", store} -> store(read_json(F, Mode), Mode);
{".yaml", store} -> store(read_yaml(F, Mode), Mode);
{".json", check} -> check_config_(catch read_json(F, Mode));
{".yaml", check} -> check_config_(catch read_yaml(F, Mode))
{Ext, Decoder} = pick_decoder(F),
case Action of
store -> store(read_file(F, Ext, Decoder, Mode), Mode);
check -> check_config_(catch read_file(F, Ext, Decoder, Mode))
end.
pick_decoder(F) ->
"." ++ Ext = filename:extension(F),
{Ext, maps:get(Ext, config_formats())}.
store(Vars, Mode) when is_map(Vars) ->
case pt_get_config() of
Map when map_size(Map) == 0 ->
@ -726,12 +751,15 @@ silent_as_report(silent) -> report;
silent_as_report(Mode ) -> Mode.
schema_string(Schema, Mode) ->
JSONSchema = json:encode(Schema),
JSONSchema = json_encode(Schema),
case Mode of
check -> json:format(JSONSchema, #{indent => 2});
report -> JSONSchema
end.
json_encode(J) ->
iolist_to_binary(json:encode(J)).
error_format(Fmt, Args, check) ->
io:format(Fmt, Args);
error_format(Fmt, Args, report) ->
@ -906,70 +934,50 @@ update_map(With, Map) when is_map(With), is_map(Map) ->
end
end, Map, With).
read_json(F, Mode) ->
validate(
try_decode(F, fun(F1) ->
json_consult(F1)
end, "JSON", Mode), F, Mode).
json_consult(F) ->
read_file(F, Type, Decoder, Mode) ->
case setup_file:read_file(F) of
{ok, Bin} ->
[json_decode(Bin)];
validate(
try_decode_bin(Bin, Decoder, Type, Mode),
F, Mode);
{error, Reason} ->
?LOG_ERROR("Read error ~s - ~p", [F, Reason]),
error({read_error, F, Reason})
end.
json_decode(Bin) ->
case json_decode_(Bin) of
{ok, Value} ->
Value;
{error, Reason} ->
?LOG_ERROR("CAUGHT: ~p", [Reason]),
error(Reason)
end.
json_decode(Str) when is_list(Str) ->
json:decode(iolist_to_binary(Str));
json_decode(Bin) when is_binary(Bin) ->
json:decode(Bin).
json_decode_(Bin) ->
case zj:binary_decode(Bin) of
{ok, _} = Ok ->
Ok;
{error, _Parsed, Remainder} ->
{error, {json_decode_error, string_slice(Remainder)}}
end.
eterm_consult(Bin) ->
setup_file:consult_binary(Bin).
-define(SliceSz, 80).
string_slice(B) ->
case size(B) of
Sz when Sz > ?SliceSz ->
Sl = string:slice(B, 0, ?SliceSz - 4),
<<Sl/binary, " ...">>;
_ ->
B
end.
read_yaml(F, Mode) ->
validate(
try_decode(
F,
fun(F1) ->
yamerl:decode_file(F1, [{str_node_as_binary, true},
{map_node_format, map}])
end, "YAML", Mode),
F, Mode).
try_decode(F, DecF, Fmt, Mode) ->
try DecF(F)
try_decode_bin(Bin, Decoder, Fmt, Mode) ->
try decode_bin(Bin, Decoder)
catch
error:E ->
error_msg(Mode, "Error reading ~s file: ~s~n", [Fmt, F]),
erlang:error(E)
decode_fail(E, Fmt, Mode)
end.
validate(JSON, F, Mode) when is_list(JSON) ->
check_validation([validate_(J) || J <- JSON], JSON, F, Mode).
%% validate(JSON, F, Mode) when is_map(JSON) ->
%% validate([JSON], F, Mode).
decode_bin(Str, Decoder) when is_list(Str) ->
decode_bin(iolist_to_binary(Str), Decoder);
decode_bin(Bin, Decoder) when is_binary(Bin), is_function(Decoder, 1) ->
case Decoder(Bin) of
{ok, Map} when is_map(Map) ->
Map;
Map when is_map(Map) ->
Map;
{error, E} ->
error(E)
end.
decode_fail(E, Fmt, Mode) ->
error_msg(Mode, "Parse error (~p)", [Fmt]),
erlang:error(E).
validate(JSON, F, Mode) when is_map(JSON) ->
check_validation([validate_(JSON)], JSON, F, Mode).
vinfo(Mode, Res, F) ->
case Res of
@ -1071,7 +1079,8 @@ ensure_schema_loaded() ->
load_schema() ->
JSON = gmconfig_schema(),
Schema = json_decode(JSON),
Decode = maps:get("json", config_formats()),
Schema = decode_bin(JSON, Decode),
pt_set_schema(Schema),
Schema.
@ -1084,3 +1093,13 @@ gmconfig_schema() ->
F when is_function(F, 0) ->
F()
end.
json_consult(F) ->
Decode = maps:get("json", config_formats()),
case setup_file:read_file(F) of
{ok, Bin} ->
decode_bin(Bin, Decode);
{error, Reason} ->
?LOG_ERROR("Read error ~s - ~p", [F, Reason]),
error({read_error, F, Reason})
end.