use crate::record::RecordView;
use crate::{validate, Channel, Error};
use scheme_edit::{list, quoted_sym, string_lit, sym, Document, Node};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChannelsShape {
Explicit,
WithDefaults,
}
#[derive(Debug)]
pub struct ChannelsFile {
doc: Document,
form_index: usize,
shape: ChannelsShape,
}
impl ChannelsFile {
pub fn parse(src: &str) -> Result<Self, Error> {
let doc = Document::parse(src)?;
let mut found = None;
for (i, form) in doc.forms().enumerate() {
if form.as_symbol() == Some("%default-channels") {
found = Some((i, ChannelsShape::WithDefaults));
break;
}
match form.head_symbol() {
Some("list") => {
found = Some((i, ChannelsShape::Explicit));
break;
}
Some(h @ ("cons" | "cons*")) => {
let tail = form.list_nodes().last();
if tail.and_then(Node::as_symbol) != Some("%default-channels") {
return Err(Error::Invalid {
field: "channels".into(),
reason: format!("`{h}` form must end in `%default-channels`"),
});
}
let elements = form.data_len().saturating_sub(2);
if h == "cons" && elements != 1 {
return Err(Error::Invalid {
field: "channels".into(),
reason: format!(
"`cons` form must have exactly one channel + tail, got {elements} channels"
),
});
}
found = Some((i, ChannelsShape::WithDefaults));
break;
}
_ => {}
}
}
let (form_index, shape) = found.ok_or(Error::NoChannelsForm)?;
let cf = ChannelsFile {
doc,
form_index,
shape,
};
cf.extract()?;
Ok(cf)
}
pub fn shape(&self) -> ChannelsShape {
self.shape
}
pub fn channels(&self) -> Vec<Channel> {
self.extract().unwrap_or_default()
}
pub fn add_channel(&mut self, ch: &Channel) -> Result<(), Error> {
validate::channel_fields(ch)?;
if ch.introduction_commit.is_none() || ch.introduction_fingerprint.is_none() {
return Err(Error::MissingField {
record: "channel".into(),
field: "introduction".into(),
});
}
if self.channels().iter().any(|c| c.name == ch.name) {
return Err(Error::Invalid {
field: "name".into(),
reason: format!("channel `{}` already present", ch.name),
});
}
let node = channel_to_node(ch);
let shape = self.shape;
let form = self.form_mut();
if form.as_symbol() == Some("%default-channels") {
let text = format!("(cons* {}\n %default-channels)", node.to_pretty(7));
*form = Document::parse(&text)
.ok()
.and_then(|doc| {
doc.items.into_iter().find_map(|i| match i {
scheme_edit::Item::Node(n) => Some(n),
_ => None,
})
})
.unwrap_or_else(|| list(vec![sym("cons*"), node, sym("%default-channels")]));
return Ok(());
}
match shape {
ChannelsShape::Explicit => form.push_child(node),
ChannelsShape::WithDefaults => {
match form.position_of(|n| n.as_symbol() == Some("%default-channels")) {
Some(tail) => form.insert_child(tail, node),
None => form.push_child(node),
}
if form.head_symbol() == Some("cons") {
form.replace_child(0, sym("cons*"));
}
}
}
Ok(())
}
pub fn remove_channel(&mut self, name: &str) -> Result<(), Error> {
{
let form = self.form_mut();
let Some(idx) = form.position_of(|n| element_channel_name(n).as_deref() == Some(name))
else {
return Err(Error::ChannelNotFound(name.to_string()));
};
form.remove_child(idx, true);
}
if self.shape == ChannelsShape::WithDefaults && self.channels().is_empty() {
*self.form_mut() = list(vec![sym("cons*"), sym("%default-channels")]);
}
Ok(())
}
fn form(&self) -> &Node {
self.doc
.forms()
.nth(self.form_index)
.expect("channels form present")
}
fn form_mut(&mut self) -> &mut Node {
self.doc
.forms_mut()
.nth(self.form_index)
.expect("channels form present")
}
fn extract(&self) -> Result<Vec<Channel>, Error> {
let form = self.form();
if form.as_symbol() == Some("%default-channels") {
return Ok(Vec::new());
}
let mut out = Vec::new();
for elt in form.list_nodes().skip(1) {
if elt.as_symbol() == Some("%default-channels") {
continue;
}
if let Some(ch) = find_inner_channel(elt) {
out.push(parse_channel(ch)?);
}
}
Ok(out)
}
}
impl std::fmt::Display for ChannelsFile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.doc.fmt(f)
}
}
fn channel_to_node(ch: &Channel) -> Node {
let mut fields = vec![
sym("channel"),
list(vec![sym("name"), quoted_sym(&ch.name)]),
list(vec![sym("url"), string_lit(&ch.url)]),
];
if let Some(b) = &ch.branch {
fields.push(list(vec![sym("branch"), string_lit(b)]));
}
if let Some(c) = &ch.commit {
fields.push(list(vec![sym("commit"), string_lit(c)]));
}
if let (Some(ic), Some(fpr)) = (&ch.introduction_commit, &ch.introduction_fingerprint) {
fields.push(list(vec![
sym("introduction"),
list(vec![
sym("make-channel-introduction"),
string_lit(ic),
list(vec![sym("openpgp-fingerprint"), string_lit(fpr)]),
]),
]));
}
list(fields)
}
fn element_channel_name(elt: &Node) -> Option<String> {
let v = RecordView::new(find_inner_channel(elt)?)?;
v.symbol_field("name")
.map(str::to_owned)
.or_else(|| v.string_field("name"))
}
fn find_inner_channel(elt: &Node) -> Option<&Node> {
if elt.head_symbol() == Some("channel") {
return Some(elt);
}
elt.head_symbol()?;
elt.list_nodes()
.skip(1)
.find(|n| n.head_symbol() == Some("channel"))
}
fn parse_channel(node: &Node) -> Result<Channel, Error> {
let missing = |field: &str| Error::MissingField {
record: "channel".into(),
field: field.into(),
};
let v = RecordView::new(node).ok_or_else(|| missing("name"))?;
let name = v
.symbol_field("name")
.map(str::to_owned)
.or_else(|| v.string_field("name"))
.ok_or_else(|| missing("name"))?;
let url = v.string_field("url").ok_or_else(|| missing("url"))?;
let (introduction_commit, introduction_fingerprint) = match v.field("introduction") {
Some(intro) => parse_introduction(intro),
None => (None, None),
};
Ok(Channel {
name,
url,
branch: v.string_field("branch"),
commit: v.string_field("commit"),
introduction_commit,
introduction_fingerprint,
})
}
fn parse_introduction(node: &Node) -> (Option<String>, Option<String>) {
if node.head_symbol() != Some("make-channel-introduction") {
return (None, None);
}
let commit = node.data_child(1).and_then(Node::as_string_lit);
let fpr = node
.data_child(2)
.filter(|n| n.head_symbol() == Some("openpgp-fingerprint"))
.and_then(|n| n.data_child(1))
.and_then(Node::as_string_lit);
(commit, fpr)
}