Add ConfigAggregator for merging config state across multiple files#4830
Add ConfigAggregator for merging config state across multiple files#4830tarek-y-ismail wants to merge 7 commits intomainfrom
ConfigAggregator for merging config state across multiple files#4830Conversation
5ad80ad to
57cb0ff
Compare
d72b128 to
1b9e5af
Compare
6665b12 to
617b76d
Compare
ae0e7ca to
b64279e
Compare
617b76d to
1c9d2dc
Compare
b5a7472 to
cda1c1f
Compare
1c9d2dc to
0b35184
Compare
Saviq
left a comment
There was a problem hiding this comment.
There's a lot of comments saying "wins… holds invalid value… nullopt", but test expects that not to be true?
Do we have a test for the main file providing invalid values - and that baiing, as it does today?
I would also like a test with:
# base.conf
array=foo
# base.conf.d/10-override.conf
array=bar
{ "invalid": "stuff" }
array=baz
This should keep [foo].
Isn't the base config being valid a core assumption? With overrides, users don't have do touch that, so it shouldn't get corrupted anyway.
We do our parsing line by line, so in this case it will go like so: We already have a couple of test cases that test simpler cases, but I'll add this one for completeness sake. |
ffa3585 to
2f5f488
Compare
| TEST_F(ConfigAggregatorTest, invalid_scalar_value_in_one_of_multiple_yields_last_valid_value_2) | ||
| { | ||
| aggregator.add_int_attribute(a_key, "a scoped int", [this](auto... args) { int_handler(args...); }); | ||
|
|
||
| std::istringstream stream1{a_key.to_string() + "=10\n" + a_key.to_string() + "=20\n"}; | ||
| std::istringstream stream2{a_key.to_string() + "=30\n" + a_key.to_string() + "=not_a_number\n"}; | ||
|
|
||
| EXPECT_CALL(*this, int_handler(a_key, Optional(20))); | ||
|
|
||
| load_all({{std::ref(stream1), "base.conf"}, | ||
| {std::ref(stream2), "base.conf.d/10-override.conf"}}); | ||
| } |
There was a problem hiding this comment.
This shouldn't be the case. Instead, the final value should be 30 (we ignore just the invalid line), or the following test has the final value of 20 (we ignore the key for the whole file)
There was a problem hiding this comment.
Agree, this should yield 30.
I'm just asking if we have a test for that. Not everyone will be using overrides.
That's wrong, then - the whole file needs to be rejected on parsing errors (parsing comes before validation). |
adec2d2 to
ae2261b
Compare
2f5f488 to
9296cf2
Compare
9296cf2 to
019c166
Compare
325426d to
bd05301
Compare
7563575 to
9e50ae8
Compare
1c0210c to
2f6d28d
Compare
8a9431f to
9d9d66e
Compare
|
Array clearing was dropped somewhere along the way. Re-implemented it again, though For the sake of keeping this PR focused on changes needed for |
c72ce7f to
1a4c0a1
Compare
04b180d to
7b331b4
Compare
7b331b4 to
cf9c1d3
Compare
cf9c1d3 to
5f707f5
Compare
ConfigAggregator merges configuration state across multiple files by combining a set of Sources (for format-specific parsing) with a BasicStore (for config value accumulation). Attribute registrations are forwarded to both: the parser accumulates raw string values into the aggregated store, and the store holds the user handlers and presets. load_all() clears prior state, loads each file in order through the parser, then dispatches all handlers once with the merged result and calls done handlers with the list of loaded paths.
5f707f5 to
0093d49
Compare
There was a problem hiding this comment.
Pull request overview
This PR introduces a new MirAL live-config utility (ConfigAggregator) intended to provide a single merged configuration view from multiple layered config sources (base + drop-ins), plus supporting API changes to enable array “initial values” propagation during parsing.
Changes:
- Added
miral::live_config::ConfigAggregator(newStoreimplementation) and exported it in the ABI map/symbols. - Extended the
live_config::Storeinterface withadd_strings_attribute(..., initial_values)overloads and implemented them inIniFile/BasicStore. - Added a new
miral-testunit test suite covering multi-file merge behavior for scalar/array keys.
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
include/miral/miral/config_aggregator.h |
Public API for the new ConfigAggregator and Source model. |
src/miral/config_aggregator.cpp |
Implementation of config merging across multiple sources using BasicStore transactions. |
include/miral/miral/live_config.h |
Adds new Store overloads for array attributes with initial_values. |
include/miral/miral/live_config_ini_file.h / src/miral/live_config_ini_file.cpp |
Implements the new Store overloads in IniFile. |
src/miral/basic_store.h / src/miral/basic_store.cpp |
Adds array-setting and attribute enumeration helpers used by the aggregator. |
src/miral/symbols.map / debian/libmiral7.symbols |
Exports the new API under MIRAL_5.8. |
tests/miral/config_aggregator.cpp |
New gtest/gmock coverage for layered merging semantics. |
tests/miral/CMakeLists.txt / src/miral/CMakeLists.txt |
Builds/links the new implementation and tests. |
absent from override
AlanGriffiths
left a comment
There was a problem hiding this comment.
I think this is trying to do too many things and loses sight of the end goal. Let's talk Monday
| virtual void add_strings_attribute( | ||
| Key const& key, | ||
| std::string_view description, | ||
| HandleStrings handler, | ||
| std::span<std::string const> initial_values) = 0; | ||
| virtual void add_strings_attribute( | ||
| Key const& key, | ||
| std::string_view description, | ||
| std::span<std::string const> preset, | ||
| HandleStrings handler, | ||
| std::span<std::string const> initial_values) = 0; | ||
|
|
There was a problem hiding this comment.
These functions do not belong in the Store (or IniFile) APIs.
- They are not part of the
Storeabstraction used to subscribe to configuration - They are an implementation detail of
ConfigAggregator - Adding to the vtab breaks ABI
| struct Source | ||
| { | ||
| std::shared_ptr<Store> store; | ||
| std::move_only_function<void()> load; | ||
| std::filesystem::path file_path; | ||
| }; |
There was a problem hiding this comment.
Having structs like this in the API is a recipe for unstable APIs.
|
|
||
| void add_source(Source&& source); | ||
| void update_source(Source&& source); | ||
| void remove_source(std::filesystem::path const& path); |
There was a problem hiding this comment.
This might make sense in an internal API, but is not what we want as a public API for this.
The end user shouldn't need to deal with sources, they should simply specify the type the sources and the files to monitor/aggregate. Any adding or deleting of sources should be done internally in response to filesystem changes.
| using ForeachAttributeCallback = | ||
| std::function<void(Key const&, std::string_view, std::optional<std::string> const& preset)>; | ||
| using ForeachArrayCallback = std::function<void( | ||
| Key const&, | ||
| std::string_view, | ||
| std::span<std::string const> current_value, | ||
| std::optional<std::span<std::string const>> preset)>; | ||
|
|
||
| void foreach_attribute(ForeachAttributeCallback const& callback) const; | ||
| void foreach_array_attribute(ForeachArrayCallback const& callback) const; |
There was a problem hiding this comment.
These functions appear to poking holes in the BasicStore abstraction - which suggests the abstraction is wrong. Or that we need a different abstraction.
Specifically, there appears to be a missing AttributesCollection that both BasicStore and ConfigAggregator can use to maintain a list of attributes. (Instead of poking a hole in BasicStore so that ConfigAggregator can get at the attributes.)
| void foreach_array_attribute(ForeachArrayCallback const& callback) const; | ||
|
|
||
| void update_key(Key const& key, std::string_view value, std::filesystem::path const& modification_path); | ||
| void set_array(Key const& key, std::optional<std::span<std::string const>> value, std::filesystem::path const& modification_path); |
There was a problem hiding this comment.
These functions appear to poking holes in the BasicStore abstraction - which suggests the abstraction is wrong. Or that we need a different abstraction.
Specifically, BasicStore is not basic. It has IniFile specific logic for accumulating arrays. This set_array() function is an alternative implementation, not one that belongs in the same class.
Maybe we can "push down" the IniFile specifics into BasicStore::Self and leave something truly basic?
| std::span<std::string const> initial_values) = 0; | ||
| virtual void add_strings_attribute( | ||
| Key const& key, | ||
| std::string_view description, | ||
| std::span<std::string const> preset, | ||
| HandleStrings handler, | ||
| std::span<std::string const> initial_values) = 0; |
There was a problem hiding this comment.
Adding new pure-virtual overloads to miral::live_config::Store changes the required interface for all Store implementations and (because Store is a public virtual type with exported vtable/typeinfo) is an ABI-breaking change for out-of-tree implementers. If this is intended to remain ABI-stable within libmiral.so.7, consider introducing a separate derived interface for the new array-initial-values behavior (or providing a non-pure default implementation) rather than extending Store directly.
| std::span<std::string const> initial_values) = 0; | |
| virtual void add_strings_attribute( | |
| Key const& key, | |
| std::string_view description, | |
| std::span<std::string const> preset, | |
| HandleStrings handler, | |
| std::span<std::string const> initial_values) = 0; | |
| std::span<std::string const> initial_values) | |
| { | |
| static_cast<void>(initial_values); | |
| add_strings_attribute(key, description, std::move(handler)); | |
| } | |
| virtual void add_strings_attribute( | |
| Key const& key, | |
| std::string_view description, | |
| std::span<std::string const> preset, | |
| HandleStrings handler, | |
| std::span<std::string const> initial_values) | |
| { | |
| static_cast<void>(initial_values); | |
| add_strings_attribute(key, description, preset, std::move(handler)); | |
| } |
| struct Source | ||
| { | ||
| std::shared_ptr<Store> store; | ||
| std::move_only_function<void()> load; | ||
| std::filesystem::path file_path; | ||
| }; |
There was a problem hiding this comment.
PR description mentions a new ParsingStore interface, but there is no ParsingStore type in the tree and ConfigAggregator::Source accepts a std::shared_ptr<Store> instead. Either the description needs updating, or the intended ParsingStore abstraction should be added (especially if the goal was to avoid extending Store’s virtual interface).
Related: #4799
ConfigAggregatoris a newStoreimplementation that merges configuration from multiple files into a single consistent view. It takes aParsingStore(e.g. anIniFile) and a list of streams, parses each in order, accumulates all values, and dispatches handlers exactly once with the final merged result.This is the intended way to support layered configuration — a base file plus a drop-in directory of overrides — without callers having to manage accumulation themselves.
What's new
ConfigAggregatorclass implementingStore, which takes aParsingStoreand a list of config streams viaload_all(). Later files take precedence for scalar keys; array keys accumulate across files and can be cleared with an empty assignment.ParsingStoreabstract interface extendingStorewith aload_file()method, as a common base for format-specific parsers.IniFilenow implementsParsingStore.BasicStoreextracted fromIniFileto provide the reusable attribute registration, typed storage, and dispatch logic thatConfigAggregatorbuilds on.How to test
IniFiletests continue to pass unchanged.ConfigAggregatortests cover scalar and array attribute handling, multi-file merging and override precedence, preset values, empty-value array clearing, done handlers, and error cases.Checklist