Make config file parsers pluggable #1
@ -3,9 +3,7 @@
|
|||||||
{plugins, [rebar3_hex]}.
|
{plugins, [rebar3_hex]}.
|
||||||
|
|
||||||
{deps, [
|
{deps, [
|
||||||
{zj, {git, "https://gitlab.com/zxq9/zj.git", {tag, "1.1.2"}}}
|
{setup, "2.2.1"}
|
||||||
, {yamerl, "0.10.0"}
|
|
||||||
, {setup, "2.1.2"}
|
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
{profiles, [
|
{profiles, [
|
||||||
@ -17,5 +15,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]}
|
||||||
]}.
|
]}.
|
||||||
|
19
rebar.lock
19
rebar.lock
@ -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">>}]}
|
|
||||||
].
|
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
[
|
[
|
||||||
kernel
|
kernel
|
||||||
, stdlib
|
, stdlib
|
||||||
, zj
|
, setup
|
||||||
]},
|
]},
|
||||||
{env, []},
|
{env, []},
|
||||||
{modules, []},
|
{modules, []},
|
||||||
|
166
src/gmconfig.erl
166
src/gmconfig.erl
@ -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().
|
||||||
@ -110,12 +119,13 @@ mock_system_defaults(Config) ->
|
|||||||
-endif.
|
-endif.
|
||||||
|
|
||||||
-spec set_gmconfig_env(gmconfig()) -> ok.
|
-spec set_gmconfig_env(gmconfig()) -> ok.
|
||||||
set_gmconfig_env(Env) when is_map(Env) ->
|
set_gmconfig_env(Env0) when is_map(Env0) ->
|
||||||
|
Env = maps:merge(default_gmconfig_env(), Env0),
|
||||||
persistent_term:put({?MODULE, gmconfig_env}, Env).
|
persistent_term:put({?MODULE, gmconfig_env}, 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 +152,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 +564,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 +654,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 +688,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 +752,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 +935,64 @@ 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]),
|
error_msg(Mode, "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]),
|
eterm_consult(Bin) ->
|
||||||
error(Reason)
|
case setup_file:consult_binary(Bin) of
|
||||||
|
{ok, [Map]} when is_map(Map) ->
|
||||||
|
{ok, normalize_config(Map)};
|
||||||
|
{ok, Other} ->
|
||||||
|
error({unknown_data, Other});
|
||||||
|
{error, _} = Error ->
|
||||||
|
Error
|
||||||
end.
|
end.
|
||||||
|
|
||||||
json_decode_(Bin) ->
|
normalize_config(Map) when is_map(Map) ->
|
||||||
case zj:binary_decode(Bin) of
|
try
|
||||||
{ok, _} = Ok ->
|
json:decode(iolist_to_binary(json:encode(Map)))
|
||||||
Ok;
|
|
||||||
{error, _Parsed, Remainder} ->
|
|
||||||
{error, {json_decode_error, string_slice(Remainder)}}
|
|
||||||
end.
|
|
||||||
|
|
||||||
-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)
|
|
||||||
catch
|
catch
|
||||||
error:E ->
|
error:E ->
|
||||||
error_msg(Mode, "Error reading ~s file: ~s~n", [Fmt, F]),
|
error({cannot_normalize, E, Map})
|
||||||
erlang:error(E)
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
validate(JSON, F, Mode) when is_list(JSON) ->
|
try_decode_bin(Bin, Decoder, Fmt, Mode) ->
|
||||||
check_validation([validate_(J) || J <- JSON], JSON, F, Mode).
|
try decode_bin(Bin, Decoder)
|
||||||
%% validate(JSON, F, Mode) when is_map(JSON) ->
|
catch
|
||||||
%% validate([JSON], F, Mode).
|
error:E:T ->
|
||||||
|
error_msg(Mode, "CAUGHT for ~p: ~p / ~p", [Fmt, E, T]),
|
||||||
|
error(E)
|
||||||
|
end.
|
||||||
|
|
||||||
|
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);
|
||||||
|
Other ->
|
||||||
|
error({bad_decoder_return, Other})
|
||||||
|
end.
|
||||||
|
|
||||||
|
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 +1094,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 +1108,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.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user