From 3bba7116e51de1e7eecfce1a7c82a969c5ba12b4 Mon Sep 17 00:00:00 2001 From: speeddragon Date: Wed, 29 Jul 2026 18:41:50 +0100 Subject: [PATCH] feat: Support concatenation for root keys of the default message and loaded config --- src/core/http/hb_http_server.erl | 6 +- src/core/resolver/hb_opts.erl | 100 +++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 3 deletions(-) diff --git a/src/core/http/hb_http_server.erl b/src/core/http/hb_http_server.erl index 5d87fa00b..fca13870c 100644 --- a/src/core/http/hb_http_server.erl +++ b/src/core/http/hb_http_server.erl @@ -36,7 +36,7 @@ start() -> ?event(boot, {failed_to_load_config, Loc, Reason}), #{} end, - MergedConfig = hb_maps:merge(EnvConfig, Loaded), + MergedConfig = hb_opts:merge_config(EnvConfig, Loaded), hb_http_client:setup_conn(MergedConfig), %% Apply store defaults before starting store StoreOpts = hb_opts:get(store, no_store, MergedConfig), @@ -57,9 +57,9 @@ start() -> Loaded ) ), - maybe_greeter(Loaded, PrivWallet), + maybe_greeter(MergedConfig, PrivWallet), start( - Loaded#{ + MergedConfig#{ <<"priv-wallet">> => PrivWallet, <<"store">> => UpdatedStoreOpts, <<"port">> => hb_opts:get(port, 8734, Loaded), diff --git a/src/core/resolver/hb_opts.erl b/src/core/resolver/hb_opts.erl index b1acd6be8..9328303b3 100644 --- a/src/core/resolver/hb_opts.erl +++ b/src/core/resolver/hb_opts.erl @@ -15,6 +15,7 @@ -module(hb_opts). -export([get/1, get/2, get/3, as/2, identities/1, load/1, load/2, load_bin/2]). -export([default_message/0, default_message_with_env/0, mimic_default_types/3]). +-export([merge_config/2]). -export([ensure_node_history/2]). -export([check_required_opts/2]). -include("include/hb.hrl"). @@ -671,6 +672,40 @@ load_paths([P | Rest], Opts, Acc) -> {error, _} = Err -> Err end. +%% @doc Merge a loaded configuration into the default node message. Root keys +%% prefixed with `+' append to the default value, while suffixed keys prepend. +merge_config(Default, Loaded) -> + maps:fold( + fun(Key, Value, Acc) -> + case config_extension(Key) of + {append, BaseKey} -> + Acc#{ BaseKey => config_list(maps:get(BaseKey, Default, [])) + ++ config_list(Value) }; + {prepend, BaseKey} -> + Acc#{ BaseKey => config_list(Value) + ++ config_list(maps:get(BaseKey, Default, [])) }; + false -> Acc#{ Key => Value } + end + end, + Default, + Loaded + ). + +config_extension(<<"+", Rest/binary>>) when Rest =/= <<>> -> + case binary:last(Rest) of + $+ -> false; + _ -> {append, Rest} + end; +config_extension(Key) when is_binary(Key), byte_size(Key) > 1 -> + case binary:last(Key) of + $+ -> {prepend, binary:part(Key, 0, byte_size(Key) - 1)}; + _ -> false + end; +config_extension(_) -> false. + +config_list(Value) when is_list(Value) -> Value; +config_list(Value) when is_binary(Value); is_atom(Value); is_map(Value) -> [Value]. + %% @doc Convert a path to a device from its file extension. If no extension is %% provided, we default to `flat@1.0'. path_to_device(Path) -> @@ -1053,6 +1088,71 @@ load_multi_mixed_extensions_test() -> hb_maps:get(<<"store">>, Conf) ). +merge_config_extensions_test() -> + Default = #{ + <<"store">> => [#{ a => b }], + <<"atom">> => default, + <<"binary">> => <<"default">>, + <<"map">> => #{ default => true }, + <<"nested">> => #{ <<"+store">> => [nested] } + }, + ?assertEqual( + [#{ a => b }, #{ b => c }], + maps:get( + <<"store">>, + merge_config(Default, #{ <<"+store">> => [#{ b => c }] }) + ) + ), + ?assertEqual( + [#{ b => c }, #{ a => b }], + maps:get( + <<"store">>, + merge_config(Default, #{ <<"store+">> => #{ b => c } }) + ) + ), + ?assertEqual( + [default, loaded], + maps:get( + <<"atom">>, + merge_config(Default, #{ <<"+atom">> => loaded }) + ) + ), + ?assertEqual( + [<<"default">>, <<"loaded">>], + maps:get( + <<"binary">>, + merge_config(Default, #{ <<"+binary">> => <<"loaded">> }) + ) + ), + ?assertEqual( + [#{ loaded => true }, #{ default => true }], + maps:get( + <<"map">>, + merge_config(Default, #{ <<"map+">> => #{ loaded => true } }) + ) + ), + ?assertEqual( + #{ <<"+store">> => [loaded_nested] }, + maps:get( + <<"nested">>, + merge_config( + Default, + #{ <<"nested">> => #{ <<"+store">> => [loaded_nested] } } + ) + ) + ), + ?assertNot(maps:is_key( + <<"+store">>, + merge_config(Default, #{ <<"+store">> => [] }) + )), + ?assertEqual( + untouched, + maps:get( + <<"+store+">>, + merge_config(Default, #{ <<"+store+">> => untouched }) + ) + ). + preloaded_env_override_test() -> StorePath = "/tmp/hb-preloaded-env-test", os:putenv("HB_PRELOADED_STORE", StorePath),