1use 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
13pub struct Node {
23 value: Value,
24 position: Option<Position>,
25}
26
27#[derive(Clone, PartialEq, Eq, Hash)]
29pub enum Value {
30 List(Vec<Node>),
32 Str(Text),
34 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 pub fn list(items: impl IntoIterator<Item = Node>) -> Self {
51 Value::List(items.into_iter().collect()).into()
52 }
53
54 pub fn atom(value: impl Into<Text>) -> Self {
56 Value::Atom(value.into()).into()
57 }
58
59 pub fn string(value: impl Into<Text>) -> Self {
63 Value::Str(value.into()).into()
64 }
65
66 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 pub fn value_mut(&mut self) -> &mut Value {
82 &mut self.value
83 }
84
85 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 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 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 pub fn as_str(&self) -> Option<&str> {
126 self.as_text()?.as_str()
127 }
128
129 pub fn to_string_lossy(&self) -> Option<Cow<'_, str>> {
131 Some(self.as_text()?.to_string_lossy())
132 }
133
134 pub fn as_quoted(&self) -> Option<&str> {
136 match &self.value {
137 Value::Str(s) => s.as_str(),
138 _ => None,
139 }
140 }
141
142 pub fn as_atom(&self) -> Option<&str> {
144 match &self.value {
145 Value::Atom(s) => s.as_str(),
146 _ => None,
147 }
148 }
149
150 pub fn as_atom_trimmed(&self) -> Option<&str> {
154 self.as_atom().map(trim)
155 }
156
157 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 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 pub fn as_guid(&self) -> Option<&str> {
185 self.as_atom_trimmed().filter(|s| is_guid(s))
186 }
187
188 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 pub fn get(&self, idx: usize) -> Option<&Node> {
196 self.as_list()?.get(idx)
197 }
198
199 pub fn get_path(&self, path: &[usize]) -> Option<&Node> {
201 path.iter().try_fold(self, |node, &idx| node.get(idx))
202 }
203
204 pub fn len(&self) -> usize {
206 self.as_list().map_or(0, <[Node]>::len)
207 }
208
209 pub fn is_empty(&self) -> bool {
211 self.len() == 0
212 }
213
214 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 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 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 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 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 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 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 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 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 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 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 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 pub fn walk<'a>(&'a self, mut visit: impl FnMut(&[usize], &'a Node)) {
429 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 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 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
482pub(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
492fn trim(s: &str) -> &str {
494 s.trim_matches(is_space)
495}
496
497pub(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 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
531 self.value.fmt(f)
532 }
533}
534
535impl fmt::Debug for Value {
536 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 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 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 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 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 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 fn drop(&mut self) {
657 let Value::List(items) = &mut self.value else {
658 return;
659 };
660 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 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 (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 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 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(©), 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}