Skip to content

Add ConfigAggregator for merging config state across multiple files#4830

Open
tarek-y-ismail wants to merge 7 commits intomainfrom
add-config-aggregator
Open

Add ConfigAggregator for merging config state across multiple files#4830
tarek-y-ismail wants to merge 7 commits intomainfrom
add-config-aggregator

Conversation

@tarek-y-ismail
Copy link
Copy Markdown
Contributor

Related: #4799

ConfigAggregator is a new Store implementation that merges configuration from multiple files into a single consistent view. It takes a ParsingStore (e.g. an IniFile) 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

  • New ConfigAggregator class implementing Store, which takes a ParsingStore and a list of config streams via load_all(). Later files take precedence for scalar keys; array keys accumulate across files and can be cleared with an empty assignment.
  • New ParsingStore abstract interface extending Store with a load_file() method, as a common base for format-specific parsers. IniFile now implements ParsingStore.
  • BasicStore extracted from IniFile to provide the reusable attribute registration, typed storage, and dispatch logic that ConfigAggregator builds on.

How to test

  • Existing IniFile tests continue to pass unchanged.
  • New ConfigAggregator tests cover scalar and array attribute handling, multi-file merging and override precedence, preset values, empty-value array clearing, done handlers, and error cases.

Checklist

  • Tests added and pass

@tarek-y-ismail tarek-y-ismail mentioned this pull request Apr 6, 2026
1 task
@tarek-y-ismail tarek-y-ismail force-pushed the add-config-aggregator branch from 5ad80ad to 57cb0ff Compare April 6, 2026 16:47
@tarek-y-ismail tarek-y-ismail changed the base branch from main to extract-basic-store April 6, 2026 16:50
@tarek-y-ismail tarek-y-ismail force-pushed the add-config-aggregator branch from 6665b12 to 617b76d Compare April 6, 2026 16:56
@tarek-y-ismail tarek-y-ismail self-assigned this Apr 6, 2026
@tarek-y-ismail tarek-y-ismail force-pushed the add-config-aggregator branch from 617b76d to 1c9d2dc Compare April 7, 2026 10:45
@tarek-y-ismail tarek-y-ismail force-pushed the add-config-aggregator branch from 1c9d2dc to 0b35184 Compare April 8, 2026 15:45
Copy link
Copy Markdown
Contributor

@Saviq Saviq left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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].

Comment thread tests/miral/config_aggregator.cpp Outdated
Comment thread tests/miral/config_aggregator.cpp Outdated
@tarek-y-ismail
Copy link
Copy Markdown
Contributor Author

Do we have a test for the main file providing invalid values - and that baiing, as it does today?

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.

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].

We do our parsing line by line, so in this case it will go like so:

# base.conf
# `aggregator.array = []`
array=foo  # base.array = [foo]
# End of base config, aggregator notified of `array += [foo]`. `aggregator.array = [foo]`

# base.conf.d/10-override.conf
array=bar # `10-override.array=[bar]`
{ "invalid": "stuff" } # Invalid format, ignored
array=baz # `10-override.array=[bar,baz]`
# end of override file, aggregator notified of `array += [bar,baz]`, `aggregator.array = [foo,bar,baz]`

We already have a couple of test cases that test simpler cases, but I'll add this one for completeness sake.

@tarek-y-ismail tarek-y-ismail force-pushed the add-config-aggregator branch from ffa3585 to 2f5f488 Compare April 10, 2026 09:28
Comment thread tests/miral/config_aggregator.cpp Outdated
Comment on lines +633 to +644
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"}});
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, this should yield 30.

@Saviq
Copy link
Copy Markdown
Contributor

Saviq commented Apr 10, 2026

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.

I'm just asking if we have a test for that. Not everyone will be using overrides.

This should keep [foo].

# end of override file, aggregator notified of `array += [bar,baz]`, `aggregator.array = [foo,bar,baz]`

That's wrong, then - the whole file needs to be rejected on parsing errors (parsing comes before validation).

@tarek-y-ismail tarek-y-ismail force-pushed the extract-basic-store branch 6 times, most recently from adec2d2 to ae2261b Compare April 14, 2026 15:28
Base automatically changed from extract-basic-store to main April 15, 2026 08:24
@tarek-y-ismail tarek-y-ismail force-pushed the add-config-aggregator branch from 2f5f488 to 9296cf2 Compare April 15, 2026 11:32
@tarek-y-ismail tarek-y-ismail changed the base branch from main to split-parsing-from-basic-store April 15, 2026 11:33
@tarek-y-ismail tarek-y-ismail force-pushed the add-config-aggregator branch from 9296cf2 to 019c166 Compare April 16, 2026 15:06
@tarek-y-ismail tarek-y-ismail force-pushed the split-parsing-from-basic-store branch from 325426d to bd05301 Compare April 16, 2026 15:18
@tarek-y-ismail tarek-y-ismail force-pushed the add-config-aggregator branch 2 times, most recently from 7563575 to 9e50ae8 Compare April 16, 2026 15:53
@tarek-y-ismail tarek-y-ismail force-pushed the split-parsing-from-basic-store branch 3 times, most recently from 1c0210c to 2f6d28d Compare April 22, 2026 14:59
@tarek-y-ismail tarek-y-ismail force-pushed the add-config-aggregator branch from 8a9431f to 9d9d66e Compare April 23, 2026 15:13
Comment thread src/miral/symbols.map
@tarek-y-ismail
Copy link
Copy Markdown
Contributor Author

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 ConfigAggregator, I'll probably split BasicStore and IniFile changes into a separate PR, as this one is already quite big.

@tarek-y-ismail tarek-y-ismail force-pushed the split-parsing-from-basic-store branch from c72ce7f to 1a4c0a1 Compare April 23, 2026 17:41
@tarek-y-ismail tarek-y-ismail force-pushed the add-config-aggregator branch from 04b180d to 7b331b4 Compare April 23, 2026 18:18
Comment thread src/miral/symbols.map
@tarek-y-ismail tarek-y-ismail force-pushed the add-config-aggregator branch from 7b331b4 to cf9c1d3 Compare April 30, 2026 14:31
@tarek-y-ismail tarek-y-ismail changed the base branch from split-parsing-from-basic-store to main April 30, 2026 14:31
@tarek-y-ismail tarek-y-ismail force-pushed the add-config-aggregator branch from cf9c1d3 to 5f707f5 Compare April 30, 2026 14:57
@tarek-y-ismail tarek-y-ismail marked this pull request as ready for review April 30, 2026 14:57
Copilot AI review requested due to automatic review settings April 30, 2026 14:57
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.
@tarek-y-ismail tarek-y-ismail force-pushed the add-config-aggregator branch from 5f707f5 to 0093d49 Compare April 30, 2026 15:01
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 (new Store implementation) and exported it in the ABI map/symbols.
  • Extended the live_config::Store interface with add_strings_attribute(..., initial_values) overloads and implemented them in IniFile/BasicStore.
  • Added a new miral-test unit 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.

Comment thread src/miral/config_aggregator.cpp Outdated
Comment thread src/miral/config_aggregator.cpp
Comment thread src/miral/basic_store.h Outdated
Comment thread src/miral/basic_store.cpp Outdated
Comment thread src/miral/basic_store.cpp
Copy link
Copy Markdown
Contributor

@AlanGriffiths AlanGriffiths left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is trying to do too many things and loses sight of the end goal. Let's talk Monday

Comment on lines +97 to +108
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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These functions do not belong in the Store (or IniFile) APIs.

  1. They are not part of the Store abstraction used to subscribe to configuration
  2. They are an implementation detail of ConfigAggregator
  3. Adding to the vtab breaks ABI

Comment on lines +30 to +35
struct Source
{
std::shared_ptr<Store> store;
std::move_only_function<void()> load;
std::filesystem::path file_path;
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/miral/basic_store.h
Comment on lines +69 to +78
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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.)

Comment thread src/miral/basic_store.h Outdated
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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 12 changed files in this pull request and generated 2 comments.

Comment on lines +101 to +107
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;
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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));
}

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +35
struct Source
{
std::shared_ptr<Store> store;
std::move_only_function<void()> load;
std::filesystem::path file_path;
};
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants