greentic-dev 0.4.68

Developer CLI and local tooling for Greentic flows, packs, and components
Documentation
use std::fs;
use std::path::Path;

use crate::cli::{ConfigCommand, ConfigSetArgs};
use anyhow::{Context, Result, anyhow, bail};
use toml_edit::{DocumentMut, Item, Table, value};

use crate::config;

pub fn run(command: ConfigCommand) -> Result<()> {
    match command {
        ConfigCommand::Set(args) => set_value(&args),
    }
}

fn set_value(args: &ConfigSetArgs) -> Result<()> {
    let path = match &args.file {
        Some(path) => path.clone(),
        None => config::config_path().ok_or_else(|| {
            anyhow!("failed to resolve default config path (no home directory found)")
        })?,
    };

    ensure_parent(&path)?;

    let mut doc = if path.exists() {
        let raw = fs::read_to_string(&path)
            .with_context(|| format!("failed to read {}", path.display()))?;
        if raw.trim().is_empty() {
            DocumentMut::new()
        } else {
            raw.parse::<DocumentMut>()
                .with_context(|| format!("failed to parse {}", path.display()))?
        }
    } else {
        DocumentMut::new()
    };

    apply_key(&mut doc, &args.key, &args.value)?;

    fs::write(&path, doc.to_string())
        .with_context(|| format!("failed to write {}", path.display()))?;
    println!("Updated {}", path.display());
    Ok(())
}

fn ensure_parent(path: &Path) -> Result<()> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)
            .with_context(|| format!("failed to create {}", parent.display()))?;
    }
    Ok(())
}

fn apply_key(doc: &mut DocumentMut, key: &str, value_str: &str) -> Result<()> {
    let segments = key
        .split('.')
        .filter(|segment| !segment.is_empty())
        .collect::<Vec<_>>();
    if segments.is_empty() {
        bail!("config key cannot be empty");
    }

    let mut current = doc.as_table_mut();
    for segment in &segments[..segments.len() - 1] {
        current = current
            .entry(segment)
            .or_insert(Item::Table(Table::new()))
            .as_table_mut()
            .ok_or_else(|| anyhow!("path `{segment}` is not a table in the config"))?;
    }

    current.insert(segments.last().unwrap(), value(value_str));
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::TempDir;

    #[test]
    fn creates_new_document() {
        let temp = TempDir::new().unwrap();
        let path = temp.path().join("config.toml");
        let args = ConfigSetArgs {
            key: "defaults.component.org".into(),
            value: "ai.greentic".into(),
            file: Some(path.clone()),
        };
        set_value(&args).unwrap();
        let written = fs::read_to_string(path).unwrap();
        assert!(written.contains("defaults"));
        assert!(written.contains("ai.greentic"));
    }

    #[test]
    fn updates_nested_tables() {
        let temp = TempDir::new().unwrap();
        let path = temp.path().join("config.toml");
        fs::write(
            &path,
            r#"
[defaults]
[defaults.component]
org = "ai.greentic"
"#,
        )
        .unwrap();

        let args = ConfigSetArgs {
            key: "defaults.component.template".into(),
            value: "rust-wasi".into(),
            file: Some(path.clone()),
        };
        set_value(&args).unwrap();
        let written = fs::read_to_string(path).unwrap();
        assert!(written.contains("template = \"rust-wasi\""));
    }
}