Skip to main content

guix_scheme/
os_view.rs

1//! Read and mutate view over an `(operating-system ...)` form.
2//!
3//! Built on top of [`RecordView`](crate::record::RecordView): fields are
4//! read by name, and disk-layout fields (`file-systems`, `mapped-devices`)
5//! are edited in place across the container shapes that appear in real
6//! configs, leaving untouched nodes byte-for-byte intact.
7
8use crate::record::{RecordView, RecordViewMut};
9use crate::Error;
10use scheme_edit::{list, sym, Document, Item, Node};
11
12fn is_os(node: &Node) -> bool {
13    node.head_symbol() == Some("operating-system")
14}
15
16fn invalid(field: &str, reason: impl Into<String>) -> Error {
17    Error::Invalid {
18        field: field.into(),
19        reason: reason.into(),
20    }
21}
22
23/// Parse SRC and return its first datum node.
24fn parse_single_datum(src: &str, field: &str) -> Result<Node, Error> {
25    let doc = Document::parse(src).map_err(|e| invalid(field, format!("invalid source: {e}")))?;
26    doc.items
27        .into_iter()
28        .find_map(|i| match i {
29            Item::Node(n) => Some(n),
30            _ => None,
31        })
32        .ok_or_else(|| invalid(field, "no datum in source"))
33}
34
35/// Splice ELT into a list-like FIELD value, preserving untouched nodes.
36///
37/// Shapes: `(list ...)` appends, `(cons* ... tail)` / `(cons elt tail)`
38/// insert before the tail (`cons` promotes to `cons*`), `(append (list
39/// ...) tail)` appends into the inner list, and a bare symbol becomes
40/// `(cons* elt <symbol>)`.
41fn splice_into_container(val: &mut Node, elt: Node, field: &str) -> Result<(), Error> {
42    if let Some(base) = val.as_symbol() {
43        let base = base.to_string();
44        *val = list(vec![sym("cons*"), elt, sym(&base)]);
45        return Ok(());
46    }
47    match val.head_symbol() {
48        Some("list") => {
49            val.push_child(elt);
50            Ok(())
51        }
52        Some("cons*") => {
53            let tail = val.data_len().saturating_sub(1);
54            val.insert_child(tail, elt);
55            Ok(())
56        }
57        Some("cons") => {
58            let tail = val.data_len().saturating_sub(1);
59            val.insert_child(tail, elt);
60            val.replace_child(0, sym("cons*"));
61            Ok(())
62        }
63        Some("append") => {
64            let idx = val.position_of(|n| n.head_symbol() == Some("list"));
65            match idx.and_then(|i| val.data_child_mut(i)) {
66                Some(inner) => {
67                    inner.push_child(elt);
68                    Ok(())
69                }
70                None => Err(invalid(field, "unsupported `append` shape for editing")),
71            }
72        }
73        _ => Err(invalid(field, "unsupported container shape for editing")),
74    }
75}
76
77/// Read-only view over an `(operating-system ...)` form. An optional
78/// leading `(inherit ...)` is accepted and ignored by field lookups.
79pub struct OperatingSystemView<'a> {
80    record: RecordView<'a>,
81}
82
83impl<'a> OperatingSystemView<'a> {
84    /// `Some` only when NODE is a list headed by `operating-system`.
85    pub fn new(node: &'a Node) -> Option<Self> {
86        if !is_os(node) {
87            return None;
88        }
89        Some(Self {
90            record: RecordView::new(node)?,
91        })
92    }
93
94    pub fn host_name(&self) -> Option<String> {
95        self.record.string_field("host-name")
96    }
97
98    pub fn timezone(&self) -> Option<String> {
99        self.record.string_field("timezone")
100    }
101
102    pub fn locale(&self) -> Option<String> {
103        self.record.string_field("locale")
104    }
105
106    /// Value node of a named field, e.g. `"file-systems"` or `"services"`.
107    pub fn field(&self, name: &str) -> Option<&'a Node> {
108        self.record.field(name)
109    }
110
111    pub fn has_field(&self, name: &str) -> bool {
112        self.record.field(name).is_some()
113    }
114}
115
116/// Mutable view: field reads plus field and disk-layout editing.
117pub struct OperatingSystemViewMut<'a> {
118    node: &'a mut Node,
119}
120
121impl<'a> OperatingSystemViewMut<'a> {
122    /// `Some` only when NODE is a list headed by `operating-system`.
123    pub fn new(node: &'a mut Node) -> Option<Self> {
124        if !is_os(node) {
125            return None;
126        }
127        Some(Self { node })
128    }
129
130    // new() guards the head symbol, so RecordViewMut always builds.
131    fn record_mut(&mut self) -> RecordViewMut<'_> {
132        RecordViewMut::new(&mut *self.node).expect("operating-system form")
133    }
134
135    /// Parse VALUE_SOURCE to a datum and set (or create) the named field.
136    pub fn set_field(&mut self, name: &str, value_source: &str) -> Result<(), Error> {
137        let value = parse_single_datum(value_source, name)?;
138        self.record_mut().set_field(name, value);
139        Ok(())
140    }
141
142    /// Splice a single `(file-system ...)` datum into `file-systems`,
143    /// creating `(file-systems (cons* <fs> %base-file-systems))` if the
144    /// field is absent.
145    pub fn add_file_system(&mut self, fs_source: &str) -> Result<(), Error> {
146        let fs = parse_single_datum(fs_source, "file-systems")?;
147        if fs.head_symbol() != Some("file-system") {
148            return Err(invalid(
149                "file-systems",
150                "expected a (file-system ...) datum",
151            ));
152        }
153        {
154            let mut record = self.record_mut();
155            if let Some(val) = record.field_mut("file-systems") {
156                return splice_into_container(val, fs, "file-systems");
157            }
158        }
159        self.record_mut().set_field(
160            "file-systems",
161            list(vec![sym("cons*"), fs, sym("%base-file-systems")]),
162        );
163        Ok(())
164    }
165
166    /// Splice a single `(mapped-device ...)` datum into `mapped-devices`,
167    /// creating `(mapped-devices (list <md>))` if the field is absent.
168    pub fn add_mapped_device(&mut self, md_source: &str) -> Result<(), Error> {
169        let md = parse_single_datum(md_source, "mapped-devices")?;
170        if md.head_symbol() != Some("mapped-device") {
171            return Err(invalid(
172                "mapped-devices",
173                "expected a (mapped-device ...) datum",
174            ));
175        }
176        {
177            let mut record = self.record_mut();
178            if let Some(val) = record.field_mut("mapped-devices") {
179                return splice_into_container(val, md, "mapped-devices");
180            }
181        }
182        self.record_mut()
183            .set_field("mapped-devices", list(vec![sym("list"), md]));
184        Ok(())
185    }
186}