guix-scheme 0.2.0

Read, edit, and generate Guix Scheme files (channels.scm, system config.scm) from Rust
Documentation
//! Thin typed layer over guix package/service module files.

use crate::record::{RecordView, RecordViewMut};
use crate::Error;
use scheme_edit::{string_lit, sym, Document, Node};

/// A parsed guix module file (e.g. `gnu/packages/*.scm`), lossless.
#[derive(Debug)]
pub struct ModuleFile {
    doc: Document,
}

impl ModuleFile {
    pub fn parse(src: &str) -> Result<Self, Error> {
        Ok(Self {
            doc: Document::parse(src)?,
        })
    }

    /// Names bound by top-level `(define-public <sym> ...)` forms whose
    /// value contains a `(package ...)` / `(package/inherit ...)` node,
    /// including under wrappers like `let`. Packages behind quoted or
    /// non-list contexts are not matched.
    pub fn package_names(&self) -> Vec<String> {
        self.doc
            .forms()
            .filter(|n| n.head_symbol() == Some("define-public"))
            .filter(|n| find_package(n).is_some())
            .filter_map(|n| n.data_child(1)?.as_symbol().map(str::to_owned))
            .collect()
    }

    pub fn package(&self, sym: &str) -> Option<PackageView<'_>> {
        let form = self.define_public_form(sym)?;
        let pkg = find_package(form)?;
        Some(PackageView {
            view: RecordView::new(pkg)?,
        })
    }

    pub fn package_mut(&mut self, sym: &str) -> Option<PackageViewMut<'_>> {
        let (form_idx, path) = {
            let (i, form) = self
                .doc
                .forms()
                .enumerate()
                .find(|(_, n)| is_define_public_of(n, sym))?;
            let mut path = Vec::new();
            if !find_package_path(form, &mut path) {
                return None;
            }
            (i, path)
        };
        let mut node = self.doc.forms_mut().nth(form_idx)?;
        for idx in path {
            node = node.data_child_mut(idx)?;
        }
        Some(PackageViewMut {
            view: RecordViewMut::new(node)?,
        })
    }

    /// Names of top-level `define-record-type*` forms, e.g.
    /// `<avahi-configuration>`.
    pub fn record_type_names(&self) -> Vec<String> {
        self.doc
            .forms()
            .filter(|n| n.head_symbol() == Some("define-record-type*"))
            .filter_map(|n| n.data_child(1)?.as_symbol().map(str::to_owned))
            .collect()
    }

    /// Read-only view over a top-level `define-record-type*` form,
    /// looked up by its type name (e.g. `<avahi-configuration>`).
    pub fn record_type(&self, name: &str) -> Option<RecordTypeView<'_>> {
        let node = self.doc.forms().find(|n| {
            n.head_symbol() == Some("define-record-type*")
                && n.data_child(1).and_then(Node::as_symbol) == Some(name)
        })?;
        Some(RecordTypeView { node })
    }

    fn define_public_form(&self, sym: &str) -> Option<&Node> {
        self.doc.forms().find(|n| is_define_public_of(n, sym))
    }
}

/// `(define-record-type* <name> ctor make-ctor pred? (field getter
/// [(default expr)]) ...)`. Clauses other than `(default ...)` (e.g.
/// `thunked`, `sanitize`) are ignored.
pub struct RecordTypeView<'a> {
    node: &'a Node,
}

impl<'a> RecordTypeView<'a> {
    pub fn name(&self) -> Option<&'a str> {
        self.node.data_child(1)?.as_symbol()
    }

    /// The syntactic constructor symbol.
    pub fn constructor(&self) -> Option<&'a str> {
        self.node.data_child(2)?.as_symbol()
    }

    pub fn predicate(&self) -> Option<&'a str> {
        self.node.data_child(4)?.as_symbol()
    }

    pub fn fields(&self) -> Vec<RecordFieldView<'a>> {
        self.node
            .list_nodes()
            .skip(5)
            .filter(|n| n.head_symbol().is_some())
            .map(|node| RecordFieldView { node })
            .collect()
    }
}

pub struct RecordFieldView<'a> {
    node: &'a Node,
}

impl<'a> RecordFieldView<'a> {
    pub fn name(&self) -> Option<&'a str> {
        self.node.data_child(0)?.as_symbol()
    }

    pub fn getter(&self) -> Option<&'a str> {
        self.node.data_child(1)?.as_symbol()
    }

    /// The default expression as source text, `None` without a
    /// `(default ...)` clause.
    pub fn default(&self) -> Option<String> {
        let clause = self
            .node
            .list_nodes()
            .skip(2)
            .find(|n| n.head_symbol() == Some("default"))?;
        Some(clause.data_child(1)?.to_source())
    }
}

impl std::fmt::Display for ModuleFile {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.doc.fmt(f)
    }
}

/// Read-only view over a `(package ...)` node.
pub struct PackageView<'a> {
    view: RecordView<'a>,
}

impl PackageView<'_> {
    pub fn name(&self) -> Option<String> {
        self.view.string_field("name")
    }

    /// `None` when the version is a computed expression, as in
    /// `(git-version ...)` / `(string-append ...)`.
    pub fn version(&self) -> Option<String> {
        self.view.string_field("version")
    }

    pub fn synopsis(&self) -> Option<String> {
        self.view.string_field("synopsis")
    }

    pub fn home_page(&self) -> Option<String> {
        self.view.string_field("home-page")
    }

    /// The base32 string under `source -> origin -> sha256 -> base32`.
    pub fn origin_sha256_base32(&self) -> Option<String> {
        let origin = self
            .view
            .field("source")
            .filter(|n| n.head_symbol() == Some("origin"))?;
        let sha = RecordView::new(origin)?.field("sha256")?;
        if sha.head_symbol() != Some("base32") {
            return None;
        }
        sha.data_child(1)?.as_string_lit()
    }

    /// Symbols in modern `(inputs (list a b c))` style. Non-symbol
    /// entries (calls like `(librsvg-for-system)`, quasiquoted pairs)
    /// are skipped; other styles yield an empty vec.
    pub fn inputs(&self) -> Vec<String> {
        self.input_list("inputs")
    }

    pub fn native_inputs(&self) -> Vec<String> {
        self.input_list("native-inputs")
    }

    pub fn propagated_inputs(&self) -> Vec<String> {
        self.input_list("propagated-inputs")
    }

    fn input_list(&self, field: &str) -> Vec<String> {
        let Some(v) = self.view.field(field) else {
            return Vec::new();
        };
        if v.head_symbol() != Some("list") {
            return Vec::new();
        }
        v.list_nodes()
            .skip(1)
            .filter_map(|n| n.as_symbol().map(str::to_owned))
            .collect()
    }
}

/// Mutable view over a `(package ...)` node.
pub struct PackageViewMut<'a> {
    view: RecordViewMut<'a>,
}

impl PackageViewMut<'_> {
    pub fn set_field(&mut self, name: &str, value: Node) {
        self.view.set_field(name, value);
    }

    pub fn field_mut(&mut self, name: &str) -> Option<&mut Node> {
        self.view.field_mut(name)
    }

    pub fn set_version(&mut self, version: &str) {
        self.view.set_field("version", string_lit(version));
    }

    /// Replaces the base32 string in place. `false` when the package
    /// does not have the `source -> origin -> sha256 -> base32` shape.
    pub fn set_origin_sha256_base32(&mut self, hash: &str) -> bool {
        let Some(origin) = self.view.field_mut("source") else {
            return false;
        };
        if origin.head_symbol() != Some("origin") {
            return false;
        }
        let Some(mut ov) = RecordViewMut::new(origin) else {
            return false;
        };
        let Some(sha) = ov.field_mut("sha256") else {
            return false;
        };
        if sha.head_symbol() != Some("base32") {
            return false;
        }
        sha.replace_child(1, string_lit(hash))
    }

    /// Swaps a symbol in list-style inputs (also native/propagated).
    pub fn replace_input(&mut self, old: &str, new: &str) -> bool {
        for field in ["inputs", "native-inputs", "propagated-inputs"] {
            if let Some(v) = self.view.field_mut(field) {
                if v.head_symbol() == Some("list") {
                    if let Some(idx) = v.position_of(|n| n.as_symbol() == Some(old)) {
                        return v.replace_child(idx, sym(new));
                    }
                }
            }
        }
        false
    }
}

fn is_define_public_of(form: &Node, name: &str) -> bool {
    form.head_symbol() == Some("define-public")
        && form.data_child(1).and_then(Node::as_symbol) == Some(name)
}

fn find_package(node: &Node) -> Option<&Node> {
    for child in node.list_nodes().skip(1) {
        if matches!(child.head_symbol(), Some("package" | "package/inherit")) {
            return Some(child);
        }
        if child.head_symbol().is_some() {
            if let Some(p) = find_package(child) {
                return Some(p);
            }
        }
    }
    None
}

/// Data-index path from NODE down to the first package node, for the
/// mutable descent in `package_mut`.
fn find_package_path(node: &Node, path: &mut Vec<usize>) -> bool {
    for (i, child) in node.list_nodes().enumerate().skip(1) {
        if matches!(child.head_symbol(), Some("package" | "package/inherit")) {
            path.push(i);
            return true;
        }
        if child.head_symbol().is_some() {
            path.push(i);
            if find_package_path(child, path) {
                return true;
            }
            path.pop();
        }
    }
    false
}