Skip to main content

vtcode_config/loader/
builder.rs

1use std::path::PathBuf;
2
3use anyhow::{Context, Result};
4
5use crate::loader::layers::{ConfigLayerEntry, ConfigLayerSource};
6use crate::loader::manager::ConfigManager;
7
8/// Builder for creating a [`ConfigManager`] with custom overrides.
9#[derive(Debug, Clone, Default)]
10pub struct ConfigBuilder {
11    workspace: Option<PathBuf>,
12    config_file: Option<PathBuf>,
13    cli_overrides: Vec<(String, toml::Value)>,
14}
15
16impl ConfigBuilder {
17    /// Create a new configuration builder.
18    pub fn new() -> Self {
19        Self::default()
20    }
21
22    /// Set the workspace directory.
23    pub fn workspace(mut self, path: PathBuf) -> Self {
24        self.workspace = Some(path);
25        self
26    }
27
28    /// Set a specific configuration file to use instead of the default workspace config.
29    pub fn config_file(mut self, path: PathBuf) -> Self {
30        self.config_file = Some(path);
31        self
32    }
33
34    /// Add a CLI override (e.g., "agent.provider", "openai").
35    pub fn cli_override(mut self, key: String, value: toml::Value) -> Self {
36        self.cli_overrides.push((key, value));
37        self
38    }
39
40    /// Add multiple CLI overrides from string pairs.
41    ///
42    /// Values are parsed as TOML. If parsing fails, they are treated as strings.
43    pub fn cli_overrides(mut self, overrides: &[(String, String)]) -> Self {
44        for (key, value) in overrides {
45            let toml_value = value
46                .parse::<toml::Value>()
47                .unwrap_or_else(|_| toml::Value::String(value.clone()));
48            self.cli_overrides.push((key.clone(), toml_value));
49        }
50        self
51    }
52
53    /// Build the [`ConfigManager`].
54    pub fn build(self) -> Result<ConfigManager> {
55        let workspace = self
56            .workspace
57            .clone()
58            .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
59
60        let mut manager = if let Some(config_file) = self.config_file {
61            ConfigManager::load_from_file(config_file)?
62        } else {
63            ConfigManager::load_from_workspace(workspace)?
64        };
65
66        if !self.cli_overrides.is_empty() {
67            let mut runtime_toml = toml::Table::new();
68            for (key, value) in self.cli_overrides {
69                Self::insert_dotted_key(&mut runtime_toml, &key, value);
70            }
71
72            let runtime_layer =
73                ConfigLayerEntry::new(ConfigLayerSource::Runtime, toml::Value::Table(runtime_toml));
74
75            manager.layer_stack.push(runtime_layer);
76
77            // Re-evaluate config
78            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.apply_compat_defaults();
83            manager
84                .config
85                .validate()
86                .context("Configuration failed validation after runtime overrides")?;
87        }
88
89        Ok(manager)
90    }
91
92    pub(crate) fn insert_dotted_key(table: &mut toml::Table, key: &str, value: toml::Value) {
93        let parts: Vec<&str> = key.split('.').collect();
94        let mut current = table;
95        for (i, part) in parts.iter().enumerate() {
96            if i == parts.len() - 1 {
97                current.insert(part.to_string(), value);
98                return;
99            }
100
101            if !current.contains_key(*part) || !current[*part].is_table() {
102                current.insert(part.to_string(), toml::Value::Table(toml::Table::new()));
103            }
104
105            current = current
106                .get_mut(*part)
107                .and_then(|v| v.as_table_mut())
108                .unwrap_or_else(|| unreachable!("inserted table should remain a table"));
109        }
110    }
111}