gmconfig is a configuration management application derived from the configuration subsystem of Aeternity and Gajumaru. It sports a pretty complete JSON-Schema validator and an efficient configuration lookup API.
In this update, the schema validator has been improved to work reliably on large OAS3 specs, including nested entity references, type coercion and custom validator extensions.
The idea of gmconfig is to simplify management and use of configuration schemas. Some examples might help to illustrate:
Setting things up:
instrument_gmconfig() ->
gmconfig:set_gmconfig_env(gmconfig_env()).
-spec gmconfig_env() -> gmconfig:gmconfig().
gmconfig_env() ->
#{ os_env_prefix => “GMHC”
, config_file_basename => “gmhive_client_config”
, config_file_os_env => “GMHIVE_CLIENT_CONFIG”
, config_file_search_path => [“.”, fun setup:home/0, fun setup:data_dir/0 \]
, config_plain_args => “-gmhc”
, system_suffix => “”
, schema => fun gmhc_config_schema:to_json/0 }.
The schema could be a JSON file, a YAML file (different encoder can be plugged in), or an Erlang module, as in this case, producing a schema in an “internal JSON” representation. Example:
schema() ->
obj(schema_init(),
#{
network => str(#{ default => <<"mainnet">> })
, pubkey => str(#{pattern => ?ACCOUNT_PATTERN},
<<"Primary client pubkey">>)
, extra_pubkeys => array(#{ description =>
<<"Additional worker pubkeys, sharing rewards">>
, default => []
},
str(#{pattern => ?ACCOUNT_PATTERN}))
, type => str(#{ enum => [<<"worker">>, <<"monitor">>]
, default => <<"worker">>
, description => <<"monitor mode can be used to see if a pool is alive">>})
...
I personally find this much easier to maintain. The config file is validated at load-time against the schema, and stored as a persistent term.
The schema is loaded as a persistent term (although the API allows for providing a schema as an argument). A typical config lookup would look like:
try_noise_connect(Opts) ->
Host = binary_to_list(gmhc_config:get_config([<<"pool">>, <<"host">>])),
Port = gmhc_config:get_config([<<"pool">>, <<"port">>]),
noise_connect(maps:merge(#{host => Host, port => Port}, Opts)).
As both the config file and schema are stored as persistent terms, both the user-provided values and schema defaults are available with minimal overhead. The typical lookup semantics are (1) look for a user-provided value (user_config), then (2) look for a schema default (schema_default), but custom strategies can be specified, including checking an OS environment variable, application environment variable, or using a hard-coded default. Of course, a corresponding find_config(...) is available.
The configuration can be overriden from the command line, or via OS environment variables. In the example above, the OS env prefix “GMHC” means that a variable like GMHC__POOL__HOST would set a value corresponding to the [<<"pool">>, <<"host">>] lookup. The OS env var overrides are treated as JSON input and also validated against the schema.
The API is still in need of some cleanup, but in my experience, the steps illustrated above go a long way.