guix_scheme/
channels_file.rs1use crate::record::RecordView;
4use crate::{validate, Channel, Error};
5use scheme_edit::{list, quoted_sym, string_lit, sym, Document, Node};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ChannelsShape {
11 Explicit,
13 WithDefaults,
16}
17
18#[derive(Debug)]
19pub struct ChannelsFile {
20 doc: Document,
21 form_index: usize,
22 shape: ChannelsShape,
23}
24
25impl ChannelsFile {
26 pub fn parse(src: &str) -> Result<Self, Error> {
30 let doc = Document::parse(src)?;
31 let mut found = None;
32 for (i, form) in doc.forms().enumerate() {
33 if form.as_symbol() == Some("%default-channels") {
34 found = Some((i, ChannelsShape::WithDefaults));
35 break;
36 }
37 match form.head_symbol() {
38 Some("list") => {
39 found = Some((i, ChannelsShape::Explicit));
40 break;
41 }
42 Some(h @ ("cons" | "cons*")) => {
43 let tail = form.list_nodes().last();
44 if tail.and_then(Node::as_symbol) != Some("%default-channels") {
45 return Err(Error::Invalid {
46 field: "channels".into(),
47 reason: format!("`{h}` form must end in `%default-channels`"),
48 });
49 }
50 let elements = form.data_len().saturating_sub(2);
52 if h == "cons" && elements != 1 {
53 return Err(Error::Invalid {
54 field: "channels".into(),
55 reason: format!(
56 "`cons` form must have exactly one channel + tail, got {elements} channels"
57 ),
58 });
59 }
60 found = Some((i, ChannelsShape::WithDefaults));
61 break;
62 }
63 _ => {}
64 }
65 }
66 let (form_index, shape) = found.ok_or(Error::NoChannelsForm)?;
67 let cf = ChannelsFile {
68 doc,
69 form_index,
70 shape,
71 };
72 cf.extract()?;
73 Ok(cf)
74 }
75
76 pub fn shape(&self) -> ChannelsShape {
77 self.shape
78 }
79
80 pub fn channels(&self) -> Vec<Channel> {
82 self.extract().unwrap_or_default()
84 }
85
86 pub fn add_channel(&mut self, ch: &Channel) -> Result<(), Error> {
89 validate::channel_fields(ch)?;
90 if ch.introduction_commit.is_none() || ch.introduction_fingerprint.is_none() {
91 return Err(Error::MissingField {
92 record: "channel".into(),
93 field: "introduction".into(),
94 });
95 }
96 if self.channels().iter().any(|c| c.name == ch.name) {
97 return Err(Error::Invalid {
98 field: "name".into(),
99 reason: format!("channel `{}` already present", ch.name),
100 });
101 }
102 let node = channel_to_node(ch);
103 let shape = self.shape;
104 let form = self.form_mut();
105 if form.as_symbol() == Some("%default-channels") {
106 let text = format!("(cons* {}\n %default-channels)", node.to_pretty(7));
108 *form = Document::parse(&text)
109 .ok()
110 .and_then(|doc| {
111 doc.items.into_iter().find_map(|i| match i {
112 scheme_edit::Item::Node(n) => Some(n),
113 _ => None,
114 })
115 })
116 .unwrap_or_else(|| list(vec![sym("cons*"), node, sym("%default-channels")]));
117 return Ok(());
118 }
119 match shape {
120 ChannelsShape::Explicit => form.push_child(node),
121 ChannelsShape::WithDefaults => {
122 match form.position_of(|n| n.as_symbol() == Some("%default-channels")) {
123 Some(tail) => form.insert_child(tail, node),
124 None => form.push_child(node),
125 }
126 if form.head_symbol() == Some("cons") {
127 form.replace_child(0, sym("cons*"));
128 }
129 }
130 }
131 Ok(())
132 }
133
134 pub fn remove_channel(&mut self, name: &str) -> Result<(), Error> {
138 {
139 let form = self.form_mut();
140 let Some(idx) = form.position_of(|n| element_channel_name(n).as_deref() == Some(name))
141 else {
142 return Err(Error::ChannelNotFound(name.to_string()));
143 };
144 form.remove_child(idx, true);
145 }
146 if self.shape == ChannelsShape::WithDefaults && self.channels().is_empty() {
147 *self.form_mut() = list(vec![sym("cons*"), sym("%default-channels")]);
148 }
149 Ok(())
150 }
151
152 fn form(&self) -> &Node {
153 self.doc
155 .forms()
156 .nth(self.form_index)
157 .expect("channels form present")
158 }
159
160 fn form_mut(&mut self) -> &mut Node {
161 self.doc
163 .forms_mut()
164 .nth(self.form_index)
165 .expect("channels form present")
166 }
167
168 fn extract(&self) -> Result<Vec<Channel>, Error> {
169 let form = self.form();
170 if form.as_symbol() == Some("%default-channels") {
171 return Ok(Vec::new());
172 }
173 let mut out = Vec::new();
174 for elt in form.list_nodes().skip(1) {
175 if elt.as_symbol() == Some("%default-channels") {
176 continue;
177 }
178 if let Some(ch) = find_inner_channel(elt) {
179 out.push(parse_channel(ch)?);
180 }
181 }
182 Ok(out)
183 }
184}
185
186impl std::fmt::Display for ChannelsFile {
187 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188 self.doc.fmt(f)
189 }
190}
191
192fn channel_to_node(ch: &Channel) -> Node {
194 let mut fields = vec![
195 sym("channel"),
196 list(vec![sym("name"), quoted_sym(&ch.name)]),
197 list(vec![sym("url"), string_lit(&ch.url)]),
198 ];
199 if let Some(b) = &ch.branch {
200 fields.push(list(vec![sym("branch"), string_lit(b)]));
201 }
202 if let Some(c) = &ch.commit {
203 fields.push(list(vec![sym("commit"), string_lit(c)]));
204 }
205 if let (Some(ic), Some(fpr)) = (&ch.introduction_commit, &ch.introduction_fingerprint) {
206 fields.push(list(vec![
207 sym("introduction"),
208 list(vec![
209 sym("make-channel-introduction"),
210 string_lit(ic),
211 list(vec![sym("openpgp-fingerprint"), string_lit(fpr)]),
212 ]),
213 ]));
214 }
215 list(fields)
216}
217
218fn element_channel_name(elt: &Node) -> Option<String> {
219 let v = RecordView::new(find_inner_channel(elt)?)?;
220 v.symbol_field("name")
221 .map(str::to_owned)
222 .or_else(|| v.string_field("name"))
223}
224
225fn find_inner_channel(elt: &Node) -> Option<&Node> {
229 if elt.head_symbol() == Some("channel") {
230 return Some(elt);
231 }
232 elt.head_symbol()?;
233 elt.list_nodes()
234 .skip(1)
235 .find(|n| n.head_symbol() == Some("channel"))
236}
237
238fn parse_channel(node: &Node) -> Result<Channel, Error> {
239 let missing = |field: &str| Error::MissingField {
240 record: "channel".into(),
241 field: field.into(),
242 };
243 let v = RecordView::new(node).ok_or_else(|| missing("name"))?;
244 let name = v
245 .symbol_field("name")
246 .map(str::to_owned)
247 .or_else(|| v.string_field("name"))
248 .ok_or_else(|| missing("name"))?;
249 let url = v.string_field("url").ok_or_else(|| missing("url"))?;
250 let (introduction_commit, introduction_fingerprint) = match v.field("introduction") {
251 Some(intro) => parse_introduction(intro),
252 None => (None, None),
253 };
254 Ok(Channel {
255 name,
256 url,
257 branch: v.string_field("branch"),
258 commit: v.string_field("commit"),
259 introduction_commit,
260 introduction_fingerprint,
261 })
262}
263
264fn parse_introduction(node: &Node) -> (Option<String>, Option<String>) {
266 if node.head_symbol() != Some("make-channel-introduction") {
267 return (None, None);
268 }
269 let commit = node.data_child(1).and_then(Node::as_string_lit);
270 let fpr = node
271 .data_child(2)
272 .filter(|n| n.head_symbol() == Some("openpgp-fingerprint"))
273 .and_then(|n| n.data_child(1))
274 .and_then(Node::as_string_lit);
275 (commit, fpr)
276}