Skip to main content

guix_scheme/
services.rs

1//! Typed view over a services expression (the value of a config's
2//! `services` field). Blessed shapes are edited; everything else is
3//! refused without touching the tree.
4
5use crate::Error;
6use scheme_edit::{list, sym, Document, Item, Node};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9enum Shape {
10    /// `(append (list <elt>...) <tail-expr>)`
11    AppendList,
12    /// `(list <elt>...)`
13    List,
14    /// `(cons* <elt>... <tail>)`
15    ConsStar,
16    /// `(cons <elt> <tail>)`
17    Cons,
18    /// e.g. `%desktop-services`
19    BareSymbol,
20    /// Detected but read-only; edits refuse.
21    ModifyServices,
22    Unknown,
23}
24
25fn shape_of(node: &Node) -> Shape {
26    if node.as_symbol().is_some() {
27        return Shape::BareSymbol;
28    }
29    match node.head_symbol() {
30        Some("append") if node.list_nodes().any(|n| n.head_symbol() == Some("list")) => {
31            Shape::AppendList
32        }
33        Some("list") => Shape::List,
34        Some("cons*") => Shape::ConsStar,
35        Some("cons") => Shape::Cons,
36        Some("modify-services") => Shape::ModifyServices,
37        _ => Shape::Unknown,
38    }
39}
40
41/// The list node holding the `(service ...)` elements.
42fn container(node: &Node) -> Option<&Node> {
43    match shape_of(node) {
44        Shape::AppendList => node.list_nodes().find(|n| n.head_symbol() == Some("list")),
45        Shape::List | Shape::Cons | Shape::ConsStar => Some(node),
46        _ => None,
47    }
48}
49
50fn is_service_of(node: &Node, type_sym: &str) -> bool {
51    node.head_symbol() == Some("service")
52        && node.data_child(1).and_then(Node::as_symbol) == Some(type_sym)
53}
54
55fn collect_service_types(node: &Node) -> Vec<String> {
56    let Some(c) = container(node) else {
57        return Vec::new();
58    };
59    c.list_nodes()
60        .skip(1)
61        .filter(|n| n.head_symbol() == Some("service"))
62        .filter_map(|n| n.data_child(1)?.as_symbol().map(str::to_owned))
63        .collect()
64}
65
66fn unsupported() -> Error {
67    Error::Invalid {
68        field: "services".into(),
69        reason: "unsupported services shape for editing".into(),
70    }
71}
72
73fn parse_config_expr(src: &str) -> Result<Node, Error> {
74    let doc = Document::parse(src).map_err(|e| Error::Invalid {
75        field: "services".into(),
76        reason: format!("invalid config expression: {e}"),
77    })?;
78    doc.items
79        .into_iter()
80        .find_map(|i| match i {
81            Item::Node(n) => Some(n),
82            _ => None,
83        })
84        .ok_or_else(|| Error::Invalid {
85            field: "services".into(),
86            reason: "empty config expression".into(),
87        })
88}
89
90/// Pretty-print and reparse so multi-line text nests as stored source.
91fn reflow(node: Node) -> Node {
92    match Document::parse(&node.to_pretty(0)) {
93        Ok(doc) => doc
94            .items
95            .into_iter()
96            .find_map(|i| match i {
97                Item::Node(n) => Some(n),
98                _ => None,
99            })
100            .unwrap_or(node),
101        Err(_) => node,
102    }
103}
104
105/// Read-only view over a services expression node.
106pub struct ServicesView<'a> {
107    node: &'a Node,
108}
109
110impl<'a> ServicesView<'a> {
111    pub fn new(node: &'a Node) -> Self {
112        Self { node }
113    }
114
115    /// Type symbols of `(service <type> ...)` elements; other elements
116    /// are skipped. Empty for bare symbols and `modify-services`.
117    pub fn service_types(&self) -> Vec<String> {
118        collect_service_types(self.node)
119    }
120
121    pub fn has_service(&self, type_sym: &str) -> bool {
122        self.service_types().iter().any(|t| t == type_sym)
123    }
124}
125
126/// Mutable view: same reads plus add/remove/replace.
127pub struct ServicesViewMut<'a> {
128    node: &'a mut Node,
129}
130
131impl<'a> ServicesViewMut<'a> {
132    pub fn new(node: &'a mut Node) -> Self {
133        Self { node }
134    }
135
136    pub fn service_types(&self) -> Vec<String> {
137        collect_service_types(self.node)
138    }
139
140    pub fn has_service(&self, type_sym: &str) -> bool {
141        self.service_types().iter().any(|t| t == type_sym)
142    }
143
144    /// Builds `(service <sym>)` or `(service <sym> <config>)` and adds
145    /// it to the blessed shape. A bare base symbol is rewritten to
146    /// `(append (list (service <sym>)) <base>)`; `cons` promotes to
147    /// `cons*`.
148    pub fn add_service(
149        &mut self,
150        type_sym: &str,
151        config_source: Option<&str>,
152    ) -> Result<(), Error> {
153        let shape = shape_of(self.node);
154        if matches!(shape, Shape::ModifyServices | Shape::Unknown) {
155            return Err(unsupported());
156        }
157        if self.has_service(type_sym) {
158            return Err(Error::Invalid {
159                field: "services".into(),
160                reason: format!("service `{type_sym}` already present"),
161            });
162        }
163        let mut parts = vec![sym("service"), sym(type_sym)];
164        if let Some(cfg) = config_source {
165            parts.push(parse_config_expr(cfg)?);
166        }
167        let svc = list(parts);
168        match shape {
169            Shape::AppendList => self
170                .container_mut()
171                .ok_or_else(unsupported)?
172                .push_child(svc),
173            Shape::List => self.node.push_child(svc),
174            Shape::Cons | Shape::ConsStar => {
175                let tail = self.node.data_len().saturating_sub(1);
176                self.node.insert_child(tail, svc);
177                if shape == Shape::Cons {
178                    self.node.replace_child(0, sym("cons*"));
179                }
180            }
181            Shape::BareSymbol => {
182                let base = self.node.as_symbol().unwrap_or_default().to_string();
183                *self.node = reflow(list(vec![
184                    sym("append"),
185                    list(vec![sym("list"), svc]),
186                    sym(&base),
187                ]));
188            }
189            Shape::ModifyServices | Shape::Unknown => unreachable!("checked above"),
190        }
191        Ok(())
192    }
193
194    /// Removes the element with attached leading trivia. A `cons`/`cons*`
195    /// left with only its tail collapses to `(cons* <tail>)` (single-arg
196    /// `cons*` is identity; a one-arg `cons` would be invalid Scheme).
197    pub fn remove_service(&mut self, type_sym: &str) -> Result<(), Error> {
198        let shape = shape_of(self.node);
199        {
200            let Some(c) = self.container_mut() else {
201                return Err(unsupported());
202            };
203            let Some(idx) = c.position_of(|n| is_service_of(n, type_sym)) else {
204                return Err(Error::Invalid {
205                    field: "services".into(),
206                    reason: format!("service `{type_sym}` not found"),
207                });
208            };
209            c.remove_child(idx, true);
210        }
211        if matches!(shape, Shape::Cons | Shape::ConsStar) && self.node.data_len() == 2 {
212            if let Some(tail) = self.node.data_child(1).cloned() {
213                *self.node = list(vec![sym("cons*"), tail]);
214            }
215        }
216        Ok(())
217    }
218
219    /// Swaps just the type symbol; a config argument is preserved.
220    pub fn replace_service_type(&mut self, old: &str, new: &str) -> bool {
221        let Some(c) = self.container_mut() else {
222            return false;
223        };
224        let Some(idx) = c.position_of(|n| is_service_of(n, old)) else {
225            return false;
226        };
227        match c.data_child_mut(idx) {
228            Some(el) => el.replace_child(1, sym(new)),
229            None => false,
230        }
231    }
232
233    fn container_mut(&mut self) -> Option<&mut Node> {
234        match shape_of(self.node) {
235            Shape::AppendList => {
236                // Edits require the (list ...) as append's FIRST argument;
237                // reads stay lenient about its position.
238                if self.node.data_child(1)?.head_symbol() != Some("list") {
239                    return None;
240                }
241                self.node.data_child_mut(1)
242            }
243            Shape::List | Shape::Cons | Shape::ConsStar => Some(&mut *self.node),
244            _ => None,
245        }
246    }
247}