use crate::Error;
use scheme_edit::{list, sym, Document, Item, Node};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Shape {
AppendList,
List,
ConsStar,
Cons,
BareSymbol,
ModifyServices,
Unknown,
}
fn shape_of(node: &Node) -> Shape {
if node.as_symbol().is_some() {
return Shape::BareSymbol;
}
match node.head_symbol() {
Some("append") if node.list_nodes().any(|n| n.head_symbol() == Some("list")) => {
Shape::AppendList
}
Some("list") => Shape::List,
Some("cons*") => Shape::ConsStar,
Some("cons") => Shape::Cons,
Some("modify-services") => Shape::ModifyServices,
_ => Shape::Unknown,
}
}
fn container(node: &Node) -> Option<&Node> {
match shape_of(node) {
Shape::AppendList => node.list_nodes().find(|n| n.head_symbol() == Some("list")),
Shape::List | Shape::Cons | Shape::ConsStar => Some(node),
_ => None,
}
}
fn is_service_of(node: &Node, type_sym: &str) -> bool {
node.head_symbol() == Some("service")
&& node.data_child(1).and_then(Node::as_symbol) == Some(type_sym)
}
fn collect_service_types(node: &Node) -> Vec<String> {
let Some(c) = container(node) else {
return Vec::new();
};
c.list_nodes()
.skip(1)
.filter(|n| n.head_symbol() == Some("service"))
.filter_map(|n| n.data_child(1)?.as_symbol().map(str::to_owned))
.collect()
}
fn unsupported() -> Error {
Error::Invalid {
field: "services".into(),
reason: "unsupported services shape for editing".into(),
}
}
fn parse_config_expr(src: &str) -> Result<Node, Error> {
let doc = Document::parse(src).map_err(|e| Error::Invalid {
field: "services".into(),
reason: format!("invalid config expression: {e}"),
})?;
doc.items
.into_iter()
.find_map(|i| match i {
Item::Node(n) => Some(n),
_ => None,
})
.ok_or_else(|| Error::Invalid {
field: "services".into(),
reason: "empty config expression".into(),
})
}
fn reflow(node: Node) -> Node {
match Document::parse(&node.to_pretty(0)) {
Ok(doc) => doc
.items
.into_iter()
.find_map(|i| match i {
Item::Node(n) => Some(n),
_ => None,
})
.unwrap_or(node),
Err(_) => node,
}
}
pub struct ServicesView<'a> {
node: &'a Node,
}
impl<'a> ServicesView<'a> {
pub fn new(node: &'a Node) -> Self {
Self { node }
}
pub fn service_types(&self) -> Vec<String> {
collect_service_types(self.node)
}
pub fn has_service(&self, type_sym: &str) -> bool {
self.service_types().iter().any(|t| t == type_sym)
}
}
pub struct ServicesViewMut<'a> {
node: &'a mut Node,
}
impl<'a> ServicesViewMut<'a> {
pub fn new(node: &'a mut Node) -> Self {
Self { node }
}
pub fn service_types(&self) -> Vec<String> {
collect_service_types(self.node)
}
pub fn has_service(&self, type_sym: &str) -> bool {
self.service_types().iter().any(|t| t == type_sym)
}
pub fn add_service(
&mut self,
type_sym: &str,
config_source: Option<&str>,
) -> Result<(), Error> {
let shape = shape_of(self.node);
if matches!(shape, Shape::ModifyServices | Shape::Unknown) {
return Err(unsupported());
}
if self.has_service(type_sym) {
return Err(Error::Invalid {
field: "services".into(),
reason: format!("service `{type_sym}` already present"),
});
}
let mut parts = vec![sym("service"), sym(type_sym)];
if let Some(cfg) = config_source {
parts.push(parse_config_expr(cfg)?);
}
let svc = list(parts);
match shape {
Shape::AppendList => self
.container_mut()
.ok_or_else(unsupported)?
.push_child(svc),
Shape::List => self.node.push_child(svc),
Shape::Cons | Shape::ConsStar => {
let tail = self.node.data_len().saturating_sub(1);
self.node.insert_child(tail, svc);
if shape == Shape::Cons {
self.node.replace_child(0, sym("cons*"));
}
}
Shape::BareSymbol => {
let base = self.node.as_symbol().unwrap_or_default().to_string();
*self.node = reflow(list(vec![
sym("append"),
list(vec![sym("list"), svc]),
sym(&base),
]));
}
Shape::ModifyServices | Shape::Unknown => unreachable!("checked above"),
}
Ok(())
}
pub fn remove_service(&mut self, type_sym: &str) -> Result<(), Error> {
let shape = shape_of(self.node);
{
let Some(c) = self.container_mut() else {
return Err(unsupported());
};
let Some(idx) = c.position_of(|n| is_service_of(n, type_sym)) else {
return Err(Error::Invalid {
field: "services".into(),
reason: format!("service `{type_sym}` not found"),
});
};
c.remove_child(idx, true);
}
if matches!(shape, Shape::Cons | Shape::ConsStar) && self.node.data_len() == 2 {
if let Some(tail) = self.node.data_child(1).cloned() {
*self.node = list(vec![sym("cons*"), tail]);
}
}
Ok(())
}
pub fn replace_service_type(&mut self, old: &str, new: &str) -> bool {
let Some(c) = self.container_mut() else {
return false;
};
let Some(idx) = c.position_of(|n| is_service_of(n, old)) else {
return false;
};
match c.data_child_mut(idx) {
Some(el) => el.replace_child(1, sym(new)),
None => false,
}
}
fn container_mut(&mut self) -> Option<&mut Node> {
match shape_of(self.node) {
Shape::AppendList => {
if self.node.data_child(1)?.head_symbol() != Some("list") {
return None;
}
self.node.data_child_mut(1)
}
Shape::List | Shape::Cons | Shape::ConsStar => Some(&mut *self.node),
_ => None,
}
}
}