Skip to main content

one_brackets/
node.rs

1//! Object model of the bracket format.
2
3use std::borrow::Cow;
4use std::fmt;
5use std::hash::{Hash, Hasher};
6use std::ops::{Index, IndexMut};
7use std::str::FromStr;
8
9use crate::{AccessError, AccessErrorKind, Position};
10
11const BASE64: &str = "#base64:";
12
13/// A node of a bracket file: a [`Value`] plus the position it was parsed from.
14///
15/// Quoted strings and atoms are kept distinct, so writing reproduces
16/// the original platform text byte for byte. Equality and hashing ignore the position.
17#[derive(Clone)]
18pub struct Node {
19    value: Value,
20    position: Option<Position>,
21}
22
23/// Content of a [`Node`].
24#[derive(Debug, Clone, PartialEq, Eq, Hash)]
25pub enum Value {
26    /// A list `{a,b,c}`.
27    List(Vec<Node>),
28    /// A quoted string; doubled quotes are already unescaped.
29    Str(String),
30    /// An unquoted value: number, GUID, literal; may be empty (`{2,x,}`).
31    /// Binary data `{#base64:…}` is an atom too, line breaks included.
32    Atom(String),
33}
34
35impl Node {
36    pub(crate) fn parsed(value: Value, position: Position) -> Self {
37        Self {
38            value,
39            position: Some(position),
40        }
41    }
42
43    /// A list of nodes.
44    pub fn list(items: impl IntoIterator<Item = Node>) -> Self {
45        Value::List(items.into_iter().collect()).into()
46    }
47
48    /// An unquoted atom (GUID, literal, number).
49    pub fn atom(value: impl Into<String>) -> Self {
50        Value::Atom(value.into()).into()
51    }
52
53    /// A quoted string.
54    ///
55    /// There is no `From<&str>` for building: `Node::try_from(&str)` parses text.
56    pub fn string(value: impl Into<String>) -> Self {
57        Value::Str(value.into()).into()
58    }
59
60    /// A counted list `{count,item1,item2,...}`, the common 1C shape.
61    pub fn counted(items: impl IntoIterator<Item = Node>) -> Self {
62        let items: Vec<Node> = items.into_iter().collect();
63        Node::list(std::iter::once(Node::from(items.len())).chain(items))
64    }
65
66    pub fn value(&self) -> &Value {
67        &self.value
68    }
69
70    pub fn into_value(self) -> Value {
71        self.value
72    }
73
74    /// Mutable access to the content; the position stays where the node was parsed.
75    pub fn value_mut(&mut self) -> &mut Value {
76        &mut self.value
77    }
78
79    /// Where the node starts in the input; `None` for nodes built in code.
80    ///
81    /// Edits do not change it: it always refers to the original text.
82    pub fn position(&self) -> Option<Position> {
83        self.position
84    }
85
86    pub fn is_list(&self) -> bool {
87        matches!(self.value, Value::List(_))
88    }
89
90    pub fn is_str(&self) -> bool {
91        matches!(self.value, Value::Str(_))
92    }
93
94    pub fn is_atom(&self) -> bool {
95        matches!(self.value, Value::Atom(_))
96    }
97
98    /// An empty atom: no value between commas (`{1,,2}`).
99    pub fn is_empty_atom(&self) -> bool {
100        matches!(&self.value, Value::Atom(s) if s.is_empty())
101    }
102
103    pub fn as_list(&self) -> Option<&[Node]> {
104        match &self.value {
105            Value::List(items) => Some(items),
106            _ => None,
107        }
108    }
109
110    /// Value of a string or an atom.
111    pub fn as_str(&self) -> Option<&str> {
112        match &self.value {
113            Value::Str(s) | Value::Atom(s) => Some(s),
114            Value::List(_) => None,
115        }
116    }
117
118    /// Quoted string only.
119    pub fn as_quoted(&self) -> Option<&str> {
120        match &self.value {
121            Value::Str(s) => Some(s),
122            _ => None,
123        }
124    }
125
126    /// Atom only.
127    pub fn as_atom(&self) -> Option<&str> {
128        match &self.value {
129            Value::Atom(s) => Some(s),
130            _ => None,
131        }
132    }
133
134    /// Atom parsed into any [`FromStr`] type: `node.as_parsed::<u32>()`.
135    pub fn as_parsed<T: FromStr>(&self) -> Option<T> {
136        self.as_atom()?.parse().ok()
137    }
138
139    pub fn as_i64(&self) -> Option<i64> {
140        self.as_parsed()
141    }
142
143    pub fn as_u64(&self) -> Option<u64> {
144        self.as_parsed()
145    }
146
147    pub fn as_f64(&self) -> Option<f64> {
148        self.as_parsed()
149    }
150
151    /// Atom `1` is `true`, `0` is `false`.
152    pub fn as_bool(&self) -> Option<bool> {
153        match self.as_atom()? {
154            "1" => Some(true),
155            "0" => Some(false),
156            _ => None,
157        }
158    }
159
160    /// An atom shaped like a GUID `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`.
161    pub fn as_guid(&self) -> Option<&str> {
162        self.as_atom().filter(|s| is_guid(s))
163    }
164
165    /// Payload of a `#base64:…` atom without the prefix and line breaks (not decoded).
166    pub fn as_base64(&self) -> Option<String> {
167        let data = self.as_atom()?.strip_prefix(BASE64)?;
168        Some(data.chars().filter(|c| !c.is_ascii_whitespace()).collect())
169    }
170
171    /// List item by index.
172    pub fn get(&self, idx: usize) -> Option<&Node> {
173        self.as_list()?.get(idx)
174    }
175
176    /// Item by a path of indices from this node: `node.get_path(&[1, 0, 2])`.
177    pub fn get_path(&self, path: &[usize]) -> Option<&Node> {
178        path.iter().try_fold(self, |node, &idx| node.get(idx))
179    }
180
181    /// Number of list items; 0 for strings and atoms.
182    pub fn len(&self) -> usize {
183        self.as_list().map_or(0, <[Node]>::len)
184    }
185
186    /// List without items; strings and atoms are considered empty too.
187    pub fn is_empty(&self) -> bool {
188        self.len() == 0
189    }
190
191    /// List items; an empty iterator for strings and atoms.
192    pub fn iter(&self) -> std::slice::Iter<'_, Node> {
193        self.as_list().unwrap_or(&[]).iter()
194    }
195
196    pub fn into_list(self) -> Option<Vec<Node>> {
197        match self.value {
198            Value::List(items) => Some(items),
199            _ => None,
200        }
201    }
202
203    /// Value of a string or an atom.
204    pub fn into_string(self) -> Option<String> {
205        match self.value {
206            Value::Str(s) | Value::Atom(s) => Some(s),
207            Value::List(_) => None,
208        }
209    }
210
211    pub fn try_list(&self) -> Result<&[Node], AccessError> {
212        self.as_list().ok_or_else(|| self.unexpected("list"))
213    }
214
215    pub fn try_str(&self) -> Result<&str, AccessError> {
216        self.as_str()
217            .ok_or_else(|| self.unexpected("string or value"))
218    }
219
220    pub fn try_quoted(&self) -> Result<&str, AccessError> {
221        self.as_quoted()
222            .ok_or_else(|| self.unexpected("quoted string"))
223    }
224
225    pub fn try_atom(&self) -> Result<&str, AccessError> {
226        self.as_atom().ok_or_else(|| self.unexpected("value"))
227    }
228
229    /// Atom parsed into any [`FromStr`] type: `node.try_parse::<u32>()?`.
230    ///
231    /// The error names the target type and keeps the reason from [`FromStr::Err`].
232    pub fn try_parse<T>(&self) -> Result<T, AccessError>
233    where
234        T: FromStr,
235        T::Err: fmt::Display,
236    {
237        let expected = type_label::<T>();
238        let atom = self
239            .as_atom()
240            .ok_or_else(|| self.unexpected(expected.clone()))?;
241        atom.parse().map_err(|e: T::Err| {
242            AccessError::new(
243                AccessErrorKind::InvalidValue {
244                    expected,
245                    found: self.describe(),
246                    reason: e.to_string(),
247                },
248                self.position,
249            )
250        })
251    }
252
253    pub fn try_i64(&self) -> Result<i64, AccessError> {
254        self.try_parse()
255    }
256
257    pub fn try_u64(&self) -> Result<u64, AccessError> {
258        self.try_parse()
259    }
260
261    pub fn try_f64(&self) -> Result<f64, AccessError> {
262        self.try_parse()
263    }
264
265    pub fn try_bool(&self) -> Result<bool, AccessError> {
266        self.as_bool()
267            .ok_or_else(|| self.unexpected("boolean (0 or 1)"))
268    }
269
270    pub fn try_guid(&self) -> Result<&str, AccessError> {
271        self.as_guid().ok_or_else(|| self.unexpected("GUID"))
272    }
273
274    pub fn try_get(&self, idx: usize) -> Result<&Node, AccessError> {
275        let items = self.try_list()?;
276        items.get(idx).ok_or_else(|| {
277            AccessError::new(
278                AccessErrorKind::IndexOutOfBounds {
279                    index: idx,
280                    len: items.len(),
281                },
282                self.position,
283            )
284        })
285    }
286
287    /// Item by a path of indices: `node.try_get_path(&[0, 2, 0])?`.
288    ///
289    /// On failure the error carries the path to the node that could not be indexed.
290    pub fn try_get_path(&self, path: &[usize]) -> Result<&Node, AccessError> {
291        let mut node = self;
292        for (depth, &idx) in path.iter().enumerate() {
293            node = node.try_get(idx).map_err(|e| e.with_path(&path[..depth]))?;
294        }
295        Ok(node)
296    }
297
298    /// List items for editing: replace, push, insert or remove nodes.
299    pub fn as_list_mut(&mut self) -> Option<&mut Vec<Node>> {
300        match &mut self.value {
301            Value::List(items) => Some(items),
302            _ => None,
303        }
304    }
305
306    pub fn get_mut(&mut self, idx: usize) -> Option<&mut Node> {
307        self.as_list_mut()?.get_mut(idx)
308    }
309
310    pub fn get_path_mut(&mut self, path: &[usize]) -> Option<&mut Node> {
311        path.iter().try_fold(self, |node, &idx| node.get_mut(idx))
312    }
313
314    /// List items; an empty iterator for strings and atoms.
315    pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, Node> {
316        match &mut self.value {
317            Value::List(items) => items.iter_mut(),
318            _ => [].iter_mut(),
319        }
320    }
321
322    pub fn try_list_mut(&mut self) -> Result<&mut Vec<Node>, AccessError> {
323        if !self.is_list() {
324            return Err(self.unexpected("list"));
325        }
326        Ok(self.as_list_mut().expect("checked above"))
327    }
328
329    pub fn try_get_mut(&mut self, idx: usize) -> Result<&mut Node, AccessError> {
330        let position = self.position;
331        let items = self.try_list_mut()?;
332        let len = items.len();
333        items.get_mut(idx).ok_or_else(|| {
334            AccessError::new(
335                AccessErrorKind::IndexOutOfBounds { index: idx, len },
336                position,
337            )
338        })
339    }
340
341    /// Item by a path of indices for editing: `*node.try_get_path_mut(&[1, 0])? = Node::from(2)`.
342    pub fn try_get_path_mut(&mut self, path: &[usize]) -> Result<&mut Node, AccessError> {
343        let mut node = self;
344        for (depth, &idx) in path.iter().enumerate() {
345            node = node
346                .try_get_mut(idx)
347                .map_err(|e| e.with_path(&path[..depth]))?;
348        }
349        Ok(node)
350    }
351
352    /// Items of a counted list `{count,item1,item2,...}`: `{2,a,b}` gives `[a, b]`.
353    ///
354    /// Fails if the count is not a number or does not match the number of items.
355    pub fn try_counted(&self) -> Result<&[Node], AccessError> {
356        let declared: usize = self
357            .try_get(0)?
358            .try_parse()
359            .map_err(|e| e.with_path(&[0]))?;
360        let items = &self.try_list()?[1..];
361        if items.len() != declared {
362            return Err(AccessError::new(
363                AccessErrorKind::CountMismatch {
364                    declared,
365                    actual: items.len(),
366                },
367                self.position,
368            ));
369        }
370        Ok(items)
371    }
372
373    /// Pre-order depth-first traversal with the path from the root. Not recursive: forms can be deeply nested.
374    pub fn walk<'a>(&'a self, mut visit: impl FnMut(&[usize], &'a Node)) {
375        // (node, depth, index in parent)
376        let mut stack = vec![(self, 0usize, 0usize)];
377        let mut path = Vec::new();
378        while let Some((node, depth, idx)) = stack.pop() {
379            if depth > 0 {
380                path.truncate(depth - 1);
381                path.push(idx);
382            }
383            visit(&path, node);
384            for (i, item) in node.iter().enumerate().rev() {
385                stack.push((item, depth + 1, i));
386            }
387        }
388    }
389
390    /// Short description of the node for error messages.
391    pub fn describe(&self) -> String {
392        const MAX: usize = 40;
393        let clip = |s: &str| match s.char_indices().nth(MAX) {
394            Some((end, _)) => format!("{}…", &s[..end]),
395            None => s.to_owned(),
396        };
397        match &self.value {
398            Value::List(items) => match items.len() {
399                0 => "empty list".to_owned(),
400                1 => "list of 1 item".to_owned(),
401                n => format!("list of {n} items"),
402            },
403            Value::Str(s) => format!("string {:?}", clip(s)),
404            Value::Atom(s) if s.is_empty() => "empty value".to_owned(),
405            Value::Atom(s) => format!("value `{}`", clip(s)),
406        }
407    }
408
409    pub(crate) fn unexpected(&self, expected: impl Into<Cow<'static, str>>) -> AccessError {
410        AccessError::new(
411            AccessErrorKind::UnexpectedType {
412                expected: expected.into(),
413                found: self.describe(),
414            },
415            self.position,
416        )
417    }
418}
419
420/// `u32` for primitives, `Foo` instead of `my_crate::model::Foo` for user types.
421pub(crate) fn type_label<T>() -> Cow<'static, str> {
422    let name = std::any::type_name::<T>();
423    if name.contains('<') {
424        Cow::Borrowed(name)
425    } else {
426        Cow::Borrowed(name.rsplit("::").next().unwrap_or(name))
427    }
428}
429
430fn is_guid(s: &str) -> bool {
431    let bytes = s.as_bytes();
432    bytes.len() == 36
433        && bytes.iter().enumerate().all(|(i, &b)| match i {
434            8 | 13 | 18 | 23 => b == b'-',
435            _ => b.is_ascii_hexdigit(),
436        })
437}
438
439impl fmt::Debug for Node {
440    /// Shows only the value: positions would drown the tree in noise.
441    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
442        self.value.fmt(f)
443    }
444}
445
446impl PartialEq for Node {
447    fn eq(&self, other: &Self) -> bool {
448        self.value == other.value
449    }
450}
451
452impl Eq for Node {}
453
454impl Hash for Node {
455    fn hash<H: Hasher>(&self, state: &mut H) {
456        self.value.hash(state);
457    }
458}
459
460impl Index<usize> for Node {
461    type Output = Node;
462
463    fn index(&self, idx: usize) -> &Node {
464        match self.try_get(idx) {
465            Ok(node) => node,
466            Err(e) => panic!("cannot index node: {e}"),
467        }
468    }
469}
470
471impl IndexMut<usize> for Node {
472    fn index_mut(&mut self, idx: usize) -> &mut Node {
473        match self.try_get_mut(idx) {
474            Ok(node) => node,
475            Err(e) => panic!("cannot index node: {e}"),
476        }
477    }
478}
479
480impl<'a> IntoIterator for &'a Node {
481    type Item = &'a Node;
482    type IntoIter = std::slice::Iter<'a, Node>;
483
484    fn into_iter(self) -> Self::IntoIter {
485        self.iter()
486    }
487}
488
489impl<'a> IntoIterator for &'a mut Node {
490    type Item = &'a mut Node;
491    type IntoIter = std::slice::IterMut<'a, Node>;
492
493    fn into_iter(self) -> Self::IntoIter {
494        self.iter_mut()
495    }
496}
497
498impl From<Value> for Node {
499    fn from(value: Value) -> Self {
500        Self {
501            value,
502            position: None,
503        }
504    }
505}
506
507impl From<bool> for Node {
508    fn from(b: bool) -> Self {
509        Node::atom(if b { "1" } else { "0" })
510    }
511}
512
513macro_rules! from_number {
514    ($($t:ty),*) => {$(
515        impl From<$t> for Node {
516            fn from(n: $t) -> Self {
517                Node::atom(n.to_string())
518            }
519        }
520    )*};
521}
522
523from_number!(i8, i16, i32, i64, u8, u16, u32, u64, usize, isize);
524
525impl From<Vec<Node>> for Node {
526    fn from(items: Vec<Node>) -> Self {
527        Value::List(items).into()
528    }
529}
530
531impl<T: Into<Node>> FromIterator<T> for Node {
532    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
533        Node::list(iter.into_iter().map(Into::into))
534    }
535}
536
537#[cfg(test)]
538mod tests {
539    use super::*;
540
541    fn sample() -> Node {
542        Node::from(vec![
543            Node::atom("12"),
544            Node::from(vec![
545                Node::string("text"),
546                Node::atom("c2f34349-b1e0-4060-8abf-3c59f38ac69f"),
547                Node::from(true),
548            ]),
549            Node::atom("#base64:QUJD\r\r\nREVG\r\r\n"),
550            Node::atom(""),
551        ])
552    }
553
554    #[test]
555    fn typed_accessors() {
556        let node = sample();
557        assert_eq!(node[0].as_i64(), Some(12));
558        assert_eq!(node[0].as_f64(), Some(12.0));
559        assert_eq!(node[0].as_parsed::<u8>(), Some(12));
560        assert_eq!(
561            node.get_path(&[1, 0]).and_then(Node::as_quoted),
562            Some("text")
563        );
564        assert_eq!(node.get_path(&[1, 0]).and_then(Node::as_atom), None);
565        assert!(node.get_path(&[1, 1]).and_then(Node::as_guid).is_some());
566        assert_eq!(node.get_path(&[1, 2]).and_then(Node::as_bool), Some(true));
567        assert_eq!(node[2].as_base64().as_deref(), Some("QUJDREVG"));
568        assert!(node[3].is_empty_atom());
569        assert_eq!(node.get_path(&[1, 5]), None);
570        assert_eq!(node.len(), 4);
571        assert_eq!(node[0].iter().count(), 0);
572        assert_eq!(node[0].position(), None);
573    }
574
575    #[test]
576    fn try_accessors_explain_mismatch() {
577        let node = sample();
578        insta::assert_snapshot!(node.try_get(7).unwrap_err(), @"index 7 is out of bounds for list of length 4");
579        insta::assert_snapshot!(node[0].try_list().unwrap_err(), @"expected list, found value `12`");
580        insta::assert_snapshot!(node[1].try_i64().unwrap_err(), @"expected i64, found list of 3 items");
581        insta::assert_snapshot!(node[3].try_guid().unwrap_err(), @"expected GUID, found empty value");
582        insta::assert_snapshot!(node[1][0].try_bool().unwrap_err(), @r#"expected boolean (0 or 1), found string "text""#);
583        insta::assert_snapshot!(node[1][1].try_parse::<u32>().unwrap_err(), @"expected u32, found value `c2f34349-b1e0-4060-8abf-3c59f38ac69f`: invalid digit found in string");
584        insta::assert_snapshot!(node.try_get_path(&[1, 0, 3]).unwrap_err(), @r#"expected list, found string "text" (path [1, 0])"#);
585        insta::assert_snapshot!(node.try_get_path(&[1, 9]).unwrap_err(), @"index 9 is out of bounds for list of length 3 (path [1])");
586    }
587
588    #[test]
589    fn parsed_nodes_report_positions() {
590        let node: Node = "{1,\r\n{x,\"s\"}}".parse().unwrap();
591        insta::assert_snapshot!(node.try_get_path(&[1, 0]).unwrap().try_parse::<u8>().unwrap_err(), @"expected u8, found value `x` at line 2, column 2: invalid digit found in string");
592        insta::assert_snapshot!(node.try_get_path(&[1, 1, 0]).unwrap_err(), @r#"expected list, found string "s" at line 2, column 4 (path [1, 1])"#);
593        insta::assert_snapshot!(node[1].try_get(5).unwrap_err(), @"index 5 is out of bounds for list of length 2 at line 2, column 1");
594    }
595
596    #[test]
597    fn equality_ignores_positions() {
598        let parsed: Node = "{1,\"a\"}".parse().unwrap();
599        assert_eq!(parsed, Node::list([Node::from(1), Node::string("a")]));
600    }
601
602    #[test]
603    fn walks_with_paths() {
604        let mut visited = Vec::new();
605        let node = Node::from(vec![
606            Node::atom("12"),
607            Node::from(vec![Node::string("a"), Node::from(vec![Node::from(true)])]),
608            Node::atom(""),
609        ]);
610        node.walk(|path, node| visited.push(format!("{path:?} {}", node.describe())));
611        insta::assert_snapshot!(visited.join("\n"), @r#"
612        [] list of 3 items
613        [0] value `12`
614        [1] list of 2 items
615        [1, 0] string "a"
616        [1, 1] list of 1 item
617        [1, 1, 0] value `1`
618        [2] empty value
619        "#);
620    }
621
622    #[test]
623    #[should_panic(expected = "cannot index node: expected list, found value `12`")]
624    fn index_panics_with_message() {
625        let _ = &sample()[0][0];
626    }
627
628    #[test]
629    fn type_labels_are_short() {
630        struct Local;
631        assert_eq!(type_label::<u32>(), "u32");
632        assert_eq!(type_label::<Local>(), "Local");
633        assert_eq!(type_label::<String>(), "String");
634    }
635
636    #[test]
637    fn edits_in_place() {
638        let mut node: Node = "{1,\r\n{a,\"b\"}}".parse().unwrap();
639        node[0] = Node::from(2);
640        *node.try_get_path_mut(&[1, 1]).unwrap().value_mut() = Value::Str("c".into());
641        node.get_mut(1)
642            .unwrap()
643            .try_list_mut()
644            .unwrap()
645            .push(Node::atom("d"));
646        for item in &mut node[1] {
647            if item.as_atom() == Some("a") {
648                *item = Node::atom("z");
649            }
650        }
651        assert_eq!(node.to_string(), "{2,\r\n{z,\"c\",d}\r\n}");
652        // The edited value keeps the position of the original node.
653        assert_eq!(
654            node.get_path(&[1, 1]).unwrap().position().unwrap().column,
655            4
656        );
657        assert!(node.get_path_mut(&[0, 0]).is_none());
658        assert_eq!(node[0].iter_mut().count(), 0);
659
660        insta::assert_snapshot!(node.try_get_path_mut(&[1, 7]).unwrap_err(), @"index 7 is out of bounds for list of length 3 at line 2, column 1 (path [1])");
661        insta::assert_snapshot!(node[0].try_list_mut().unwrap_err(), @"expected list, found value `2`");
662    }
663
664    #[test]
665    #[should_panic(expected = "cannot index node: index 5 is out of bounds for list of length 2")]
666    fn index_mut_panics_with_message() {
667        let mut node = Node::list([Node::from(1), Node::from(2)]);
668        node[5] = Node::from(3);
669    }
670
671    #[test]
672    fn counted_lists() {
673        let node: Node = "{2,a,\r\n{b}}".parse().unwrap();
674        assert_eq!(
675            node.try_counted().unwrap(),
676            [Node::atom("a"), Node::list([Node::atom("b")])]
677        );
678        assert_eq!(Node::counted(node.try_counted().unwrap().to_vec()), node);
679        assert_eq!(Node::counted([]).to_string(), "{0}");
680
681        let bad: Node = "{\r\n{x},\r\n{3,a},\r\n{}}".parse().unwrap();
682        insta::assert_snapshot!(bad[0].try_counted().unwrap_err(), @"expected usize, found value `x` at line 2, column 2 (path [0]): invalid digit found in string");
683        insta::assert_snapshot!(bad[1].try_counted().unwrap_err(), @"list declares 3 items, but has 1 at line 3, column 1");
684        insta::assert_snapshot!(bad[2].try_counted().unwrap_err(), @"index 0 is out of bounds for list of length 0 at line 4, column 1");
685    }
686
687    #[test]
688    fn detects_guid() {
689        assert!(is_guid("c2f34349-b1e0-4060-8abf-3c59f38ac69f"));
690        assert!(!is_guid("c2f34349-b1e0-4060-8abf-3c59f38ac69"));
691    }
692}