1use crate::Error;
6use scheme_edit::{list, sym, Document, Item, Node};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9enum Shape {
10 AppendList,
12 List,
14 ConsStar,
16 Cons,
18 BareSymbol,
20 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
41fn 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
90fn 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
105pub 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 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
126pub 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 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 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 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 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}