greentic_dev/cmd/
config.rs

1use std::fs;
2use std::path::Path;
3
4use crate::cli::{ConfigCommand, ConfigSetArgs};
5use anyhow::{Context, Result, anyhow, bail};
6use toml_edit::{DocumentMut, Item, Table, value};
7
8use crate::config;
9
10pub fn run(command: ConfigCommand) -> Result<()> {
11    match command {
12        ConfigCommand::Set(args) => set_value(&args),
13    }
14}
15
16fn set_value(args: &ConfigSetArgs) -> Result<()> {
17    let path = match &args.file {
18        Some(path) => path.clone(),
19        None => config::config_path().ok_or_else(|| {
20            anyhow!("failed to resolve default config path (no home directory found)")
21        })?,
22    };
23
24    ensure_parent(&path)?;
25
26    let mut doc = if path.exists() {
27        let raw = fs::read_to_string(&path)
28            .with_context(|| format!("failed to read {}", path.display()))?;
29        if raw.trim().is_empty() {
30            DocumentMut::new()
31        } else {
32            raw.parse::<DocumentMut>()
33                .with_context(|| format!("failed to parse {}", path.display()))?
34        }
35    } else {
36        DocumentMut::new()
37    };
38
39    apply_key(&mut doc, &args.key, &args.value)?;
40
41    fs::write(&path, doc.to_string())
42        .with_context(|| format!("failed to write {}", path.display()))?;
43    println!("Updated {}", path.display());
44    Ok(())
45}
46
47fn ensure_parent(path: &Path) -> Result<()> {
48    if let Some(parent) = path.parent() {
49        fs::create_dir_all(parent)
50            .with_context(|| format!("failed to create {}", parent.display()))?;
51    }
52    Ok(())
53}
54
55fn apply_key(doc: &mut DocumentMut, key: &str, value_str: &str) -> Result<()> {
56    let segments = key
57        .split('.')
58        .filter(|segment| !segment.is_empty())
59        .collect::<Vec<_>>();
60    if segments.is_empty() {
61        bail!("config key cannot be empty");
62    }
63
64    let mut current = doc.as_table_mut();
65    for segment in &segments[..segments.len() - 1] {
66        current = current
67            .entry(segment)
68            .or_insert(Item::Table(Table::new()))
69            .as_table_mut()
70            .ok_or_else(|| anyhow!("path `{segment}` is not a table in the config"))?;
71    }
72
73    current.insert(segments.last().unwrap(), value(value_str));
74    Ok(())
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use tempfile::TempDir;
81
82    #[test]
83    fn creates_new_document() {
84        let temp = TempDir::new().unwrap();
85        let path = temp.path().join("config.toml");
86        let args = ConfigSetArgs {
87            key: "defaults.component.org".into(),
88            value: "ai.greentic".into(),
89            file: Some(path.clone()),
90        };
91        set_value(&args).unwrap();
92        let written = fs::read_to_string(path).unwrap();
93        assert!(written.contains("defaults"));
94        assert!(written.contains("ai.greentic"));
95    }
96
97    #[test]
98    fn updates_nested_tables() {
99        let temp = TempDir::new().unwrap();
100        let path = temp.path().join("config.toml");
101        fs::write(
102            &path,
103            r#"
104[defaults]
105[defaults.component]
106org = "ai.greentic"
107"#,
108        )
109        .unwrap();
110
111        let args = ConfigSetArgs {
112            key: "defaults.component.template".into(),
113            value: "rust-wasi".into(),
114            file: Some(path.clone()),
115        };
116        set_value(&args).unwrap();
117        let written = fs::read_to_string(path).unwrap();
118        assert!(written.contains("template = \"rust-wasi\""));
119    }
120}