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, Text};
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 text written by the platform is written back
16/// byte for byte. Whitespace between items is not kept: writing uses the platform layout.
17/// Whitespace inside an atom, trailing whitespace included, is part of its text.
18/// Equality and hashing ignore the position.
19///
20/// Like parsing and writing, cloning, comparison, hashing, `Debug` and dropping
21/// are not recursive: a tree of any depth does not overflow the stack.
22pub struct Node {
23    value: Value,
24    position: Option<Position>,
25}
26
27/// Content of a [`Node`].
28#[derive(Clone, PartialEq, Eq, Hash)]
29pub enum Value {
30    /// A list `{a,b,c}`.
31    List(Vec<Node>),
32    /// A quoted string; doubled quotes and surrogate escapes `"\d83d` are already unescaped.
33    Str(Text),
34    /// An unquoted value: number, GUID, literal; may be empty (`{2,x,}`).
35    ///
36    /// It runs up to the next `,` or `}`: whitespace and line breaks before them are part of it.
37    /// Binary data `{#base64:…}` is an atom too, line breaks included.
38    Atom(Text),
39}
40
41impl Node {
42    pub(crate) fn parsed(value: Value, position: Position) -> Self {
43        Self {
44            value,
45            position: Some(position),
46        }
47    }
48
49    /// A list of nodes.
50    pub fn list(items: impl IntoIterator<Item = Node>) -> Self {
51        Value::List(items.into_iter().collect()).into()
52    }
53
54    /// An unquoted atom (GUID, literal, number).
55    pub fn atom(value: impl Into<Text>) -> Self {
56        Value::Atom(value.into()).into()
57    }
58
59    /// A quoted string.
60    ///
61    /// There is no `From<&str>` for building: `Node::try_from(&str)` parses text.
62    pub fn string(value: impl Into<Text>) -> Self {
63        Value::Str(value.into()).into()
64    }
65
66    /// A counted list `{count,item1,item2,...}`, the common 1C shape.
67    pub fn counted(items: impl IntoIterator<Item = Node>) -> Self {
68        let items: Vec<Node> = items.into_iter().collect();
69        Node::list(std::iter::once(Node::from(items.len())).chain(items))
70    }
71
72    pub fn value(&self) -> &Value {
73        &self.value
74    }
75
76    pub fn into_value(mut self) -> Value {
77        self.take_value()
78    }
79
80    /// Mutable access to the content; the position stays where the node was parsed.
81    pub fn value_mut(&mut self) -> &mut Value {
82        &mut self.value
83    }
84
85    /// Where the node starts in the input; `None` for nodes built in code.
86    ///
87    /// Edits do not change it: it always refers to the original text.
88    pub fn position(&self) -> Option<Position> {
89        self.position
90    }
91
92    pub fn is_list(&self) -> bool {
93        matches!(self.value, Value::List(_))
94    }
95
96    pub fn is_str(&self) -> bool {
97        matches!(self.value, Value::Str(_))
98    }
99
100    pub fn is_atom(&self) -> bool {
101        matches!(self.value, Value::Atom(_))
102    }
103
104    /// An empty atom: no value between commas (`{1,,2}`).
105    pub fn is_empty_atom(&self) -> bool {
106        matches!(&self.value, Value::Atom(s) if s.is_empty())
107    }
108
109    pub fn as_list(&self) -> Option<&[Node]> {
110        match &self.value {
111            Value::List(items) => Some(items),
112            _ => None,
113        }
114    }
115
116    /// Text of a string or an atom, unpaired surrogates included.
117    pub fn as_text(&self) -> Option<&Text> {
118        match &self.value {
119            Value::Str(s) | Value::Atom(s) => Some(s),
120            Value::List(_) => None,
121        }
122    }
123
124    /// Value of a string or an atom; `None` if it holds an unpaired surrogate.
125    pub fn as_str(&self) -> Option<&str> {
126        self.as_text()?.as_str()
127    }
128
129    /// Value of a string or an atom with unpaired surrogates replaced by U+FFFD.
130    pub fn to_string_lossy(&self) -> Option<Cow<'_, str>> {
131        Some(self.as_text()?.to_string_lossy())
132    }
133
134    /// Quoted string only.
135    pub fn as_quoted(&self) -> Option<&str> {
136        match &self.value {
137            Value::Str(s) => s.as_str(),
138            _ => None,
139        }
140    }
141
142    /// Atom only, as written: trailing whitespace included.
143    pub fn as_atom(&self) -> Option<&str> {
144        match &self.value {
145            Value::Atom(s) => s.as_str(),
146            _ => None,
147        }
148    }
149
150    /// Atom without leading and trailing whitespace: `{12\r\n}` gives `12`.
151    ///
152    /// The typed accessors (`as_i64`, `try_parse` and the like) read this text.
153    pub fn as_atom_trimmed(&self) -> Option<&str> {
154        self.as_atom().map(trim)
155    }
156
157    /// Atom parsed into any [`FromStr`] type: `node.as_parsed::<u32>()`.
158    pub fn as_parsed<T: FromStr>(&self) -> Option<T> {
159        self.as_atom_trimmed()?.parse().ok()
160    }
161
162    pub fn as_i64(&self) -> Option<i64> {
163        self.as_parsed()
164    }
165
166    pub fn as_u64(&self) -> Option<u64> {
167        self.as_parsed()
168    }
169
170    pub fn as_f64(&self) -> Option<f64> {
171        self.as_parsed()
172    }
173
174    /// Atom `1` is `true`, `0` is `false`.
175    pub fn as_bool(&self) -> Option<bool> {
176        match self.as_atom_trimmed()? {
177            "1" => Some(true),
178            "0" => Some(false),
179            _ => None,
180        }
181    }
182
183    /// An atom shaped like a GUID `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`.
184    pub fn as_guid(&self) -> Option<&str> {
185        self.as_atom_trimmed().filter(|s| is_guid(s))
186    }
187
188    /// Payload of a `#base64:…` atom without the prefix and line breaks (not decoded).
189    pub fn as_base64(&self) -> Option<String> {
190        let data = self.as_atom()?.strip_prefix(BASE64)?;
191        Some(data.chars().filter(|c| !c.is_ascii_whitespace()).collect())
192    }
193
194    /// List item by index.
195    pub fn get(&self, idx: usize) -> Option<&Node> {
196        self.as_list()?.get(idx)
197    }
198
199    /// Item by a path of indices from this node: `node.get_path(&[1, 0, 2])`.
200    pub fn get_path(&self, path: &[usize]) -> Option<&Node> {
201        path.iter().try_fold(self, |node, &idx| node.get(idx))
202    }
203
204    /// Number of list items; 0 for strings and atoms.
205    pub fn len(&self) -> usize {
206        self.as_list().map_or(0, <[Node]>::len)
207    }
208
209    /// List without items; strings and atoms are considered empty too.
210    pub fn is_empty(&self) -> bool {
211        self.len() == 0
212    }
213
214    /// List items; an empty iterator for strings and atoms.
215    pub fn iter(&self) -> std::slice::Iter<'_, Node> {
216        self.as_list().unwrap_or(&[]).iter()
217    }
218
219    pub fn into_list(mut self) -> Option<Vec<Node>> {
220        match self.take_value() {
221            Value::List(items) => Some(items),
222            _ => None,
223        }
224    }
225
226    /// Text of a string or an atom.
227    pub fn into_text(mut self) -> Option<Text> {
228        match self.take_value() {
229            Value::Str(s) | Value::Atom(s) => Some(s),
230            Value::List(_) => None,
231        }
232    }
233
234    /// Value of a string or an atom; unpaired surrogates are replaced by U+FFFD.
235    pub fn into_string(self) -> Option<String> {
236        self.into_text()
237            .map(|text| text.into_string().unwrap_or_else(|text| text.to_string()))
238    }
239
240    pub fn try_list(&self) -> Result<&[Node], AccessError> {
241        self.as_list().ok_or_else(|| self.unexpected("list"))
242    }
243
244    pub fn try_text(&self) -> Result<&Text, AccessError> {
245        self.as_text()
246            .ok_or_else(|| self.unexpected("string or value"))
247    }
248
249    /// Fails for a list and for text with an unpaired surrogate.
250    pub fn try_str(&self) -> Result<&str, AccessError> {
251        self.unicode(self.try_text()?, "string or value")
252    }
253
254    pub fn try_quoted(&self) -> Result<&str, AccessError> {
255        match &self.value {
256            Value::Str(s) => self.unicode(s, "quoted string"),
257            _ => Err(self.unexpected("quoted string")),
258        }
259    }
260
261    /// Atom as written: trailing whitespace included.
262    pub fn try_atom(&self) -> Result<&str, AccessError> {
263        match &self.value {
264            Value::Atom(s) => self.unicode(s, "value"),
265            _ => Err(self.unexpected("value")),
266        }
267    }
268
269    /// The text as `&str`, or an error about the unpaired surrogate in it.
270    fn unicode<'a>(&self, text: &'a Text, expected: &'static str) -> Result<&'a str, AccessError> {
271        text.as_str().ok_or_else(|| {
272            AccessError::new(
273                AccessErrorKind::InvalidValue {
274                    expected: expected.into(),
275                    found: self.describe(),
276                    reason: "it holds an unpaired surrogate".to_owned(),
277                },
278                self.position,
279            )
280        })
281    }
282
283    /// Atom parsed into any [`FromStr`] type: `node.try_parse::<u32>()?`.
284    ///
285    /// The error names the target type and keeps the reason from [`FromStr::Err`].
286    pub fn try_parse<T>(&self) -> Result<T, AccessError>
287    where
288        T: FromStr,
289        T::Err: fmt::Display,
290    {
291        let expected = type_label::<T>();
292        let atom = self
293            .as_atom_trimmed()
294            .ok_or_else(|| self.unexpected(expected.clone()))?;
295        atom.parse().map_err(|e: T::Err| {
296            AccessError::new(
297                AccessErrorKind::InvalidValue {
298                    expected,
299                    found: self.describe(),
300                    reason: e.to_string(),
301                },
302                self.position,
303            )
304        })
305    }
306
307    pub fn try_i64(&self) -> Result<i64, AccessError> {
308        self.try_parse()
309    }
310
311    pub fn try_u64(&self) -> Result<u64, AccessError> {
312        self.try_parse()
313    }
314
315    pub fn try_f64(&self) -> Result<f64, AccessError> {
316        self.try_parse()
317    }
318
319    pub fn try_bool(&self) -> Result<bool, AccessError> {
320        self.as_bool()
321            .ok_or_else(|| self.unexpected("boolean (0 or 1)"))
322    }
323
324    pub fn try_guid(&self) -> Result<&str, AccessError> {
325        self.as_guid().ok_or_else(|| self.unexpected("GUID"))
326    }
327
328    pub fn try_get(&self, idx: usize) -> Result<&Node, AccessError> {
329        let items = self.try_list()?;
330        items.get(idx).ok_or_else(|| {
331            AccessError::new(
332                AccessErrorKind::IndexOutOfBounds {
333                    index: idx,
334                    len: items.len(),
335                },
336                self.position,
337            )
338        })
339    }
340
341    /// Item by a path of indices: `node.try_get_path(&[0, 2, 0])?`.
342    ///
343    /// On failure the error carries the path to the node that could not be indexed.
344    pub fn try_get_path(&self, path: &[usize]) -> Result<&Node, AccessError> {
345        let mut node = self;
346        for (depth, &idx) in path.iter().enumerate() {
347            node = node.try_get(idx).map_err(|e| e.with_path(&path[..depth]))?;
348        }
349        Ok(node)
350    }
351
352    /// List items for editing: replace, push, insert or remove nodes.
353    pub fn as_list_mut(&mut self) -> Option<&mut Vec<Node>> {
354        match &mut self.value {
355            Value::List(items) => Some(items),
356            _ => None,
357        }
358    }
359
360    pub fn get_mut(&mut self, idx: usize) -> Option<&mut Node> {
361        self.as_list_mut()?.get_mut(idx)
362    }
363
364    pub fn get_path_mut(&mut self, path: &[usize]) -> Option<&mut Node> {
365        path.iter().try_fold(self, |node, &idx| node.get_mut(idx))
366    }
367
368    /// List items; an empty iterator for strings and atoms.
369    pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, Node> {
370        match &mut self.value {
371            Value::List(items) => items.iter_mut(),
372            _ => [].iter_mut(),
373        }
374    }
375
376    pub fn try_list_mut(&mut self) -> Result<&mut Vec<Node>, AccessError> {
377        if !self.is_list() {
378            return Err(self.unexpected("list"));
379        }
380        Ok(self.as_list_mut().expect("checked above"))
381    }
382
383    pub fn try_get_mut(&mut self, idx: usize) -> Result<&mut Node, AccessError> {
384        let position = self.position;
385        let items = self.try_list_mut()?;
386        let len = items.len();
387        items.get_mut(idx).ok_or_else(|| {
388            AccessError::new(
389                AccessErrorKind::IndexOutOfBounds { index: idx, len },
390                position,
391            )
392        })
393    }
394
395    /// Item by a path of indices for editing: `*node.try_get_path_mut(&[1, 0])? = Node::from(2)`.
396    pub fn try_get_path_mut(&mut self, path: &[usize]) -> Result<&mut Node, AccessError> {
397        let mut node = self;
398        for (depth, &idx) in path.iter().enumerate() {
399            node = node
400                .try_get_mut(idx)
401                .map_err(|e| e.with_path(&path[..depth]))?;
402        }
403        Ok(node)
404    }
405
406    /// Items of a counted list `{count,item1,item2,...}`: `{2,a,b}` gives `[a, b]`.
407    ///
408    /// Fails if the count is not a number or does not match the number of items.
409    pub fn try_counted(&self) -> Result<&[Node], AccessError> {
410        let declared: usize = self
411            .try_get(0)?
412            .try_parse()
413            .map_err(|e| e.with_path(&[0]))?;
414        let items = &self.try_list()?[1..];
415        if items.len() != declared {
416            return Err(AccessError::new(
417                AccessErrorKind::CountMismatch {
418                    declared,
419                    actual: items.len(),
420                },
421                self.position,
422            ));
423        }
424        Ok(items)
425    }
426
427    /// Pre-order depth-first traversal with the path from the root. Not recursive: forms can be deeply nested.
428    pub fn walk<'a>(&'a self, mut visit: impl FnMut(&[usize], &'a Node)) {
429        // (node, depth, index in parent)
430        let mut stack = vec![(self, 0usize, 0usize)];
431        let mut path = Vec::new();
432        while let Some((node, depth, idx)) = stack.pop() {
433            if depth > 0 {
434                path.truncate(depth - 1);
435                path.push(idx);
436            }
437            visit(&path, node);
438            for (i, item) in node.iter().enumerate().rev() {
439                stack.push((item, depth + 1, i));
440            }
441        }
442    }
443
444    /// Short description of the node for error messages.
445    pub fn describe(&self) -> String {
446        const MAX: usize = 40;
447        let clip = |text: &Text| {
448            let s = text.to_string_lossy();
449            match s.char_indices().nth(MAX) {
450                Some((end, _)) => format!("{}…", &s[..end]),
451                None => s.into_owned(),
452            }
453        };
454        match &self.value {
455            Value::List(items) => match items.len() {
456                0 => "empty list".to_owned(),
457                1 => "list of 1 item".to_owned(),
458                n => format!("list of {n} items"),
459            },
460            Value::Str(s) => format!("string {:?}", clip(s)),
461            Value::Atom(s) if s.is_empty() => "empty value".to_owned(),
462            Value::Atom(s) => format!("value `{}`", clip(s)),
463        }
464    }
465
466    /// Moves the value out: fields of a `Drop` type cannot be moved directly.
467    fn take_value(&mut self) -> Value {
468        std::mem::replace(&mut self.value, Value::Atom(Text::new()))
469    }
470
471    pub(crate) fn unexpected(&self, expected: impl Into<Cow<'static, str>>) -> AccessError {
472        AccessError::new(
473            AccessErrorKind::UnexpectedType {
474                expected: expected.into(),
475                found: self.describe(),
476            },
477            self.position,
478        )
479    }
480}
481
482/// `u32` for primitives, `Foo` instead of `my_crate::model::Foo` for user types.
483pub(crate) fn type_label<T>() -> Cow<'static, str> {
484    let name = std::any::type_name::<T>();
485    if name.contains('<') {
486        Cow::Borrowed(name)
487    } else {
488        Cow::Borrowed(name.rsplit("::").next().unwrap_or(name))
489    }
490}
491
492/// Trims whitespace as the platform skips it (see [`is_space`]).
493fn trim(s: &str) -> &str {
494    s.trim_matches(is_space)
495}
496
497/// Whitespace of the platform: ICU `u_isspace` as of Unicode 6.0 (ICU 4.6).
498///
499/// Categories Zs, Zl and Zp, `\t`..`\r`, `\x1c`..`\x1f` and U+0085: unlike
500/// [`char::is_whitespace`], also U+00A0, U+2007, U+202F and U+180E.
501pub(crate) fn is_space(c: char) -> bool {
502    matches!(
503        c,
504        '\t'..='\r'
505            | '\x1c'..='\x20'
506            | '\u{85}'
507            | '\u{a0}'
508            | '\u{1680}'
509            | '\u{180e}'
510            | '\u{2000}'..='\u{200a}'
511            | '\u{2028}'
512            | '\u{2029}'
513            | '\u{202f}'
514            | '\u{205f}'
515            | '\u{3000}'
516    )
517}
518
519fn is_guid(s: &str) -> bool {
520    let bytes = s.as_bytes();
521    bytes.len() == 36
522        && bytes.iter().enumerate().all(|(i, &b)| match i {
523            8 | 13 | 18 | 23 => b == b'-',
524            _ => b.is_ascii_hexdigit(),
525        })
526}
527
528impl fmt::Debug for Node {
529    /// Shows only the value: positions would drown the tree in noise.
530    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
531        self.value.fmt(f)
532    }
533}
534
535impl fmt::Debug for Value {
536    /// The same text as a derived `Debug`, `{:#?}` included, but not recursive.
537    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
538        fn indent(f: &mut fmt::Formatter<'_>, depth: usize) -> fmt::Result {
539            (0..depth).try_for_each(|_| f.write_str("    "))
540        }
541
542        let pretty = f.alternate();
543        // (items left, indentation depth of the list's `List(`)
544        let mut stack: Vec<(std::slice::Iter<'_, Node>, usize)> = Vec::new();
545        let mut next = Some((self, 0));
546        loop {
547            if let Some((value, depth)) = next.take() {
548                let (name, text) = match value {
549                    Value::List(items) if !items.is_empty() => {
550                        if pretty {
551                            f.write_str("List(\n")?;
552                            indent(f, depth + 1)?;
553                            f.write_str("[\n")?;
554                            indent(f, depth + 2)?;
555                        } else {
556                            f.write_str("List([")?;
557                        }
558                        let mut items = items.iter();
559                        next = items.next().map(|item| (&item.value, depth + 2));
560                        stack.push((items, depth));
561                        continue;
562                    }
563                    Value::List(_) => ("List", None),
564                    Value::Str(s) => ("Str", Some(s)),
565                    Value::Atom(s) => ("Atom", Some(s)),
566                };
567                f.write_str(name)?;
568                f.write_str("(")?;
569                if pretty {
570                    f.write_str("\n")?;
571                    indent(f, depth + 1)?;
572                }
573                match text {
574                    Some(s) => write!(f, "{s:?}")?,
575                    None => f.write_str("[]")?,
576                }
577                if pretty {
578                    f.write_str(",\n")?;
579                    indent(f, depth)?;
580                }
581                f.write_str(")")?;
582            } else {
583                // The innermost open list has no items left.
584                let Some((_, depth)) = stack.pop() else {
585                    return Ok(());
586                };
587                if pretty {
588                    indent(f, depth + 1)?;
589                    f.write_str("],\n")?;
590                    indent(f, depth)?;
591                    f.write_str(")")?;
592                } else {
593                    f.write_str("])")?;
594                }
595            }
596            // A value is written: separate it from the next item of its list.
597            let Some((items, depth)) = stack.last_mut() else {
598                return Ok(());
599            };
600            if pretty {
601                f.write_str(",\n")?;
602            }
603            if let Some(item) = items.next() {
604                if pretty {
605                    indent(f, *depth + 2)?;
606                } else {
607                    f.write_str(", ")?;
608                }
609                next = Some((&item.value, *depth + 2));
610            }
611        }
612    }
613}
614
615impl Clone for Node {
616    /// Not recursive: a deep tree does not overflow the stack.
617    fn clone(&self) -> Self {
618        let Value::List(items) = &self.value else {
619            return Self {
620                value: self.value.clone(),
621                position: self.position,
622            };
623        };
624        // (source items left, copied items, position of the list)
625        let mut stack = vec![(items.iter(), Vec::with_capacity(items.len()), self.position)];
626        loop {
627            let (source, copied, _) = stack.last_mut().expect("clone stack is not empty");
628            match source.next() {
629                Some(item) => match &item.value {
630                    Value::List(items) => {
631                        stack.push((items.iter(), Vec::with_capacity(items.len()), item.position));
632                    }
633                    scalar => copied.push(Self {
634                        value: scalar.clone(),
635                        position: item.position,
636                    }),
637                },
638                None => {
639                    let (_, items, position) = stack.pop().expect("clone stack is not empty");
640                    let done = Self {
641                        value: Value::List(items),
642                        position,
643                    };
644                    match stack.last_mut() {
645                        Some((_, parent, _)) => parent.push(done),
646                        None => return done,
647                    }
648                }
649            }
650        }
651    }
652}
653
654impl Drop for Node {
655    /// Not recursive: nested lists are moved onto a heap stack and dropped one by one.
656    fn drop(&mut self) {
657        let Value::List(items) = &mut self.value else {
658            return;
659        };
660        // Items without nested items of their own drop without recursion anyway.
661        if items.iter().all(Node::is_empty) {
662            return;
663        }
664        let mut stack = std::mem::take(items);
665        while let Some(mut node) = stack.pop() {
666            if let Value::List(items) = &mut node.value {
667                stack.append(items);
668            }
669        }
670    }
671}
672
673impl PartialEq for Node {
674    /// Ignores positions. Not recursive.
675    fn eq(&self, other: &Self) -> bool {
676        let mut stack = vec![(std::slice::from_ref(self), std::slice::from_ref(other))];
677        while let Some((left, right)) = stack.pop() {
678            if left.len() != right.len() {
679                return false;
680            }
681            for (left, right) in left.iter().zip(right) {
682                match (&left.value, &right.value) {
683                    (Value::List(left), Value::List(right)) => stack.push((left, right)),
684                    // Scalars, or a list against a scalar: compared without recursion.
685                    (left, right) if left != right => return false,
686                    _ => {}
687                }
688            }
689        }
690        true
691    }
692}
693
694impl Eq for Node {}
695
696impl Hash for Node {
697    /// Ignores positions. Not recursive: pre-order with list lengths.
698    fn hash<H: Hasher>(&self, state: &mut H) {
699        let mut stack = vec![self];
700        while let Some(node) = stack.pop() {
701            std::mem::discriminant(&node.value).hash(state);
702            match &node.value {
703                Value::List(items) => {
704                    items.len().hash(state);
705                    stack.extend(items.iter().rev());
706                }
707                Value::Str(s) | Value::Atom(s) => s.hash(state),
708            }
709        }
710    }
711}
712
713impl Index<usize> for Node {
714    type Output = Node;
715
716    fn index(&self, idx: usize) -> &Node {
717        match self.try_get(idx) {
718            Ok(node) => node,
719            Err(e) => panic!("cannot index node: {e}"),
720        }
721    }
722}
723
724impl IndexMut<usize> for Node {
725    fn index_mut(&mut self, idx: usize) -> &mut Node {
726        match self.try_get_mut(idx) {
727            Ok(node) => node,
728            Err(e) => panic!("cannot index node: {e}"),
729        }
730    }
731}
732
733impl<'a> IntoIterator for &'a Node {
734    type Item = &'a Node;
735    type IntoIter = std::slice::Iter<'a, Node>;
736
737    fn into_iter(self) -> Self::IntoIter {
738        self.iter()
739    }
740}
741
742impl<'a> IntoIterator for &'a mut Node {
743    type Item = &'a mut Node;
744    type IntoIter = std::slice::IterMut<'a, Node>;
745
746    fn into_iter(self) -> Self::IntoIter {
747        self.iter_mut()
748    }
749}
750
751impl From<Value> for Node {
752    fn from(value: Value) -> Self {
753        Self {
754            value,
755            position: None,
756        }
757    }
758}
759
760impl From<bool> for Node {
761    fn from(b: bool) -> Self {
762        Node::atom(if b { "1" } else { "0" })
763    }
764}
765
766macro_rules! from_number {
767    ($($t:ty),*) => {$(
768        impl From<$t> for Node {
769            fn from(n: $t) -> Self {
770                Node::atom(n.to_string())
771            }
772        }
773    )*};
774}
775
776from_number!(i8, i16, i32, i64, u8, u16, u32, u64, usize, isize);
777
778impl From<Vec<Node>> for Node {
779    fn from(items: Vec<Node>) -> Self {
780        Value::List(items).into()
781    }
782}
783
784impl<T: Into<Node>> FromIterator<T> for Node {
785    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
786        Node::list(iter.into_iter().map(Into::into))
787    }
788}
789
790#[cfg(test)]
791mod tests {
792    use super::*;
793
794    fn sample() -> Node {
795        Node::from(vec![
796            Node::atom("12"),
797            Node::from(vec![
798                Node::string("text"),
799                Node::atom("c2f34349-b1e0-4060-8abf-3c59f38ac69f"),
800                Node::from(true),
801            ]),
802            Node::atom("#base64:QUJD\r\r\nREVG\r\r\n"),
803            Node::atom(""),
804        ])
805    }
806
807    #[test]
808    fn typed_accessors() {
809        let node = sample();
810        assert_eq!(node[0].as_i64(), Some(12));
811        assert_eq!(node[0].as_f64(), Some(12.0));
812        assert_eq!(node[0].as_parsed::<u8>(), Some(12));
813        assert_eq!(
814            node.get_path(&[1, 0]).and_then(Node::as_quoted),
815            Some("text")
816        );
817        assert_eq!(node.get_path(&[1, 0]).and_then(Node::as_atom), None);
818        assert!(node.get_path(&[1, 1]).and_then(Node::as_guid).is_some());
819        assert_eq!(node.get_path(&[1, 2]).and_then(Node::as_bool), Some(true));
820        assert_eq!(node[2].as_base64().as_deref(), Some("QUJDREVG"));
821        assert!(node[3].is_empty_atom());
822        assert_eq!(node.get_path(&[1, 5]), None);
823        assert_eq!(node.len(), 4);
824        assert_eq!(node[0].iter().count(), 0);
825        assert_eq!(node[0].position(), None);
826    }
827
828    #[test]
829    fn try_accessors_explain_mismatch() {
830        let node = sample();
831        insta::assert_snapshot!(node.try_get(7).unwrap_err(), @"index 7 is out of bounds for list of length 4");
832        insta::assert_snapshot!(node[0].try_list().unwrap_err(), @"expected list, found value `12`");
833        insta::assert_snapshot!(node[1].try_i64().unwrap_err(), @"expected i64, found list of 3 items");
834        insta::assert_snapshot!(node[3].try_guid().unwrap_err(), @"expected GUID, found empty value");
835        insta::assert_snapshot!(node[1][0].try_bool().unwrap_err(), @r#"expected boolean (0 or 1), found string "text""#);
836        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");
837        insta::assert_snapshot!(node.try_get_path(&[1, 0, 3]).unwrap_err(), @r#"expected list, found string "text" (path [1, 0])"#);
838        insta::assert_snapshot!(node.try_get_path(&[1, 9]).unwrap_err(), @"index 9 is out of bounds for list of length 3 (path [1])");
839    }
840
841    #[test]
842    fn parsed_nodes_report_positions() {
843        let node: Node = "{1,\r\n{x,\"s\"}}".parse().unwrap();
844        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");
845        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])"#);
846        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");
847    }
848
849    #[test]
850    fn equality_ignores_positions() {
851        let parsed: Node = "{1,\"a\"}".parse().unwrap();
852        assert_eq!(parsed, Node::list([Node::from(1), Node::string("a")]));
853    }
854
855    #[test]
856    fn walks_with_paths() {
857        let mut visited = Vec::new();
858        let node = Node::from(vec![
859            Node::atom("12"),
860            Node::from(vec![Node::string("a"), Node::from(vec![Node::from(true)])]),
861            Node::atom(""),
862        ]);
863        node.walk(|path, node| visited.push(format!("{path:?} {}", node.describe())));
864        insta::assert_snapshot!(visited.join("\n"), @r#"
865        [] list of 3 items
866        [0] value `12`
867        [1] list of 2 items
868        [1, 0] string "a"
869        [1, 1] list of 1 item
870        [1, 1, 0] value `1`
871        [2] empty value
872        "#);
873    }
874
875    #[test]
876    #[should_panic(expected = "cannot index node: expected list, found value `12`")]
877    fn index_panics_with_message() {
878        let _ = &sample()[0][0];
879    }
880
881    #[test]
882    fn type_labels_are_short() {
883        struct Local;
884        assert_eq!(type_label::<u32>(), "u32");
885        assert_eq!(type_label::<Local>(), "Local");
886        assert_eq!(type_label::<String>(), "String");
887    }
888
889    #[test]
890    fn edits_in_place() {
891        let mut node: Node = "{1,\r\n{a,\"b\"}}".parse().unwrap();
892        node[0] = Node::from(2);
893        *node.try_get_path_mut(&[1, 1]).unwrap().value_mut() = Value::Str("c".into());
894        node.get_mut(1)
895            .unwrap()
896            .try_list_mut()
897            .unwrap()
898            .push(Node::atom("d"));
899        for item in &mut node[1] {
900            if item.as_atom() == Some("a") {
901                *item = Node::atom("z");
902            }
903        }
904        assert_eq!(node.to_string(), "{2,\r\n{z,\"c\",d}\r\n}");
905        // The edited value keeps the position of the original node.
906        assert_eq!(
907            node.get_path(&[1, 1]).unwrap().position().unwrap().column,
908            4
909        );
910        assert!(node.get_path_mut(&[0, 0]).is_none());
911        assert_eq!(node[0].iter_mut().count(), 0);
912
913        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])");
914        insta::assert_snapshot!(node[0].try_list_mut().unwrap_err(), @"expected list, found value `2`");
915    }
916
917    #[test]
918    #[should_panic(expected = "cannot index node: index 5 is out of bounds for list of length 2")]
919    fn index_mut_panics_with_message() {
920        let mut node = Node::list([Node::from(1), Node::from(2)]);
921        node[5] = Node::from(3);
922    }
923
924    #[test]
925    fn counted_lists() {
926        let node: Node = "{2,a,\r\n{b}}".parse().unwrap();
927        assert_eq!(
928            node.try_counted().unwrap(),
929            [Node::atom("a"), Node::list([Node::atom("b")])]
930        );
931        assert_eq!(Node::counted(node.try_counted().unwrap().to_vec()), node);
932        assert_eq!(Node::counted([]).to_string(), "{0}");
933
934        let bad: Node = "{\r\n{x},\r\n{3,a},\r\n{}}".parse().unwrap();
935        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");
936        insta::assert_snapshot!(bad[1].try_counted().unwrap_err(), @"list declares 3 items, but has 1 at line 3, column 1");
937        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");
938    }
939
940    #[test]
941    fn debug_matches_derived_layout() {
942        let node = Node::list([
943            Node::atom("1"),
944            Node::list([]),
945            Node::list([Node::string("a \"b\"")]),
946        ]);
947        assert_eq!(
948            format!("{node:?}"),
949            r#"List([Atom("1"), List([]), List([Str("a \"b\"")])])"#
950        );
951        insta::assert_snapshot!(format!("{node:#?}"), @r#"
952        List(
953            [
954                Atom(
955                    "1",
956                ),
957                List(
958                    [],
959                ),
960                List(
961                    [
962                        Str(
963                            "a \"b\"",
964                        ),
965                    ],
966                ),
967            ],
968        )
969        "#);
970    }
971
972    #[test]
973    fn deep_trees_without_recursion() {
974        let depth = 100_000;
975        let deep = || {
976            (0..depth).fold(Node::string("leaf"), |inner, _| {
977                Node::list([Node::from(1), inner])
978            })
979        };
980        let node = deep();
981        let copy = node.clone();
982        assert_eq!(copy, node);
983        assert_ne!(copy, Node::list([]));
984
985        let hash = |node: &Node| {
986            let mut hasher = std::hash::DefaultHasher::new();
987            node.hash(&mut hasher);
988            hasher.finish()
989        };
990        assert_eq!(hash(&copy), hash(&deep()));
991
992        let debug = format!("{node:?}");
993        assert!(debug.starts_with("List([Atom(\"1\"), List(["));
994
995        assert_eq!(node.into_list().map(|items| items.len()), Some(2));
996        drop(copy);
997    }
998
999    #[test]
1000    fn typed_accessors_trim_atoms() {
1001        let node: Node = "{12\u{a0}\r\n,1 ,c2f34349-b1e0-4060-8abf-3c59f38ac69f\r\n}"
1002            .parse()
1003            .unwrap();
1004        assert_eq!(node[0].as_atom(), Some("12\u{a0}\r\n"));
1005        assert_eq!(node[0].as_atom_trimmed(), Some("12"));
1006        assert_eq!(node[0].try_parse::<u8>(), Ok(12));
1007        assert_eq!(node[1].as_bool(), Some(true));
1008        assert!(node[2].as_guid().is_some());
1009    }
1010
1011    #[test]
1012    fn unpaired_surrogates() {
1013        let node = Node::string(Text::from_utf16(&[0x61, 0xD800]));
1014        assert_eq!(node.as_str(), None);
1015        assert_eq!(node.to_string_lossy().as_deref(), Some("a\u{fffd}"));
1016        insta::assert_snapshot!(node.try_str().unwrap_err(), @r#"expected string or value, found string "a�": it holds an unpaired surrogate"#);
1017        assert_eq!(node.clone().into_string().as_deref(), Some("a\u{fffd}"));
1018        assert_eq!(node.into_text(), Some(Text::from_utf16(&[0x61, 0xD800])));
1019    }
1020
1021    #[test]
1022    fn detects_guid() {
1023        assert!(is_guid("c2f34349-b1e0-4060-8abf-3c59f38ac69f"));
1024        assert!(!is_guid("c2f34349-b1e0-4060-8abf-3c59f38ac69"));
1025    }
1026}