guix-scheme 0.2.0

Read, edit, and generate Guix Scheme files (channels.scm, system config.scm) from Rust
Documentation
use guix_scheme::{validate_syntax, OperatingSystemView, OperatingSystemViewMut};
use scheme_edit::Document;

const CONFIG: &str = "\
(operating-system
  (host-name \"x\")
  (timezone \"UTC\")
  (locale \"en_US.utf8\")
  (file-systems
   (cons* (file-system
            (mount-point \"/boot\")
            (device \"/dev/sda1\")
            (type \"vfat\"))
          %base-file-systems))
  (services (list (service foo-service-type))))
";

fn os_form(doc: &Document) -> &scheme_edit::Node {
    doc.forms().next().unwrap()
}

fn os_form_mut(doc: &mut Document) -> &mut scheme_edit::Node {
    doc.forms_mut().next().unwrap()
}

#[test]
fn reads_string_fields_and_presence() {
    let doc = Document::parse(CONFIG).unwrap();
    let view = OperatingSystemView::new(os_form(&doc)).unwrap();
    assert_eq!(view.host_name().as_deref(), Some("x"));
    assert_eq!(view.timezone().as_deref(), Some("UTC"));
    assert_eq!(view.locale().as_deref(), Some("en_US.utf8"));
    assert!(view.field("services").is_some());
    assert!(view.has_field("file-systems"));
    assert!(!view.has_field("mapped-devices"));
}

#[test]
fn new_rejects_non_os_form() {
    let doc = Document::parse("(list 1 2)").unwrap();
    assert!(OperatingSystemView::new(os_form(&doc)).is_none());
}

#[test]
fn add_file_system_into_cons_star_preserves_tail_and_existing() {
    let mut doc = Document::parse(CONFIG).unwrap();
    {
        let mut view = OperatingSystemViewMut::new(os_form_mut(&mut doc)).unwrap();
        view.add_file_system(
            "(file-system (mount-point \"/\") (device \"/dev/sda2\") (type \"ext4\"))",
        )
        .unwrap();
    }
    let out = doc.to_string();
    assert!(out.contains("(mount-point \"/\")"), "new fs missing: {out}");
    assert!(
        out.contains("(mount-point \"/boot\")"),
        "old fs lost: {out}"
    );
    assert!(out.contains("%base-file-systems"), "tail lost: {out}");
    assert!(Document::parse(&out).is_ok(), "does not reparse: {out}");
}

#[test]
fn add_file_system_when_absent_creates_cons_star() {
    let mut doc = Document::parse("(operating-system (host-name \"h\"))").unwrap();
    {
        let mut view = OperatingSystemViewMut::new(os_form_mut(&mut doc)).unwrap();
        view.add_file_system("(file-system (mount-point \"/\") (device \"root\") (type \"ext4\"))")
            .unwrap();
    }
    let out = doc.to_string();
    assert!(out.contains("(file-systems"), "field not created: {out}");
    assert!(out.contains("cons*"), "not a cons* shape: {out}");
    assert!(out.contains("%base-file-systems"), "no base tail: {out}");
    assert!(Document::parse(&out).is_ok());
}

#[test]
fn add_mapped_device_when_absent_creates_list() {
    let mut doc = Document::parse("(operating-system (host-name \"h\"))").unwrap();
    {
        let mut view = OperatingSystemViewMut::new(os_form_mut(&mut doc)).unwrap();
        view.add_mapped_device(
            "(mapped-device (source \"my-root\") (target \"root\") (type luks-device-mapping))",
        )
        .unwrap();
    }
    let out = doc.to_string();
    assert!(out.contains("(mapped-devices"), "field not created: {out}");
    assert!(out.contains("(list"), "not a list shape: {out}");
    assert!(out.contains("(source \"my-root\")"));
    assert!(Document::parse(&out).is_ok());
}

#[test]
fn add_file_system_rejects_wrong_datum() {
    let mut doc = Document::parse(CONFIG).unwrap();
    let mut view = OperatingSystemViewMut::new(os_form_mut(&mut doc)).unwrap();
    assert!(view
        .add_file_system("(mapped-device (source \"x\"))")
        .is_err());
    assert!(view.add_file_system("(unclosed").is_err());
}

#[test]
fn set_field_creates_and_replaces() {
    let mut doc = Document::parse("(operating-system (host-name \"h\"))").unwrap();
    {
        let mut view = OperatingSystemViewMut::new(os_form_mut(&mut doc)).unwrap();
        view.set_field("timezone", "\"Europe/Berlin\"").unwrap();
        view.set_field("host-name", "\"renamed\"").unwrap();
    }
    let view_doc = doc.to_string();
    let reparsed = Document::parse(&view_doc).unwrap();
    let view = OperatingSystemView::new(os_form(&reparsed)).unwrap();
    assert_eq!(view.timezone().as_deref(), Some("Europe/Berlin"));
    assert_eq!(view.host_name().as_deref(), Some("renamed"));
}

#[test]
fn enterprise_merge_injects_disk_layout() {
    let mut doc =
        Document::parse("(operating-system\n  (inherit %os-base)\n  (host-name \"ent\"))").unwrap();
    {
        let mut view = OperatingSystemViewMut::new(os_form_mut(&mut doc)).unwrap();
        view.add_file_system(
            "(file-system (mount-point \"/\") (device \"/dev/sda2\") (type \"ext4\"))",
        )
        .unwrap();
        view.add_mapped_device(
            "(mapped-device (source \"crypt\") (target \"root\") (type luks-device-mapping))",
        )
        .unwrap();
    }
    let out = doc.to_string();
    assert!(out.contains("(inherit %os-base)"), "inherit lost: {out}");
    assert!(out.contains("(file-systems"), "no file-systems: {out}");
    assert!(out.contains("(mapped-devices"), "no mapped-devices: {out}");
    assert!(Document::parse(&out).is_ok(), "does not reparse: {out}");

    let reparsed = Document::parse(&out).unwrap();
    let view = OperatingSystemView::new(os_form(&reparsed)).unwrap();
    assert!(view.has_field("file-systems"));
    assert!(view.has_field("mapped-devices"));
    assert_eq!(view.host_name().as_deref(), Some("ent"));
}

#[test]
fn read_only_view_is_byte_identical() {
    let doc = Document::parse(CONFIG).unwrap();
    let _view = OperatingSystemView::new(os_form(&doc)).unwrap();
    assert_eq!(doc.to_string(), CONFIG);
}

#[test]
fn validate_syntax_accepts_and_rejects() {
    assert!(validate_syntax("(a b)").is_ok());
    assert!(validate_syntax("(unclosed").is_err());
}