hb_store_opts.erl - Store Configuration Defaults Manager
Overview
Purpose: Apply default configuration values to store options based on store type
Module: hb_store_opts
Pattern: Recursive map merger with module-specific defaults
This module takes store options and store defaults, applying type-specific default properties based on the store-module field. Supports recursive application to nested store configurations, allowing complex multi-layered store hierarchies to inherit appropriate defaults.
Dependencies
- Erlang/OTP:
lists,maps - Records: None
Public Functions Overview
%% Configuration Management
-spec apply(StoreOpts, Defaults) -> UpdatedStoreOpts
when
StoreOpts :: [map()],
Defaults :: map(),
UpdatedStoreOpts :: [map()].Public Functions
1. apply/2
-spec apply(StoreOpts, Defaults) -> UpdatedStoreOpts
when
StoreOpts :: [map()],
Defaults :: map(),
UpdatedStoreOpts :: [map()].Description: Apply store defaults to a list of store configuration maps. For each store in the list, applies defaults based on its store-module type and recursively processes any nested stores in the <<"store">> field. Store options take precedence over defaults.
hb_store_lmdb→ Uses<<"lmdb">>defaultshb_store_fs→ Uses<<"fs">>defaultshb_store_rocksdb→ Uses<<"rocksdb">>defaultshb_store_gateway→ Uses<<"gateway">>defaults
-module(test_hb_store_opts).
-include_lib("eunit/include/eunit.hrl").
%% Basic apply tests
apply_single_store_test() ->
StoreOpts = [
#{
<<"name">> => <<"cache-mainnet/lmdb">>,
<<"store-module">> => hb_store_lmdb
}
],
Defaults = #{
<<"lmdb">> => #{
<<"capacity">> => 1073741824
}
},
Result = hb_store_opts:apply(StoreOpts, Defaults),
[Updated] = Result,
?assertEqual(1073741824, maps:get(<<"capacity">>, Updated)),
?assertEqual(<<"cache-mainnet/lmdb">>, maps:get(<<"name">>, Updated)).
apply_empty_defaults_test() ->
StoreOpts = [
#{
<<"name">> => <<"test">>,
<<"store-module">> => hb_store_lmdb
}
],
Defaults = #{},
Result = hb_store_opts:apply(StoreOpts, Defaults),
?assertEqual(StoreOpts, Result).
apply_empty_store_opts_test() ->
StoreOpts = [],
Defaults = #{<<"lmdb">> => #{<<"capacity">> => 1000}},
Result = hb_store_opts:apply(StoreOpts, Defaults),
?assertEqual([], Result).
apply_multiple_stores_test() ->
StoreOpts = [
#{<<"name">> => <<"lmdb1">>, <<"store-module">> => hb_store_lmdb},
#{<<"name">> => <<"lmdb2">>, <<"store-module">> => hb_store_lmdb},
#{<<"name">> => <<"fs1">>, <<"store-module">> => hb_store_fs}
],
Defaults = #{
<<"lmdb">> => #{<<"capacity">> => 5000},
<<"fs">> => #{<<"buffer-size">> => 1024}
},
Result = hb_store_opts:apply(StoreOpts, Defaults),
[Lmdb1, Lmdb2, Fs1] = Result,
?assertEqual(5000, maps:get(<<"capacity">>, Lmdb1)),
?assertEqual(5000, maps:get(<<"capacity">>, Lmdb2)),
?assertEqual(1024, maps:get(<<"buffer-size">>, Fs1)),
?assertEqual(false, maps:is_key(<<"capacity">>, Fs1)).
%% Module type tests
apply_lmdb_defaults_test() ->
StoreOpt = #{
<<"name">> => <<"test">>,
<<"store-module">> => hb_store_lmdb
},
Defaults = #{
<<"lmdb">> => #{
<<"capacity">> => 1000,
<<"sync">> => true
}
},
Result = hb_store_opts:apply([StoreOpt], Defaults),
[Updated] = Result,
?assertEqual(1000, maps:get(<<"capacity">>, Updated)),
?assertEqual(true, maps:get(<<"sync">>, Updated)).
apply_fs_defaults_test() ->
StoreOpt = #{
<<"name">> => <<"test">>,
<<"store-module">> => hb_store_fs
},
Defaults = #{
<<"fs">> => #{<<"buffer-size">> => 2048}
},
Result = hb_store_opts:apply([StoreOpt], Defaults),
[Updated] = Result,
?assertEqual(2048, maps:get(<<"buffer-size">>, Updated)).
apply_rocksdb_defaults_test() ->
StoreOpt = #{
<<"name">> => <<"test">>,
<<"store-module">> => hb_store_rocksdb
},
Defaults = #{
<<"rocksdb">> => #{<<"block-size">> => 8192}
},
Result = hb_store_opts:apply([StoreOpt], Defaults),
[Updated] = Result,
?assertEqual(8192, maps:get(<<"block-size">>, Updated)).
apply_gateway_defaults_test() ->
StoreOpt = #{
<<"name">> => <<"test">>,
<<"store-module">> => hb_store_gateway
},
Defaults = #{
<<"gateway">> => #{<<"timeout">> => 30000}
},
Result = hb_store_opts:apply([StoreOpt], Defaults),
[Updated] = Result,
?assertEqual(30000, maps:get(<<"timeout">>, Updated)).
apply_unknown_module_test() ->
StoreOpt = #{
<<"name">> => <<"test">>,
<<"store-module">> => unknown_module
},
Defaults = #{<<"lmdb">> => #{<<"capacity">> => 1000}},
Result = hb_store_opts:apply([StoreOpt], Defaults),
[Updated] = Result,
?assertEqual(StoreOpt, Updated).
%% Store options precedence
apply_store_opts_precedence_test() ->
StoreOpt = #{
<<"name">> => <<"test">>,
<<"store-module">> => hb_store_lmdb,
<<"capacity">> => 9999 %% User-specified
},
Defaults = #{
<<"lmdb">> => #{
<<"capacity">> => 1000, %% Default
<<"sync">> => true %% Additional default
}
},
Result = hb_store_opts:apply([StoreOpt], Defaults),
[Updated] = Result,
?assertEqual(9999, maps:get(<<"capacity">>, Updated)), %% Kept from store
?assertEqual(true, maps:get(<<"sync">>, Updated)). %% Added from defaults
%% Nested store tests
apply_nested_stores_test() ->
StoreOpts = [
#{
<<"store-module">> => hb_store_gateway,
<<"store">> => [
#{
<<"name">> => <<"cache-mainnet/lmdb">>,
<<"store-module">> => hb_store_lmdb
}
]
}
],
Defaults = #{
<<"lmdb">> => #{<<"capacity">> => 1073741824}
},
Result = hb_store_opts:apply(StoreOpts, Defaults),
[Gateway] = Result,
[NestedLmdb] = maps:get(<<"store">>, Gateway),
?assertEqual(1073741824, maps:get(<<"capacity">>, NestedLmdb)).
apply_deeply_nested_stores_test() ->
StoreOpts = [
#{
<<"store-module">> => hb_store_gateway,
<<"store">> => [
#{
<<"store-module">> => hb_store_lru,
<<"store">> => [
#{
<<"name">> => <<"lmdb">>,
<<"store-module">> => hb_store_lmdb
}
]
}
]
}
],
Defaults = #{<<"lmdb">> => #{<<"capacity">> => 5000}},
Result = hb_store_opts:apply(StoreOpts, Defaults),
[Gateway] = Result,
[Lru] = maps:get(<<"store">>, Gateway),
[Lmdb] = maps:get(<<"store">>, Lru),
?assertEqual(5000, maps:get(<<"capacity">>, Lmdb)).
apply_nested_with_parent_defaults_test() ->
StoreOpts = [
#{
<<"store-module">> => hb_store_gateway,
<<"store">> => [
#{
<<"name">> => <<"nested">>,
<<"store-module">> => hb_store_lmdb
}
]
}
],
Defaults = #{
<<"gateway">> => #{<<"timeout">> => 30000},
<<"lmdb">> => #{<<"capacity">> => 5000}
},
Result = hb_store_opts:apply(StoreOpts, Defaults),
[Gateway] = Result,
?assertEqual(30000, maps:get(<<"timeout">>, Gateway)),
[NestedLmdb] = maps:get(<<"store">>, Gateway),
?assertEqual(5000, maps:get(<<"capacity">>, NestedLmdb)).Internal Functions
apply_defaults_to_store/2
-spec apply_defaults_to_store(StoreOpt, Defaults) -> UpdatedStoreOpt
when
StoreOpt :: map(),
Defaults :: map(),
UpdatedStoreOpt :: map().Description: Apply defaults to a single store configuration by first applying module-type defaults, then recursively processing any sub-stores.
apply_defaults_by_module_type/2
-spec apply_defaults_by_module_type(StoreOpt, Defaults) -> UpdatedStoreOpt
when
StoreOpt :: map(),
Defaults :: map(),
UpdatedStoreOpt :: map().Description: Apply type-specific defaults based on the <<"store-module">> field. If the module type has defaults defined, merges them with the store options (store options take precedence).
apply_type_defaults/3
-spec apply_type_defaults(StoreOpt, TypeKey, Defaults) -> UpdatedStoreOpt
when
StoreOpt :: map(),
TypeKey :: binary(),
Defaults :: map(),
UpdatedStoreOpt :: map().Description: Apply defaults for a specific type key by merging type-specific defaults with the store options. Store options take precedence over defaults.
apply_defaults_to_substores/2
-spec apply_defaults_to_substores(StoreOpt, Defaults) -> UpdatedStoreOpt
when
StoreOpt :: map(),
Defaults :: map(),
UpdatedStoreOpt :: map().Description: Recursively apply defaults to nested stores found in the <<"store">> field. Processes each sub-store independently.
Common Patterns
%% Basic store defaults application
DefaultStoreOpts = [
#{
<<"name">> => <<"cache-mainnet/lmdb">>,
<<"store-module">> => hb_store_lmdb
}
],
StoreDefaults = #{
<<"lmdb">> => #{
<<"capacity">> => 16_000_000_000
}
},
UpdatedStoreOpts = hb_store_opts:apply(DefaultStoreOpts, StoreDefaults).
%% Multiple store types with different defaults
MultiStoreOpts = [
#{
<<"name">> => <<"cache/lmdb">>,
<<"store-module">> => hb_store_lmdb
},
#{
<<"name">> => <<"cache/fs">>,
<<"store-module">> => hb_store_fs
},
#{
<<"name">> => <<"cache/rocks">>,
<<"store-module">> => hb_store_rocksdb
}
],
Defaults = #{
<<"lmdb">> => #{
<<"capacity">> => 16_000_000_000,
<<"no-sync">> => true
},
<<"fs">> => #{
<<"buffer-size">> => 4096
},
<<"rocksdb">> => #{
<<"block-size">> => 8192
}
},
UpdatedMultiStoreOpts = hb_store_opts:apply(MultiStoreOpts, Defaults).
%% Nested store configuration (gateway → lmdb)
NestedStoreOpts = [
#{
<<"store-module">> => hb_store_gateway,
<<"gateway">> => <<"https://arweave.net">>,
<<"store">> => [
#{
<<"name">> => <<"cache-mainnet/lmdb">>,
<<"store-module">> => hb_store_lmdb
}
]
}
],
NestedDefaults = #{
<<"lmdb">> => #{
<<"capacity">> => 10_000_000_000
},
<<"gateway">> => #{
<<"timeout">> => 30000
}
},
UpdatedNestedOpts = hb_store_opts:apply(NestedStoreOpts, NestedDefaults).
%% Complex multi-layer hierarchy (gateway → lru → lmdb)
ComplexStoreOpts = [
#{
<<"store-module">> => hb_store_gateway,
<<"store">> => [
#{
<<"store-module">> => hb_store_lru,
<<"name">> => <<"main-cache">>,
<<"store">> => [
#{
<<"name">> => <<"persistent">>,
<<"store-module">> => hb_store_lmdb
}
]
}
]
}
],
ComplexDefaults = #{
<<"lmdb">> => #{<<"capacity">> => 5_000_000_000},
<<"lru">> => #{<<"capacity">> => 1_000_000_000}
},
UpdatedComplexOpts = hb_store_opts:apply(ComplexStoreOpts, ComplexDefaults).
%% Integration with hb_http_server configuration
LoadedConfig = #{
<<"store_defaults">> => #{
<<"lmdb">> => #{<<"capacity">> => 5000}
}
},
DefaultStores = [
#{
<<"name">> => <<"cache-mainnet/lmdb">>,
<<"store-module">> => hb_store_lmdb
}
],
MergedConfig = maps:merge(
#{<<"store">> => DefaultStores},
LoadedConfig
),
FinalStoreOpts = hb_store_opts:apply(
maps:get(<<"store">>, MergedConfig),
maps:get(<<"store_defaults">>, MergedConfig, #{})
).Module Type Mappings
Supported Mappings
| Store Module | Default Key | Example Defaults |
|---|---|---|
hb_store_lmdb | <<"lmdb">> | #{<<"capacity">> => 16GB} |
hb_store_fs | <<"fs">> | #{<<"buffer-size">> => 4096} |
hb_store_rocksdb | <<"rocksdb">> | #{<<"block-size">> => 8192} |
hb_store_gateway | <<"gateway">> | #{<<"timeout">> => 30000} |
| Other modules | None | No defaults applied |
Merge Behavior
Priority Rules
When merging defaults with store options:
% Store options take precedence over defaults
StoreOpt = #{
<<"name">> => <<"test">>,
<<"capacity">> => 1000 % User-specified
},
Defaults = #{
<<"lmdb">> => #{
<<"capacity">> => 5000, % Default value
<<"sync">> => true % Additional default
}
},
% Result:
#{
<<"name">> => <<"test">>,
<<"capacity">> => 1000, % Kept from StoreOpt
<<"sync">> => true % Added from Defaults
}Recursive Application
Level 1: Gateway Store
├─ Apply gateway defaults
└─ Process <<"store">> field recursively
│
Level 2: LRU Store
├─ Apply lru defaults
└─ Process <<"store">> field recursively
│
Level 3: LMDB Store
└─ Apply lmdb defaultsConfiguration Examples
Typical Server Configuration
% In hb_http_server.erl
DefaultStoreOpts = [
#{
<<"name">> => <<"cache-mainnet/lmdb">>,
<<"store-module">> => hb_store_lmdb
},
#{
<<"store-module">> => hb_store_fs,
<<"name">> => <<"cache-mainnet">>
}
],
StoreDefaults = #{
<<"lmdb">> => #{
<<"capacity">> => 16 * 1024 * 1024 * 1024
},
<<"fs">> => #{
<<"buffer-size">> => 4096
}
},
FinalStoreOpts = hb_store_opts:apply(DefaultStoreOpts, StoreDefaults).Custom Capacity Configuration
% Load from configuration file
LoadedDefaults = #{
<<"store_defaults">> => #{
<<"lmdb">> => #{
<<"capacity">> => 5_000_000_000 % 5GB custom
}
}
},
% Apply to store options
UpdatedOpts = hb_store_opts:apply(
StoreOpts,
maps:get(<<"store_defaults">>, LoadedDefaults, #{})
).References
- hb_store - HyperBEAM store interface
- hb_http_server - Uses this module for configuration
- Store Modules - hb_store_lmdb, hb_store_fs, hb_store_rocksdb, etc.
Notes
- No Auto-Import: Module disables auto-import for
apply/2to avoid conflicts - Recursive Processing: Handles arbitrarily nested store hierarchies
- Type-Based Defaults: Different stores get different default configurations
- Merge Strategy: Store options always take precedence over defaults
- Unknown Modules: Silently skips modules without defined defaults
- List Processing: Always operates on lists of store configurations
- Immutable Operations: Returns new maps, doesn't modify originals
- Empty Handling: Gracefully handles empty store lists and default maps
- Configuration Files: Designed for integration with external configuration
- Server Integration: Used by hb_http_server during initialization