guix-scheme 0.2.0

Read, edit, and generate Guix Scheme files (channels.scm, system config.scm) from Rust
Documentation
//! Optional round-trip validation of generated Scheme through guile.

use std::process::Command;

fn which_guile() -> Option<std::path::PathBuf> {
    let path = std::env::var_os("PATH")?;
    std::env::split_paths(&path)
        .map(|d| d.join("guile"))
        .find(|p| p.is_file())
}

/// Feeds SOURCE through guile's reader; a non-zero exit maps to
/// `Error::Invalid`. Absent guile is an error too — callers gate on
/// availability themselves.
pub fn validate(source: &str) -> Result<(), crate::Error> {
    let guile = which_guile().ok_or_else(|| crate::Error::Invalid {
        field: "guile".into(),
        reason: "guile not found in PATH".into(),
    })?;
    let script = r#"(let loop () (let ((x (read))) (unless (eof-object? x) (loop))))"#;
    let mut child = Command::new(guile)
        .args(["-c", script])
        .stdin(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .spawn()
        .map_err(|e| crate::Error::Invalid {
            field: "guile".into(),
            reason: e.to_string(),
        })?;
    use std::io::Write;
    if let Some(mut stdin) = child.stdin.take() {
        stdin.write_all(source.as_bytes()).ok();
    }
    let out = child
        .wait_with_output()
        .map_err(|e| crate::Error::Invalid {
            field: "guile".into(),
            reason: e.to_string(),
        })?;
    if out.status.success() {
        Ok(())
    } else {
        Err(crate::Error::Invalid {
            field: "source".into(),
            reason: String::from_utf8_lossy(&out.stderr).trim().to_string(),
        })
    }
}

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

    #[test]
    fn accepts_valid_and_rejects_invalid() {
        if which_guile().is_none() {
            eprintln!("skipping: no guile");
            return;
        }
        assert!(validate("(list (channel (name 'a)))").is_ok());
        assert!(validate("(list (channel").is_err());
    }
}