Make config file parsers pluggable
This commit is contained in:
parent
ec02bfed41
commit
29ebb5dfc9
@ -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]}
|
||||
]}.
|
||||
|
19
rebar.lock
19
rebar.lock
@ -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}].
|
||||
|
@ -7,7 +7,7 @@
|
||||
[
|
||||
kernel
|
||||
, stdlib
|
||||
, zj
|
||||
, setup
|
||||
]},
|
||||
{env, []},
|
||||
{modules, []},
|
||||
|
145
src/gmconfig.erl
145
src/gmconfig.erl
@ -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()
|
||||
, 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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user