Skip to content

Commit 92242d4

Browse files
committed
feat: add global configuration support for module initialization
1 parent 47252e3 commit 92242d4

File tree

2 files changed

+150
-34
lines changed

2 files changed

+150
-34
lines changed

process/src/process.rs

Lines changed: 33 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//! Loads and runs modules built with caryatid-sdk
33
44
use anyhow::{anyhow, Result};
5-
use caryatid_sdk::config::{config_from_value, get_sub_config};
5+
use caryatid_sdk::config::{build_module_config, config_from_value, get_sub_config};
66
use caryatid_sdk::context::GlobalContext;
77
use caryatid_sdk::{Context, MessageBounds, MessageBus, Module, ModuleRegistry};
88
use config::Config;
@@ -58,7 +58,7 @@ impl<M: MessageBounds> Process<M> {
5858
}
5959
};
6060

61-
return Ok(BusInfo { id, bus });
61+
Ok(BusInfo { id, bus })
6262
}
6363

6464
/// Create a process with the given config
@@ -78,7 +78,6 @@ impl<M: MessageBounds> Process<M> {
7878
Ok(bus) => {
7979
buses.push(Arc::new(bus));
8080
}
81-
8281
_ => {}
8382
}
8483
}
@@ -112,36 +111,36 @@ impl<M: MessageBounds> Process<M> {
112111
}
113112

114113
// Initialise all the modules from [module.<id>] configuration
115-
if let Ok(mod_confs) = self.config.get_table("module") {
116-
for (id, mod_conf) in mod_confs {
117-
if let Ok(modt) = mod_conf.into_table() {
118-
let modc = config_from_value(modt);
119-
let mut module_name = id.clone(); // Default
120-
if let Ok(class) = modc.get_string("class") {
121-
module_name = class;
122-
}
123-
124-
// Look up the module
125-
if let Some(module) = self.modules.get(&module_name) {
126-
info!("Initialising module {id}");
127-
let message_bus = self.context.message_bus.clone();
128-
let message_bus = if let Some(m) = &mut monitor {
129-
m.spy_on_bus(&module_name, message_bus)
130-
} else {
131-
message_bus
132-
};
133-
let context = Arc::new(Context::new(
134-
self.config.clone(),
135-
message_bus,
136-
self.context.startup_watch.subscribe(),
137-
));
138-
module.init(context, Arc::new(modc)).await.unwrap();
139-
} else {
140-
error!("Unrecognised module class: {module_name} in [module.{id}]");
141-
}
142-
} else {
114+
if let Ok(module_cfgs) = self.config.get_table("module") {
115+
for (id, module_cfg) in module_cfgs {
116+
let Ok(module_tbl) = module_cfg.into_table() else {
143117
warn!("Bad configuration for module {id} ignored");
144-
}
118+
continue;
119+
};
120+
121+
let module_cfg = build_module_config(&self.config, module_tbl);
122+
let module_name = module_cfg.get_string("class").unwrap_or_else(|_| id.clone());
123+
124+
let Some(module) = self.modules.get(&module_name) else {
125+
error!("Unrecognised module class: {module_name} in [module.{id}]");
126+
continue;
127+
};
128+
129+
info!("Initialising module {id}");
130+
131+
let message_bus = self.context.message_bus.clone();
132+
let message_bus = match &mut monitor {
133+
Some(m) => m.spy_on_bus(&module_name, message_bus),
134+
None => message_bus,
135+
};
136+
137+
let context = Arc::new(Context::new(
138+
self.config.clone(),
139+
message_bus,
140+
self.context.startup_watch.subscribe(),
141+
));
142+
143+
module.init(context, Arc::new(module_cfg)).await?;
145144
}
146145
}
147146

@@ -151,9 +150,9 @@ impl<M: MessageBounds> Process<M> {
151150
tokio::spawn(monitor.monitor());
152151
}
153152

154-
// Send startup message if required
153+
// Send the startup message if required
155154
let _ = self.context.startup_watch.send(true);
156-
if let Ok(topic) = self.config.get_string("startup.topic") {
155+
if let Ok(topic) = self.config.get_string("global.startup.topic") {
157156
self.context
158157
.message_bus
159158
.publish(&topic, Arc::new(M::default()))

sdk/src/config.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,120 @@ pub fn config_from_value(map: HashMap<String, config::Value>) -> Config {
2323
}
2424
builder.build().unwrap_or_default()
2525
}
26+
27+
/// Build a module config by merging the `[global]` section with module-specific config.
28+
///
29+
/// This allows modules to access shared configuration values defined in `[global.*]`
30+
/// while still having their own module-specific overrides. Module-specific values
31+
/// take precedence over global values when keys collide.
32+
///
33+
/// # Example TOML structure
34+
///
35+
/// ```toml
36+
/// [global.startup]
37+
/// method = "default"
38+
/// topic = "app.startup"
39+
///
40+
/// [module.my-module]
41+
/// some-setting = "value"
42+
/// ```
43+
///
44+
/// The resulting module config will contain both `startup.method` and `some-setting`.
45+
pub fn build_module_config(
46+
root_config: &Config,
47+
module_table: HashMap<String, config::Value>,
48+
) -> Config {
49+
Config::builder()
50+
.add_source(get_sub_config(root_config, "global"))
51+
.add_source(config_from_value(module_table))
52+
.build()
53+
.unwrap_or_default()
54+
}
55+
56+
#[cfg(test)]
57+
mod tests {
58+
use super::*;
59+
use config::FileFormat;
60+
61+
fn config_from_toml(toml: &str) -> Config {
62+
Config::builder()
63+
.add_source(config::File::from_str(toml, FileFormat::Toml))
64+
.build()
65+
.unwrap()
66+
}
67+
68+
#[test]
69+
fn test_get_sub_config_extracts_nested_section() {
70+
let config = config_from_toml(
71+
r#"
72+
[global.startup]
73+
method = "custom"
74+
75+
[global.network]
76+
name = "production"
77+
"#,
78+
);
79+
80+
let global = get_sub_config(&config, "global");
81+
82+
assert_eq!(global.get_string("startup.method").unwrap(), "custom");
83+
assert_eq!(global.get_string("network.name").unwrap(), "production");
84+
}
85+
86+
#[test]
87+
fn test_get_sub_config_returns_empty_for_missing_path() {
88+
let config = config_from_toml("[other]\nkey = \"value\"");
89+
let sub = get_sub_config(&config, "nonexistent");
90+
assert!(sub.get_string("anything").is_err());
91+
}
92+
93+
#[test]
94+
fn test_config_from_value_creates_config() {
95+
let mut map = HashMap::new();
96+
map.insert("key1".to_string(), config::Value::new(None, "value1"));
97+
map.insert("key2".to_string(), config::Value::new(None, 42_i64));
98+
99+
let config = config_from_value(map);
100+
101+
assert_eq!(config.get_string("key1").unwrap(), "value1");
102+
assert_eq!(config.get_int("key2").unwrap(), 42);
103+
}
104+
105+
#[test]
106+
fn test_build_module_config_merges_global_and_module() {
107+
let root_config = config_from_toml(
108+
r#"
109+
[global.startup]
110+
method = "default"
111+
"#,
112+
);
113+
114+
let mut module_table = HashMap::new();
115+
module_table.insert("name".to_string(), config::Value::new(None, "my-module"));
116+
117+
let module_cfg = build_module_config(&root_config, module_table);
118+
119+
assert_eq!(module_cfg.get_string("startup.method").unwrap(), "default");
120+
assert_eq!(module_cfg.get_string("name").unwrap(), "my-module");
121+
}
122+
123+
#[test]
124+
fn test_build_module_config_module_overrides_global() {
125+
let root_config = config_from_toml(
126+
r#"
127+
[global]
128+
name = "global-default"
129+
"#,
130+
);
131+
132+
let mut module_table = HashMap::new();
133+
module_table.insert(
134+
"name".to_string(),
135+
config::Value::new(None, "module-override"),
136+
);
137+
138+
let module_cfg = build_module_config(&root_config, module_table);
139+
140+
assert_eq!(module_cfg.get_string("name").unwrap(), "module-override");
141+
}
142+
}

0 commit comments

Comments
 (0)