defect_config/
overrides.rs1use std::path::PathBuf;
2
3use toml::Value as TomlValue;
4
5use crate::types::{CliOverrides, ConfigError, ConfigLayerEntry, ConfigSource};
6
7pub 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}