vtcode_config/loader/
builder.rs1use std::path::PathBuf;
2
3use anyhow::{Context, Result};
4
5use crate::loader::layers::{ConfigLayerEntry, ConfigLayerSource};
6use crate::loader::manager::ConfigManager;
7
8#[derive(Debug, Clone, Default)]
10#[must_use = "build the configuration before discarding the builder"]
11pub struct ConfigBuilder {
12 workspace: Option<PathBuf>,
13 config_file: Option<PathBuf>,
14 cli_overrides: Vec<(String, toml::Value)>,
15}
16
17impl ConfigBuilder {
18 pub fn new() -> Self {
20 Self::default()
21 }
22
23 pub fn workspace(mut self, path: PathBuf) -> Self {
25 self.workspace = Some(path);
26 self
27 }
28
29 pub fn config_file(mut self, path: PathBuf) -> Self {
31 self.config_file = Some(path);
32 self
33 }
34
35 pub fn cli_override(mut self, key: String, value: toml::Value) -> Self {
37 self.cli_overrides.push((key, value));
38 self
39 }
40
41 pub fn cli_overrides(mut self, overrides: &[(String, String)]) -> Self {
45 for (key, value) in overrides {
46 let toml_value = value
47 .parse::<toml::Value>()
48 .unwrap_or_else(|_| toml::Value::String(value.clone()));
49 self.cli_overrides.push((key.clone(), toml_value));
50 }
51 self
52 }
53
54 pub fn build(self) -> Result<ConfigManager> {
56 let workspace = self
57 .workspace
58 .clone()
59 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
60
61 let mut manager = if let Some(config_file) = self.config_file {
62 ConfigManager::load_from_file(config_file)?
63 } else {
64 ConfigManager::load_from_workspace_with_repository_repair(workspace)?
65 };
66
67 if !self.cli_overrides.is_empty() {
68 let mut runtime_toml = toml::Table::new();
69 for (key, value) in self.cli_overrides {
70 Self::insert_dotted_key(&mut runtime_toml, &key, value).context("Failed to apply CLI override")?;
71 }
72
73 let runtime_layer = ConfigLayerEntry::new(ConfigLayerSource::Runtime, toml::Value::Table(runtime_toml));
74
75 manager.layer_stack.push(runtime_layer);
76
77 let effective_toml = manager.layer_stack.effective_config();
79 manager.config = effective_toml
80 .try_into()
81 .context("Failed to deserialize effective configuration after runtime overrides")?;
82 manager.config.workspace_lifecycle_hooks =
83 Some(ConfigManager::collect_workspace_lifecycle_hooks(&manager.layer_stack, &manager.config.hooks));
84 manager
85 .config
86 .validate()
87 .context("Configuration failed validation after runtime overrides")?;
88 }
89
90 Ok(manager)
91 }
92
93 pub(crate) fn insert_dotted_key(table: &mut toml::Table, key: &str, value: toml::Value) -> Result<()> {
94 let parts: Vec<&str> = key.split('.').collect();
95 let mut current = table;
96 for (i, part) in parts.iter().enumerate() {
97 if i == parts.len() - 1 {
98 current.insert(part.to_string(), value);
99 return Ok(());
100 }
101
102 if !current.contains_key(*part) || !current[*part].is_table() {
103 current.insert(part.to_string(), toml::Value::Table(toml::Table::new()));
104 }
105
106 current = current
107 .get_mut(*part)
108 .and_then(|v| v.as_table_mut())
109 .with_context(|| format!("failed to descend into TOML table at `{part}` while inserting `{key}`"))?;
110 }
111 Ok(())
112 }
113}