Skip to main content

defect_config/
overrides.rs

1use std::path::PathBuf;
2
3use toml::Value as TomlValue;
4
5use crate::types::{CliOverrides, ConfigError, ConfigLayerEntry, ConfigSource};
6
7/// Parse a single CLI override in `KEY=VALUE` form.
8///
9/// # Errors
10///
11/// Returns [`ConfigError::Invalid`] if the input does not contain `=`, or if the override
12/// path is empty.
13pub fn parse_cli_override(spec: &str) -> Result<(String, TomlValue), ConfigError> {
14    let Some((path, raw_value)) = spec.split_once('=') else {
15        return Err(ConfigError::Invalid {
16            path: PathBuf::from("<cli>"),
17            message: format!("expected KEY=VALUE, got {spec:?}"),
18        });
19    };
20    let path = path.trim();
21    if path.is_empty() {
22        return Err(ConfigError::Invalid {
23            path: PathBuf::from("<cli>"),
24            message: format!("override path must not be empty in {spec:?}"),
25        });
26    }
27
28    let toml_snippet = format!("value = {}", raw_value.trim());
29    let value = match toml_snippet.parse::<TomlValue>() {
30        Ok(TomlValue::Table(mut table)) => table
31            .remove("value")
32            .unwrap_or_else(|| TomlValue::String(raw_value.trim().to_string())),
33        Ok(_) => unreachable!("wrapper snippet always parses to a table"),
34        Err(_) => TomlValue::String(raw_value.trim().to_string()),
35    };
36    Ok((path.to_string(), value))
37}
38
39pub(crate) fn build_cli_layer(cli: &CliOverrides) -> Result<Option<ConfigLayerEntry>, ConfigError> {
40    let mut root = TomlValue::Table(Default::default());
41    let mut has_values = false;
42
43    for (path, value) in &cli.config_overrides {
44        apply_toml_override(&mut root, path, value.clone());
45        has_values = true;
46    }
47    if let Some(provider) = &cli.provider {
48        apply_toml_override(
49            &mut root,
50            "default.provider",
51            TomlValue::String(provider.to_string()),
52        );
53        has_values = true;
54    }
55    if let Some(model) = &cli.model {
56        apply_toml_override(&mut root, "default.model", TomlValue::String(model.clone()));
57        has_values = true;
58    }
59    if let Some(sandbox) = &cli.sandbox {
60        apply_toml_override(
61            &mut root,
62            "sandbox.mode",
63            TomlValue::String(sandbox.as_str().to_string()),
64        );
65        has_values = true;
66    }
67    if let Some(log_format) = &cli.log_format {
68        apply_toml_override(
69            &mut root,
70            "tracing.format",
71            TomlValue::String(log_format.as_str().to_string()),
72        );
73        has_values = true;
74    }
75
76    if !has_values {
77        return Ok(None);
78    }
79
80    Ok(Some(ConfigLayerEntry {
81        source: ConfigSource::Cli,
82        path: None,
83        raw_toml: None,
84        value: root,
85    }))
86}
87
88pub(crate) fn merge_toml_values(base: &mut TomlValue, overlay: &TomlValue) {
89    if let TomlValue::Table(base_table) = base
90        && let TomlValue::Table(overlay_table) = overlay
91    {
92        for (key, value) in overlay_table {
93            if let Some(existing) = base_table.get_mut(key) {
94                merge_toml_values(existing, value);
95            } else {
96                base_table.insert(key.clone(), value.clone());
97            }
98        }
99    } else {
100        *base = overlay.clone();
101    }
102}
103
104pub(crate) fn apply_toml_override(root: &mut TomlValue, path: &str, value: TomlValue) {
105    use toml::map::Map;
106
107    let mut current = root;
108    let mut segments = path.split('.').peekable();
109    while let Some(segment) = segments.next() {
110        let is_last = segments.peek().is_none();
111        if is_last {
112            match current {
113                TomlValue::Table(table) => {
114                    table.insert(segment.to_string(), value);
115                }
116                _ => {
117                    let mut table = Map::new();
118                    table.insert(segment.to_string(), value);
119                    *current = TomlValue::Table(table);
120                }
121            }
122            return;
123        }
124
125        match current {
126            TomlValue::Table(table) => {
127                current = table
128                    .entry(segment.to_string())
129                    .or_insert_with(|| TomlValue::Table(Map::new()));
130            }
131            _ => {
132                *current = TomlValue::Table(Map::new());
133                let TomlValue::Table(table) = current else {
134                    unreachable!();
135                };
136                current = table
137                    .entry(segment.to_string())
138                    .or_insert_with(|| TomlValue::Table(Map::new()));
139            }
140        }
141    }
142}