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]}. {plugins, [rebar3_hex]}.
{deps, [ {deps, [
{zj, {git, "https://gitlab.com/zxq9/zj.git", {tag, "1.1.2"}}} {setup, {git, "https://github.com/uwiger/setup.git", {ref, "9675f9a"}}}
, {yamerl, "0.10.0"} %% , {setup, "2.1.2"}
, {setup, "2.1.2"}
]}. ]}.
{profiles, [ {profiles, [
@ -17,5 +16,5 @@
deprecated_function_calls, deprecated_functions]}. deprecated_function_calls, deprecated_functions]}.
{dialyzer, [ {warnings, [unknown]} {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">>,
[{<<"setup">>,{pkg,<<"setup">>,<<"2.1.2">>},0}, {git,"https://github.com/uwiger/setup.git",
{<<"yamerl">>,{pkg,<<"yamerl">>,<<"0.10.0">>},0}, {ref,"9675f9a25c2087e676660911987935caeff9fef8"}},
{<<"zj">>, 0}].
{git,"https://gitlab.com/zxq9/zj.git",
{ref,"090a43d23edc481695664f16763f147a78c45afc"}},
0}]}.
[
{pkg_hash,[
{<<"setup">>, <<"43C0BBFE9160DE7925BF2FC2FE4396A99DC4EE1B73F0DC46ACC3F10E27B07A9C">>},
{<<"yamerl">>, <<"4FF81FEE2F1F6A46F1700C0D880B24D193DDB74BD14EF42CB0BCF46E81EF2F8E">>}]},
{pkg_hash_ext,[
{<<"setup">>, <<"596713D48D8241DF31821C08A9F7BAAF3E7CDD042C8396BC956CC7AE056925DC">>},
{<<"yamerl">>, <<"346ADB2963F1051DC837A2364E4ACF6EB7D80097C0F53CBDC3046EC8EC4B4E6E">>}]}
].

View File

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

View File

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