gmconfig/src/gmconfig.erl
2025-05-12 23:26:35 +02:00

1152 lines
35 KiB
Erlang

%%% -*- erlang-indent-level:4; indent-tabs-mode: nil -*-
%%%-------------------------------------------------------------------
%%% @copyright (C) 2025, QPQ AG
%%% @copyright (C) 2017, Aeternity Anstalt
%%% @doc Bootstrapping and querying the Gajumaru environment
%%%
%%% @end
%%%-------------------------------------------------------------------
-module(gmconfig).
-export([get_env/2, get_env/3]).
-export([user_config/0]). % returns current user config
-export([load_main_config_schema/0,
load_system_defaults/0]).
-export([load_user_config/0,
load_user_config/1]).
-export([schema/0,
schema/1,
schema/2]).
-export([load_schema/0]). % load_user_config() ensures schema is loaded
-export([schema_default/1, schema_default/2]).
-export([schema_keys/0, schema_keys/1]).
-export([config_value/4]).
-export([find_config/1, find_config/2, %% -> {ok, Value} | undefined
get_config/1, get_config/2 %% -> Value (or exception)
]).
-export([merge_config_maps/2]).
-export([nested_map_get/2]).
-export([load_config_file/1]). % apply system-/network-specific defaults
-export([search_for_config_file/2]).
-export([apply_os_env/0,
apply_os_env/3]).
-export([process_plain_args/0]).
-export([data_dir/1]).
-export([check_config/1]).
-export([update_config/1,
update_config/2,
update_config/3,
silent_update_config/1,
delete_config_value/1,
suggest_config/2,
set_config_value/2]). %% (Path, Value)
-export([export_config/0]).
-export([ gmconfig_env/0
, gmconfig_env/1
, gmconfig_env/2
, set_gmconfig_env/1
, default_gmconfig_env/0
]).
-ifdef(TEST).
-export([mock_config/0,
mock_config/1,
mock_system_defaults/0,
mock_system_defaults/1,
ensure_schema_loaded/0,
unmock_schema/0,
unmock_config/0]).
-endif.
-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())]
, config_plain_args => string() | 'undefined'
, system_suffix => string()
, config_formats => #{ extension() => decoder_fun() }
, schema => string() | map() | fun(() -> map())}.
-type basic_type() :: number() | binary() | boolean().
-type basic_or_list() :: basic_type() | [basic_type()].
-type config_tree() :: [{binary(), config_tree() | basic_or_list()}].
%% -type env_key() :: atom() | list().
-type config_key() :: binary() | [binary()].
-type find_opts() :: [user_config | schema_default | {env, atom(), atom()} | {value, any()}].
-export_type([ gmconfig/0
, find_opts/0 ]).
-ifdef(TEST).
mock_config() ->
mock_config(#{}).
mock_config(Cfg) ->
ensure_schema_loaded(),
store(Cfg, _Notify = false, _Mode = silent).
unmock_config() ->
gmconfig_schema_utils:clear(),
ok.
unmock_schema() ->
gmconfig_schema_utils:clear(),
ok.
mock_system_defaults() ->
mock_config(),
load_system_defaults(silent).
mock_system_defaults(Config) ->
ensure_schema_loaded(),
load_system_defaults(silent),
mock_config(Config).
-endif.
-spec set_gmconfig_env(gmconfig()) -> ok.
set_gmconfig_env(Env0) when is_map(Env0) ->
Env = maps:merge(default_gmconfig_env(), Env0),
persistent_term:put({?MODULE, gmconfig_env}, Env).
-spec gmconfig_env() -> gmconfig().
gmconfig_env() ->
persistent_term:get({?MODULE, gmconfig_env}, default_gmconfig_env()).
-spec gmconfig_env(atom()) -> any().
gmconfig_env(Key) ->
gmconfig_env(Key, undefined).
gmconfig_env(Key, Default) ->
maps:get(Key, gmconfig_env(), Default).
search_path() -> search_path(config_file).
search_path(Kind) ->
Key = case Kind of
config_file -> config_file_search_path;
system_defaults -> system_defaults_search_path
end,
case gmconfig_env(Key, undefined) of
undefined -> [];
Path0 when is_list(Path0) ->
lists:map(
fun(F) when is_function(F, 0) ->
F();
(D) when is_list(D) ->
D
end, Path0)
end.
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 }
, config_plain_args => undefined
, system_defaults_search_path => [fun setup:data_dir/0]
, system_suffix => "" }.
%% This function is similar to application:get_env/2, except
%% 1. It uses the setup:get_env/2 function, which supports a number
%% of useful variable expansions (see the setup documentation)
%% 2. It supports a hierarchical key format, [A,B,...], where each
%% part of the key represents a level in a tree structure.
%% Example:
%% if get_env(A, a) returns {ok, {a, [{1, foo}, {2, bar}]}}, or
%% {ok, [{a, [{1, foo}, {2, bar}]}]}, then
%% get_env(A, [a,1]) will return {ok, foo}
-spec get_env(atom(), atom() | list()) -> undefined | {ok, any()}.
get_env(App, [H|T]) ->
case setup:get_env(App, H) of
{ok, V} ->
get_env_l(T, V);
undefined ->
undefined
end;
get_env(App, K) when is_atom(K) ->
setup:get_env(App, K).
get_env(App, K, Default) ->
case get_env(App, K) of
{ok, V} -> V;
undefined -> Default
end.
get_env_l([], V) ->
{ok, V};
get_env_l([H|T], [_|_] = L) ->
case lists:keyfind(H, 1, L) of
{_, V} ->
get_env_l(T, V);
false ->
undefined
end;
get_env_l(_, _) ->
undefined.
-spec user_config() -> config_tree().
user_config() ->
pt_get_config().
config_value(CfgKey, App, Env, Default) ->
{ok, Value} =find_config(CfgKey, [ user_config
, {env, App, Env}
, schema_default
, {value, Default} ]),
Value.
%%% ====================================================================================
%%% Config lookup API
%%%
%%% These are the preferred functions for config data lookup
get_config(CfgKey) ->
get_config(CfgKey, [user_config, schema_default]).
get_config(CfgKey, Path) ->
case find_config(CfgKey, Path) of
{ok, Value} ->
Value;
undefined ->
error(expected_value, [CfgKey, Path])
end.
find_config(CfgKey) ->
find_config(CfgKey, [user_config, schema_default]).
find_config(CfgKey, [H|T]) ->
case find_config_(CfgKey, H) of
undefined -> find_config(CfgKey, T);
{ok,_} = Ok -> Ok
end;
find_config(_, []) ->
undefined.
find_config_(K, user_config ) -> user_map(K);
find_config_(_, {env, App, EnvKey}) -> get_env(App, EnvKey);
find_config_(K, schema_default ) -> schema_default(K);
find_config_(_, {value, V} ) -> {ok, V}.
%%% ====================================================================================
%%% Merging config maps
%%%
%%% This function implements a deep map merge, ensuring that sub-maps also get merged.
-spec merge_config_maps(map(), map()) -> map().
merge_config_maps(A, B) ->
{A1, B1} =
maps:fold(
fun(K, V, {Ca, Cb}) when is_map(V), is_map(map_get(K, Cb)) ->
{Ca#{K := merge_config_maps(V, map_get(K, Cb))}, maps:remove(K, Cb)};
(_, _, Acc) ->
Acc
end, {A, B}, A),
maps:merge(A1, B1).
%% The user_map() functions are equivalent to user_config(), but
%% operate on a tree of maps rather than a tree of {K,V} tuples.
%% Actually, the user_map() data is the original validated object,
%% which is then transformed to the Key-Value tree used in user_config().
-spec user_map() -> map().
user_map() ->
pt_get_config().
-spec user_map([any()] | any()) -> {ok, any()} | undefined.
user_map(Key) when is_list(Key) ->
M = user_map(),
case maps:find(Key, M) of
{ok, _} = Ok ->
Ok;
error ->
nested_map_get(Key, M)
end;
user_map(Key) ->
case maps:find(Key, user_map()) of
{ok, _} = Ok -> Ok;
error -> undefined
end.
nested_map_get([], V) ->
{ok, V};
nested_map_get([H|T], M) when is_map(M) ->
case maps:find(H, M) of
{ok, M1} when is_map(M1); is_list(M1) ->
nested_map_get(T, M1);
{ok, V} when T == [] ->
{ok, V};
_ ->
undefined
end;
nested_map_get([H|T], L) when is_list(L) ->
case lists_map_key_find(H, L) of
{ok, V} when is_map(V); is_list(V) ->
nested_map_get(T, V);
{ok, V} when T == [] ->
{ok, V};
error ->
undefined
end.
lists_map_key_find({K,V,Then} = Pat, [#{} = H|T]) ->
case maps:find(K, H) of
{ok, V} ->
maps:find(Then, H);
{ok, _} ->
lists_map_key_find(Pat, T);
error ->
lists_map_key_find(Pat, T)
end;
lists_map_key_find(K, [#{} = H|T]) ->
case maps:find(K, H) of
{ok, _} = Ok ->
Ok;
error ->
lists_map_key_find(K, T)
end;
lists_map_key_find(K, [{K, V}|_]) ->
{ok, V};
lists_map_key_find(_, []) ->
error.
schema() ->
case pt_get_schema(undefined) of
undefined ->
load_schema(),
pt_get_schema();
S ->
S
end.
pt_get_schema() ->
gmconfig_schema_utils:get_schema().
pt_get_schema(Default) ->
gmconfig_schema_utils:get_schema(Default).
pt_set_schema(Schema) ->
gmconfig_schema_utils:set_schema(Schema).
pt_get_config() ->
gmconfig_schema_utils:get_config().
pt_set_config(Config) ->
gmconfig_schema_utils:set_config(Config).
schema(Key) ->
gmconfig_schema_utils:schema(Key).
schema(Key, Schema) ->
gmconfig_schema_utils:schema(Key, Schema).
schema_keys() ->
schema_keys([]).
schema_keys(Path) ->
case schema(Path) of
undefined ->
undefined;
{ok, Map} ->
just_schema_keys(Map)
end.
just_schema_keys(#{<<"properties">> := Ps}) ->
maps:fold(fun(K, V, Acc) ->
Acc#{K => just_schema_keys(V)}
end, #{}, Ps);
just_schema_keys(#{<<"patternProperties">> := Ps}) ->
maps:fold(fun(K, V, Acc) ->
Acc#{{'$pat', K} => just_schema_keys(V)}
end, #{}, Ps);
just_schema_keys(#{<<"type">> := <<"array">>, <<"items">> := Items}) ->
case Items of
#{<<"type">> := <<"object">>} ->
[just_schema_keys(Items)];
#{<<"type">> := T} ->
[binary_to_atom(T, utf8)];
#{<<"pattern">> := _} ->
[string]
end;
just_schema_keys(#{<<"oneOf">> := List}) ->
#{<<"oneOf">> => lists:foldr(
fun(Alt, Acc) ->
case just_schema_keys(Alt) of
[] ->
[maps:fold(
fun(K, #{<<"type">> := T}, Acc1) ->
Acc1#{K => binary_to_atom(T, utf8)};
(_, _, Acc1) ->
Acc1
end, #{}, Alt) | Acc];
Other ->
[Other|Acc]
end
end, [], List)};
just_schema_keys(#{<<"type">> := T}) ->
binary_to_atom(T, utf8);
just_schema_keys(#{properties := Ps}) ->
maps:fold(fun(K, V, Acc) ->
Acc#{K => just_schema_keys(V)}
end, #{}, Ps);
just_schema_keys(#{patternProperties := Ps}) ->
maps:fold(fun(K, V, Acc) ->
Acc#{{'$pat', K} => just_schema_keys(V)}
end, #{}, Ps);
just_schema_keys(#{type := array, items := Items}) ->
case Items of
#{type := object} ->
[just_schema_keys(Items)];
#{type := T} ->
[T];
#{pattern := _} ->
[string]
end;
just_schema_keys(#{oneOf := List}) ->
#{oneOf => lists:foldr(
fun(Alt, Acc) ->
case just_schema_keys(Alt) of
[] ->
[maps:fold(
fun(K, #{type := T}, Acc1) ->
Acc1#{K => T};
(_, _, Acc1) ->
Acc1
end, #{}, Alt) | Acc];
Other ->
[Other|Acc]
end
end, [], List)};
just_schema_keys(#{type := T}) ->
T;
just_schema_keys(_) ->
[].
schema_default(Path) when is_list(Path) ->
schema_default(Path, schema()).
schema_default(Path, Schema) ->
case schema(Path, Schema) of
undefined -> undefined;
{ok, Tree} ->
case schema_default_(Tree) of
undefined -> undefined;
Res ->
{ok, Res}
end
end.
schema_default_(#{<<"type">> := <<"object">>, <<"properties">> := Props}) ->
maps:map(fun(_, #{<<"type">> := <<"object">>} = PP) ->
schema_default_(PP);
(_, #{<<"type">> := <<"array">>, <<"default">> := Def}) ->
Def;
(_, #{<<"type">> := <<"array">>, <<"items">> := Items}) ->
[schema_default_(Items)];
(_, #{<<"default">> := Def}) -> Def;
(_, _) -> undefined
end, Props);
schema_default_(#{<<"type">> := <<"array">>, <<"default">> := Def}) ->
Def;
schema_default_(#{<<"type">> := <<"array">>, <<"items">> := Items}) ->
[schema_default_(Items)];
schema_default_(#{<<"default">> := Def}) ->
Def;
schema_default_(#{type := <<"object">>, properties := Props}) ->
maps:map(fun(_, #{<<"type">> := <<"object">>} = PP) ->
schema_default_(PP);
(_, #{type := array, default := Def}) ->
Def;
(_, #{type := array, items := Items}) ->
[schema_default_(Items)];
(_, #{default := Def}) -> Def;
(_, _) -> undefined
end, Props);
schema_default_(#{type := array, default := Def}) ->
Def;
schema_default_(#{type := array, items := Items}) ->
[schema_default_(Items)];
schema_default_(#{default := Def}) ->
Def;
schema_default_(_) ->
undefined.
load_user_config() ->
load_user_config(silent).
load_user_config(Mode) when Mode =:= check; Mode =:= silent; Mode =:= report ->
case config_file() of
undefined ->
info_msg(Mode, "No config file specified; using default settings~n", []),
ok;
F ->
info_msg(Mode, "Reading config file ~s~n", [F]),
do_load_user_config(F, store, Mode)
end.
load_config_file(File) ->
load_config_file(File, report).
load_config_file(File, Mode) when Mode =:= check;
Mode =:= silent;
Mode =:= report ->
do_load_user_config(File, store, Mode).
apply_os_env() ->
Pfx = os_env_prefix(),
ConfigMap = pt_get_config(),
case apply_os_env(Pfx, schema(), ConfigMap) of
NewConfig when is_map(NewConfig) ->
pt_set_config(NewConfig),
NewConfig;
Other ->
Other
end.
os_env_prefix() ->
gmconfig_env(os_env_prefix, "GM").
%% Plugin API version, using plugin schema and config.
%% The plugin API might decide to publish a specific event...
apply_os_env(Pfx, Schema, ConfigMap) ->
%% We sort on variable names to allow specific values to override object
%% definitions at a higher level (e.g. GM__MEMPOOL followed by GM__MEMPOOL__TX_TTL)
%% Note that all schema name parts are converted to uppercase.
try
Names = lists:keysort(1, schema_key_names(Pfx, Schema)),
error_logger:info_msg("OS env config: ~p~n", [Names]),
Map = lists:foldl(
fun({_Name, Key, Value}, Acc) ->
Value1 = coerce_type(Key, Value, Schema),
update_map(to_map(Key, Value1), Acc)
end, #{}, Names),
error_logger:info_msg("Map for OS env config: ~p~n", [Map]),
if map_size(Map) > 0 ->
update_config_(Map, ConfigMap, Schema, report);
true ->
no_change
end
catch
error:E:ST ->
error_logger:info_msg("CAUGHT error:~p / ~p~n", [E, ST]),
error(E)
end.
process_plain_args() ->
case gmconfig_env(config_plain_args, undefined) of
undefined ->
ok;
Str ->
PlainArgs = init:get_plain_arguments(),
Schema = schema(),
Map = process_plain_args_(PlainArgs, Str, schema(), #{}),
?LOG_INFO("Map from plain args: ~p", [Map]),
if map_size(Map) > 0 ->
update_config_(Map, pt_get_config(), Schema, report);
true ->
no_change
end
end.
process_plain_args_([Tag, K, V | Rest], Tag, Schema, Map) ->
Path = plain_path_arg(K),
Value = coerce_type(Path, V, Schema),
Map1 = update_map(to_map(Path, Value), Map),
process_plain_args_(Rest, Tag, Schema, Map1);
process_plain_args_([_ | T], Tag, Schema, Map) ->
process_plain_args_(T, Tag, Schema, Map);
process_plain_args_([], _, _, Map) ->
Map.
plain_path_arg(Str) ->
re:split(Str, <<"__">>, [{return, binary}]).
to_map(K, V) ->
to_map(K, V, #{}).
to_map([K], Val, M) ->
M#{K => Val};
to_map([H|T], Val, M) ->
SubMap = maps:get(H, M, #{}),
M#{H => to_map(T, Val, SubMap)}.
coerce_type(Key, Value, Schema) ->
case schema(Key, Schema) of
{ok, #{<<"type">> := Type}} ->
case Type of
<<"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))
end;
_ ->
error({unknown_key, Key})
end.
to_integer(I) when is_integer(I) -> I;
to_integer(L) when is_list(L) -> list_to_integer(L);
to_integer(B) when is_binary(B) -> binary_to_integer(B).
to_string(L) when is_list(L) -> list_to_binary(L);
to_string(B) when is_binary(B) -> B.
to_bool("true") -> true;
to_bool("false") -> false;
to_bool(B) when is_boolean(B) ->
B;
to_bool(Other) ->
error({expected_boolean, Other}).
schema_key_names(Prefix, Schema) ->
case Schema of
#{<<"$schema">> := _, <<"properties">> := Props} ->
schema_key_names(Prefix, [], Props, []);
_ ->
[]
end.
schema_key_names(NamePfx, KeyPfx, Map, Acc0) when is_map(Map) ->
maps:fold(
fun(SubKey, SubMap, Acc) ->
NamePfx1 = NamePfx ++ "__" ++ string:to_upper(binary_to_list(SubKey)),
KeyPfx1 = KeyPfx ++ [SubKey],
EnvKey = unhyphenate(NamePfx1),
Acc1 = case os:getenv(EnvKey) of
false -> Acc;
Value ->
[{EnvKey, KeyPfx1, Value} | Acc]
end,
case maps:find(<<"properties">>, SubMap) of
error ->
Acc1;
{ok, Props} ->
schema_key_names(NamePfx1, KeyPfx1, Props, Acc1)
end
end, Acc0, Map).
%% Unfortunately, the config schema contains some names with hyphens in them.
%% Since hyphens aren't allowed in OS environment names, we replace them with underscores.
%% This should be safe, since we DO NOT stupidly have names that differ only on '-' v '_'.
unhyphenate(Str) ->
re:replace(Str, <<"\\-">>, <<"_">>, [global, {return, list}]).
check_config(F) ->
do_load_user_config(F, check, check).
data_dir(Name) when is_atom(Name) ->
filename:join([setup:data_dir(), Name]).
config_file() ->
case command_line_config_file() of
undefined ->
default_config_file();
F -> F
end.
command_line_config_file() ->
case init:get_argument('-config') of
{ok, [[F]]} -> F;
_ -> undefined
end.
default_config_file() ->
case gmconfig_env(config_file_os_env, undefined) of
undefined ->
search_default_config();
E ->
case os:getenv(E) of
false -> search_default_config();
F -> F
end
end.
search_default_config() ->
case gmconfig_env(config_file_basename, undefined) of
undefined -> undefined;
Basename ->
Dirs = search_path(),
SystemSuffix = get_system_suffix(),
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) ->
D = to_list_string(D0),
error_logger:info_msg("Searching for config file ~s "
"in directory ~s~n", [FileWildcard, D]),
case filelib:wildcard(FileWildcard, D) of
[] -> undefined;
[F|_] -> filename:join(D, F)
end;
(_, Acc) -> Acc
end, undefined, Dirs).
to_list_string(S) when is_binary(S) ->
binary_to_list(S);
to_list_string(S) when is_list(S) ->
binary_to_list(iolist_to_binary(S)).
do_load_user_config(F, Action, 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 ->
check_validation([validate_(Vars)],
[Vars], set_config_value, report),
pt_set_config(Vars);
Map ->
?LOG_DEBUG("Existing config, updating ...", []),
update_config(Vars, Map, Mode)
end;
store([Vars], Mode) when is_map(Vars) ->
store(Vars, Mode).
check_config_({yamerl_exception, _StackTrace} = Error) ->
{error, Error};
check_config_({'EXIT', Reason}) ->
ShortError = pp_error(Reason),
{error, ShortError};
check_config_([Vars]) ->
{ok, {Vars, to_tree(Vars)}}.
pp_error({validation_failed, _}) ->
validation_failed;
pp_error(Other) ->
Other.
pp_error_({error, {schema_file_not_found, Schema}}, Mode) ->
error_format("Schema not found : ~s", [Schema], Mode);
pp_error_({error, [{data_invalid, _Schema, Type, Value, Pos}]}, Mode) ->
PosStr = pp_pos(Pos),
ValStr = pp_val(Value),
TypeStr = pp_type(Type),
case Mode of
check ->
io:format("Validation failed~n"
"Position: ~s~n"
"Value : ~s~n"
"Reason : ~s~n",
[PosStr, ValStr, TypeStr]);
report ->
error_format("Validation failed, Pos: ~s~n"
"Value: ~s~n"
"Reason: ~s", [PosStr, ValStr, TypeStr], Mode)
end;
pp_error_({error, [{schema_invalid, Section, Description}]}, Mode) ->
SchemaStr = schema_string(Section, Mode),
error_format("Reading schema failed~n"
"Section: ~n~s~n"
"Reason: ~p~n", [SchemaStr, Description], Mode).
silent_as_report(silent) -> report;
silent_as_report(Mode ) -> Mode.
schema_string(Schema, Mode) ->
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) ->
Str = io_lib:format(Fmt, Args),
Parts = re:split(Str, <<"\n">>),
Out = iolist_to_binary(
[hd(Parts) |
[[" | ", P] || P <- tl(Parts)]]),
Out = re:replace(Str, <<"\n">>, <<" | ">>, [global, {return, binary}]),
?LOG_ERROR("~s", [Out]).
pp_pos([H | T]) ->
[to_string(H) | [pp_pos_(X) || X <- T]].
pp_pos_(I) when is_integer(I) -> ["[", integer_to_list(I), "]"];
pp_pos_(S) when is_binary(S) -> ["/", S].
pp_val(I) when is_integer(I) -> integer_to_list(I);
pp_val(S) when is_binary(S) -> ["\"", S, "\""];
pp_val(X) ->
try schema_string(X, report)
catch
error:_ ->
io_lib:fwrite("~w", [X])
end.
pp_type(data_invalid) -> "Data invalid";
pp_type(missing_id_field) -> "Missing ID field";
pp_type(missing_required_property) -> "Missing required property";
pp_type(no_match ) -> "No match";
pp_type(no_extra_properties_allowed) -> "No extra properties allowed";
pp_type(no_extra_items_allowed) -> "No extra items allowed";
pp_type(not_allowed) -> "Not allowed";
pp_type(not_unique) -> "Not unique";
pp_type(not_in_enum) -> "Not in enum";
pp_type(not_in_range) -> "Not in range";
pp_type(not_divisible) -> "Not divisible";
pp_type(wrong_type) -> "Wrong type";
pp_type(wrong_size) -> "Wrong size";
pp_type(wrong_length) -> "Wrong length";
pp_type(wrong_format) -> "Wrong format";
pp_type(too_many_properties) -> "Too many properties";
pp_type(too_few_properties) -> "Too few properties";
pp_type(all_schemas_not_valid) -> "The 'allOf' requirement is not upheld";
pp_type(any_schemas_not_valid) -> "The 'anyOf' requirement is not upheld";
pp_type(not_multiple_of) -> "Not an instance of 'multipleOf'";
pp_type(not_one_schema_valid) -> "The 'oneOf' requirement is not upheld";
pp_type(not_schema_valid) -> "The 'not' requirement is not upheld";
pp_type(wrong_not_schema) -> "Wrong not schema";
pp_type(external) -> "External";
pp_type(Other) -> io_lib:fwrite("~w", [Other]).
to_tree(Vars) ->
to_tree_(expand_maps(Vars)).
expand_maps(M) when is_map(M) ->
[{K, expand_maps(V)} || {K, V} <- maps:to_list(M)];
expand_maps(L) when is_list(L) ->
[expand_maps(E) || E <- L];
expand_maps(E) ->
E.
to_tree_(L) when is_list(L) ->
lists:flatten([to_tree_(E) || E <- L]);
to_tree_({K, V}) ->
{K, to_tree_(V)};
to_tree_(E) ->
E.
silent_update_config(Map) ->
update_config(Map, _Notify = false, silent).
update_config(Map) when is_map(Map) ->
update_config(Map, report).
update_config(Map, Mode) when is_map(Map) ->
ConfigMap = pt_get_config(),
update_config(Map, ConfigMap, Mode).
update_config(Map, ConfigMap, Mode) ->
Schema = pt_get_schema(),
ConfigMap1 = update_config_(Map, ConfigMap, Schema, Mode),
?LOG_DEBUG("New config: ~p", [ConfigMap1]),
pt_set_config(ConfigMap1),
ok.
export_config() ->
Config = pt_get_config(),
JSON = json:format(Config, #{indent => 2}),
TS = calendar:system_time_to_rfc3339(erlang:system_time(second)),
File = filename:join(setup:log_dir(), "user_config-" ++ TS ++ ".json"),
file:write_file(File, JSON).
-spec delete_config_value(config_key()) -> boolean().
delete_config_value(Key) when is_binary(Key) ->
delete_config_value([Key]);
delete_config_value(Path) when is_list(Path) ->
Config = pt_get_config(),
case delete_(Path, Config) of
Config1 when Config1 =/= Config ->
pt_set_config(Config1),
true;
_ ->
false
end.
delete_([H], Config) when is_map_key(H, Config) ->
maps:remove(H, Config);
delete_([H|T], Map) when T =/= [], is_map_key(H, Map) ->
Map#{H := delete_(T, maps:get(H, Map))};
delete_(_, C) ->
C.
set_config_value(Path, Value) ->
ConfigMap = pt_get_config(),
NewConfig = set_config_value(Path, ConfigMap, Value),
check_validation([validate_(NewConfig)],
[NewConfig], set_config_value, report),
pt_set_config(NewConfig).
set_config_value([K], Map, Value) when is_map(Map) ->
Map#{K => Value};
set_config_value([H|T], Map, Value) when is_map(Map) ->
case maps:find(H, Map) of
{ok, Sub} when is_map(Sub) ->
Map#{H := set_config_value(T, Sub, Value)};
_ ->
Map#{H => set_config_value(T, #{}, Value)}
end.
%% Checks if a given config key (list of binary keys corresponding to the GM
%% config schema) is already configured. If not, the suggested value is used.
suggest_config(Key, Value) ->
case find_config(Key, [user_config]) of
{ok, _} ->
{error, already_configured};
undefined ->
Map = kv_to_config_map(Key, Value),
update_config(Map),
ok
end.
kv_to_config_map([H], V) ->
#{H => V};
kv_to_config_map([H|T], V) ->
#{H => kv_to_config_map(T, V)}.
update_config_(Map, _ConfigMap, Schema, Mode) when Mode =:= check;
Mode =:= silent;
Mode =:= report ->
NewConfig = gmconfig_schema_utils:update_config(Map),
%% NewConfig = update_map(Map, ConfigMap),
?LOG_DEBUG("NewConfig = ~p", [NewConfig]),
%% Note: We must validate the *resulting* config, since some validation
%% steps will require the full structure (e.g. multiple required fields in
%% the original config, which should not have to be included in the update)
check_validation([validate_(NewConfig, Schema)],
[NewConfig], update_config, Mode),
NewConfig.
update_map(With, Map) when is_map(With), is_map(Map) ->
maps:fold(
fun(K, V, Acc) ->
case maps:find(K, Acc) of
{ok, Submap} when is_map(Submap) ->
Acc#{K => update_map(V, Submap)};
{ok, _} ->
Acc#{K => V};
error ->
Acc#{K => V}
end
end, Map, With).
read_file(F, Type, Decoder, Mode) ->
case setup_file:read_file(F) of
{ok, Bin} ->
validate(
try_decode_bin(Bin, Decoder, Type, Mode),
F, Mode);
{error, Reason} ->
error_msg(Mode, "Read error ~s - ~p", [F, Reason]),
error({read_error, F, 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).
eterm_consult(Bin) ->
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.
normalize_config(Map) when is_map(Map) ->
try
json:decode(iolist_to_binary(json:encode(Map)))
catch
error:E ->
error({cannot_normalize, E, Map})
end.
try_decode_bin(Bin, Decoder, Fmt, Mode) ->
try decode_bin(Bin, Decoder)
catch
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) ->
case Res of
{ok, _} ->
info_report(Mode, [{validation, F},
{result, Res}]);
{error, Errors} ->
Mode1 = silent_as_report(Mode),
[pp_error_(E, Mode1) || E <- Errors],
ok
end.
info_report(report, Info) ->
error_logger:info_report(Info);
info_report(_, _) ->
ok.
info_msg(report, Fmt, Args) ->
error_logger:info_msg(Fmt, Args);
info_msg(_, _,_ ) ->
ok.
error_msg(report, Fmt, Args) ->
error_logger:error_msg(Fmt, Args);
error_msg(_, _, _) ->
ok.
check_validation(Res, _JSON, F, Mode) ->
case lists:foldr(
fun({ok, M}, {Ok,Err}) when is_map(M) ->
{[M|Ok], Err};
(Other, {Ok,Err}) ->
{Ok, [Other|Err]}
end, {[], []}, Res) of
{Ok, []} ->
vinfo(Mode, {ok, Ok}, F),
Ok;
{_, Errors} ->
vinfo(Mode, {error, Errors}, F),
erlang:error(validation_failed)
end.
validate_(JSON) ->
Schema = ensure_schema_loaded(),
validate_(JSON, Schema).
validate_(JSON, Schema) ->
_ = gmconfig_schema_utils:valid(JSON, Schema),
{ok, JSON}.
load_main_config_schema() ->
_ = load_schema(),
?LOG_INFO("Loaded config schema", []),
ok.
load_system_defaults() ->
load_system_defaults(report).
load_system_defaults(Mode) ->
case gmconfig_env(config_file_basename, undefined) of
undefined ->
ok;
Basename ->
SystemSuffix = get_system_suffix(),
Fname = Basename ++ "_defaults" ++ SystemSuffix ++ ".{yaml,json}",
Path = search_path(system_defaults),
case search_for_config_file(Path, Fname) of
undefined ->
ok;
ConfigFile ->
?LOG_DEBUG("Loading system defaults from ~s", [ConfigFile]),
load_config_file(ConfigFile, Mode)
end
end.
get_system_suffix() ->
case init:get_argument('-system') of
error ->
Pfx = os_env_prefix(),
case os:getenv(Pfx ++ "_SYSTEM") of
false ->
"";
Suffix ->
"_" ++ Suffix
end;
{ok, [[Suffix]]} ->
"_" ++ Suffix
end.
ensure_schema_loaded() ->
case pt_get_schema(undefined) of
undefined ->
Res = load_schema(),
%% ?LOG_INFO("Schema Res = ~p", [Res]),
Res;
S ->
S
end.
load_schema() ->
JSON = gmconfig_schema(),
Decode = maps:get("json", config_formats()),
Schema = decode_bin(JSON, Decode),
pt_set_schema(Schema),
Schema.
gmconfig_schema() ->
case gmconfig_env(schema, #{}) of
File when is_list(File) ->
json_consult(File);
Json when is_map(Json) ->
Json;
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.