Skip to main content

cirru_edn/
lib.rs

1//! # Cirru EDN
2//!
3//! Extensible Data Notation (EDN) implementation using Cirru syntax.
4//!
5//! This crate provides a data format similar to EDN but using Cirru's syntax instead of
6//! traditional s-expressions. It supports rich data types including primitives, collections,
7//! and special constructs like records and tuples.
8//!
9//! ## Features
10//!
11//! - **Rich data types**: nil, boolean, number, string, symbol, tag, list, set, map, record, tuple, buffer, atom
12//! - **Serde integration**: Seamless serialization/deserialization with Rust structs
13//! - **Efficient string handling**: Uses `Arc<str>` for string deduplication
14//! - **Runtime references**: Support for arbitrary Rust data via `AnyRef`
15//! - **Type-safe API**: Strong typing with convenient conversion methods
16//!
17//! ## Basic Usage
18//!
19//! ```rust
20//! use cirru_edn::{parse, format, Edn};
21//!
22//! // Parse Cirru EDN from string
23//! let data = parse("[] 1 2 3").unwrap();
24//!
25//! // Create EDN values programmatically
26//! let map = Edn::map_from_iter([
27//!     (Edn::tag("name"), Edn::str("Alice")),
28//!     (Edn::tag("age"), Edn::Number(30.0)),
29//! ]);
30//!
31//! // Format back to string
32//! let formatted = format(&map, true).unwrap();
33//! ```
34//!
35//! ## Type Checking and Conversion
36//!
37//! The library provides type-safe methods for checking and converting values:
38//!
39//! ```rust
40//! use cirru_edn::Edn;
41//!
42//! let value = Edn::Number(42.0);
43//!
44//! // Type checking
45//! assert!(value.is_number());
46//! assert!(!value.is_string());
47//!
48//! // Safe conversion
49//! let number: f64 = value.read_number().unwrap();
50//! assert_eq!(number, 42.0);
51//!
52//! // Get type name for debugging
53//! assert_eq!(value.type_name(), "number");
54//! ```
55//!
56//! ## Working with Collections
57//!
58//! ```rust
59//! use cirru_edn::Edn;
60//!
61//! // Create and access lists
62//! let list = Edn::List(vec![
63//!     Edn::Number(1.0),
64//!     Edn::str("hello"),
65//!     Edn::Bool(true)
66//! ].into());
67//!
68//! if let Some(first) = list.get_list_item(0) {
69//!     assert_eq!(first.read_number().unwrap(), 1.0);
70//! }
71//!
72//! // Create and access maps
73//! let map = Edn::map_from_iter([
74//!     (Edn::tag("name"), Edn::str("Bob")),
75//!     (Edn::tag("age"), Edn::Number(25.0)),
76//! ]);
77//!
78//! if let Some(name) = map.get_map_value(&Edn::tag("name")) {
79//!     assert_eq!(name.read_string().unwrap(), "Bob");
80//! }
81//! ```
82//!
83//! ## Serde Integration
84//!
85//! The crate includes built-in serde support for seamless serialization:
86//!
87//! ```rust
88//! use cirru_edn::{to_edn, from_edn};
89//! use serde::{Serialize, Deserialize};
90//!
91//! #[derive(Serialize, Deserialize)]
92//! struct Person {
93//!     name: String,
94//!     age: u32,
95//! }
96//!
97//! let person = Person { name: "Bob".to_string(), age: 25 };
98//! let edn_value = to_edn(&person).unwrap();
99//! let recovered: Person = from_edn(edn_value).unwrap();
100//! ```
101
102mod edn;
103mod error;
104mod tag;
105
106pub mod serde_support;
107
108use std::cmp::Ordering::*;
109use std::collections::{HashMap, HashSet};
110use std::iter::FromIterator;
111use std::sync::Arc;
112use std::vec;
113
114use cirru_parser::Cirru;
115
116pub use edn::{
117  DynEq, Edn, EdnAnyRef, EdnListView, EdnMapView, EdnRecordView, EdnSetView, EdnTupleView, is_simple_char,
118};
119pub use error::{EdnError, EdnResult, Position};
120pub use tag::EdnTag;
121
122/// Returns the version of the crate.
123pub fn version() -> &'static str {
124  env!("CARGO_PKG_VERSION")
125}
126
127// Backward compatible type alias
128#[deprecated(since = "0.7.0", note = "Use EdnError instead")]
129pub type EdnResultString<T> = Result<T, String>;
130
131// Convenience type aliases for common patterns
132pub type EdnList = EdnListView;
133pub type EdnMap = EdnMapView;
134pub type EdnSet = EdnSetView;
135pub type EdnRecord = EdnRecordView;
136pub type EdnTuple = EdnTupleView;
137
138// Common constants for convenience
139impl Edn {
140  /// Predefined nil constant for convenience
141  pub const NIL: Edn = Edn::Nil;
142
143  /// Predefined true constant for convenience
144  pub const TRUE: Edn = Edn::Bool(true);
145
146  /// Predefined false constant for convenience
147  pub const FALSE: Edn = Edn::Bool(false);
148
149  /// Convert an EDN value into a Cirru AST node.
150  pub fn cirru(&self) -> Cirru {
151    assemble_cirru_node(self)
152  }
153}
154
155pub use serde_support::{from_edn, to_edn};
156
157/// Parse Cirru code into Edn data.
158///
159/// This function takes a string containing Cirru syntax and converts it into an Edn value.
160/// The input must contain exactly one expression.
161///
162/// # Arguments
163///
164/// * `s` - A string slice containing the Cirru code to parse
165///
166/// # Returns
167///
168/// * `Result<Edn, String>` - Returns the parsed Edn value on success, or an error message on failure
169///
170/// # Examples
171///
172/// ```
173/// use cirru_edn::parse;
174///
175/// // Parse a simple number
176/// let result = parse("do 42").unwrap();
177///
178/// // Parse a list
179/// let result = parse("[] 1 2 3").unwrap();
180///
181/// // Parse a map
182/// let result = parse("{} (:name |Alice) (:age 30)").unwrap();
183///
184/// // Parse nested structures
185/// let result = parse("{} (:items $ [] 1 2 3) (:meta $ {} (:version 1))").unwrap();
186/// ```
187///
188/// # Errors
189///
190/// Returns an error if:
191/// - The input contains no expressions or more than one expression
192/// - The syntax is invalid
193/// - The input contains unsupported constructs
194pub fn parse(s: &str) -> EdnResult<Edn> {
195  let xs = cirru_parser::parse(s).map_err(|e| EdnError::from_parse_error_detailed(e, s))?;
196  if xs.len() == 1 {
197    match &xs[0] {
198      Cirru::Leaf(s) => Err(EdnError::structure(
199        format!("expected expr for data, got leaf: {s}"),
200        vec![],
201        Some(&xs[0]),
202      )),
203      Cirru::List(_) => extract_cirru_edn(&xs[0]),
204    }
205  } else {
206    Err(EdnError::structure(
207      format!("Expected 1 expr for edn, got length {}: {:?} ", xs.len(), xs),
208      vec![],
209      None,
210    ))
211  }
212}
213
214/// Extract EDN value from a Cirru AST node.
215///
216/// This is useful when caller already has parsed Cirru tree and wants
217/// to decode one expression without going through string parsing again.
218pub fn extract_cirru_edn(node: &Cirru) -> EdnResult<Edn> {
219  extract_cirru_edn_with_path(node, vec![])
220}
221
222fn extract_cirru_edn_with_path(node: &Cirru, path: Vec<usize>) -> EdnResult<Edn> {
223  match node {
224    Cirru::Leaf(s) => match &**s {
225      "nil" => Ok(Edn::Nil),
226      "true" => Ok(Edn::Bool(true)),
227      "false" => Ok(Edn::Bool(false)),
228      "" => Err(EdnError::value(
229        "empty string is invalid for edn",
230        path.clone(),
231        Some(node),
232      )),
233      s1 => match s1.chars().next().unwrap() {
234        '\'' => Ok(Edn::Symbol(s1[1..].into())),
235        ':' => Ok(Edn::tag(&s1[1..])),
236        '"' | '|' => Ok(Edn::Str(s1[1..].into())),
237        _ => {
238          if let Ok(f) = s1.trim().parse::<f64>() {
239            Ok(Edn::Number(f))
240          } else {
241            Err(EdnError::value(
242              format!("unknown token for edn value: {s1:?}"),
243              path.clone(),
244              Some(node),
245            ))
246          }
247        }
248      },
249    },
250    Cirru::List(xs) => {
251      if xs.is_empty() {
252        Err(EdnError::structure(
253          "empty expr is invalid for edn",
254          path.clone(),
255          Some(node),
256        ))
257      } else {
258        match &xs[0] {
259          Cirru::Leaf(s) => match &**s {
260            "quote" => {
261              if xs.len() == 2 {
262                Ok(Edn::Quote(xs[1].to_owned()))
263              } else {
264                Err(EdnError::structure("missing edn quote value", path.clone(), Some(node)))
265              }
266            }
267            "do" => {
268              let mut ret: Option<Edn> = None;
269
270              for (i, x) in xs.iter().enumerate().skip(1) {
271                if is_comment(x) {
272                  continue;
273                }
274                if ret.is_some() {
275                  return Err(EdnError::structure("multiple values in do", path.clone(), Some(node)));
276                }
277                let mut child_path = path.clone();
278                child_path.push(i);
279                ret = Some(extract_cirru_edn_with_path(x, child_path)?);
280              }
281              if ret.is_none() {
282                return Err(EdnError::structure("missing edn do value", path.clone(), Some(node)));
283              }
284              ret.ok_or_else(|| EdnError::structure("missing edn do value", path.clone(), Some(node)))
285            }
286            "::" => {
287              let mut tag: Option<Edn> = None;
288              let mut extra: Vec<Edn> = vec![];
289              for (i, x) in xs.iter().enumerate().skip(1) {
290                if is_comment(x) {
291                  continue;
292                }
293                let mut child_path = path.clone();
294                child_path.push(i);
295                if tag.is_some() {
296                  extra.push(extract_cirru_edn_with_path(x, child_path)?);
297                  continue;
298                } else {
299                  tag = Some(extract_cirru_edn_with_path(x, child_path)?);
300                }
301              }
302              if let Some(x0) = tag {
303                Ok(Edn::Tuple(EdnTupleView {
304                  tag: Arc::new(x0),
305                  enum_tag: None,
306                  extra,
307                }))
308              } else {
309                Err(EdnError::structure(
310                  "missing edn :: fst value",
311                  path.clone(),
312                  Some(node),
313                ))
314              }
315            }
316            "%::" => {
317              let mut enum_tag: Option<Edn> = None;
318              let mut tag: Option<Edn> = None;
319              let mut extra: Vec<Edn> = vec![];
320              for (i, x) in xs.iter().enumerate().skip(1) {
321                if is_comment(x) {
322                  continue;
323                }
324                let mut child_path = path.clone();
325                child_path.push(i);
326                if enum_tag.is_none() {
327                  enum_tag = Some(extract_cirru_edn_with_path(x, child_path)?);
328                } else if tag.is_none() {
329                  tag = Some(extract_cirru_edn_with_path(x, child_path)?);
330                } else {
331                  extra.push(extract_cirru_edn_with_path(x, child_path)?);
332                }
333              }
334              if let (Some(e0), Some(x0)) = (enum_tag, tag) {
335                Ok(Edn::Tuple(EdnTupleView {
336                  tag: Arc::new(x0),
337                  enum_tag: Some(Arc::new(e0)),
338                  extra,
339                }))
340              } else {
341                Err(EdnError::structure(
342                  "missing edn %:: enum_tag or tag value",
343                  path.clone(),
344                  Some(node),
345                ))
346              }
347            }
348            "[]" => {
349              let mut ys: Vec<Edn> = Vec::with_capacity(xs.len() - 1);
350              for (i, x) in xs.iter().enumerate().skip(1) {
351                if is_comment(x) {
352                  continue;
353                }
354                let mut child_path = path.clone();
355                child_path.push(i);
356                match extract_cirru_edn_with_path(x, child_path) {
357                  Ok(v) => ys.push(v),
358                  Err(v) => return Err(v),
359                }
360              }
361              Ok(Edn::List(EdnListView(ys)))
362            }
363            "#{}" => {
364              #[allow(clippy::mutable_key_type)]
365              let mut ys: HashSet<Edn> = HashSet::new();
366              for (i, x) in xs.iter().enumerate().skip(1) {
367                if is_comment(x) {
368                  continue;
369                }
370                let mut child_path = path.clone();
371                child_path.push(i);
372                match extract_cirru_edn_with_path(x, child_path) {
373                  Ok(v) => {
374                    ys.insert(v);
375                  }
376                  Err(v) => return Err(v),
377                }
378              }
379              Ok(Edn::Set(EdnSetView(ys)))
380            }
381            "{}" => {
382              #[allow(clippy::mutable_key_type)]
383              let mut zs: HashMap<Edn, Edn> = HashMap::new();
384              for (i, x) in xs.iter().enumerate().skip(1) {
385                if is_comment(x) {
386                  continue;
387                }
388                let mut child_path = path.clone();
389                child_path.push(i);
390                match x {
391                  Cirru::Leaf(s) => {
392                    return Err(EdnError::structure(
393                      format!("expected a pair, invalid map entry: {s}"),
394                      child_path,
395                      Some(node),
396                    ));
397                  }
398                  Cirru::List(ys) => {
399                    if ys.len() == 2 {
400                      let mut k_path = child_path.clone();
401                      k_path.push(0);
402                      let mut v_path = child_path.clone();
403                      v_path.push(1);
404                      match (
405                        extract_cirru_edn_with_path(&ys[0], k_path.clone()),
406                        extract_cirru_edn_with_path(&ys[1], v_path.clone()),
407                      ) {
408                        (Ok(k), Ok(v)) => {
409                          zs.insert(k, v);
410                        }
411                        (Err(e), _) => {
412                          return Err(EdnError::structure(
413                            format!("invalid map entry `{}` from `{}`", e, &ys[0]),
414                            k_path,
415                            Some(node),
416                          ));
417                        }
418                        (Ok(k), Err(e)) => {
419                          return Err(EdnError::structure(
420                            format!("invalid map entry for `{k}`, got {e}"),
421                            v_path,
422                            Some(node),
423                          ));
424                        }
425                      }
426                    }
427                  }
428                }
429              }
430              Ok(Edn::Map(EdnMapView(zs)))
431            }
432            "%{}" => {
433              if xs.len() >= 3 {
434                let name = match &xs[1] {
435                  Cirru::Leaf(s) => EdnTag::new(s.strip_prefix(':').unwrap_or(s)),
436                  Cirru::List(e) => {
437                    let mut name_path = path.clone();
438                    name_path.push(1);
439                    return Err(EdnError::structure(
440                      format!("expected record name in string: {e:?}"),
441                      name_path,
442                      Some(node),
443                    ));
444                  }
445                };
446                let mut entries: Vec<(EdnTag, Edn)> = Vec::with_capacity(xs.len() - 1);
447
448                for (i, x) in xs.iter().enumerate().skip(2) {
449                  if is_comment(x) {
450                    continue;
451                  }
452                  let mut child_path = path.clone();
453                  child_path.push(i);
454                  match x {
455                    Cirru::Leaf(s) => {
456                      return Err(EdnError::structure(
457                        format!("expected record, invalid record entry: {s}"),
458                        child_path,
459                        Some(node),
460                      ));
461                    }
462                    Cirru::List(ys) => {
463                      if ys.len() == 2 {
464                        let mut v_path = child_path.clone();
465                        v_path.push(1);
466                        match (&ys[0], extract_cirru_edn_with_path(&ys[1], v_path.clone())) {
467                          (Cirru::Leaf(s), Ok(v)) => {
468                            entries.push((EdnTag::new(s.strip_prefix(':').unwrap_or(s)), v));
469                          }
470                          (Cirru::Leaf(s), Err(e)) => {
471                            return Err(EdnError::structure(
472                              format!("invalid record value for `{s}`, got: {e}"),
473                              v_path,
474                              Some(node),
475                            ));
476                          }
477                          (Cirru::List(zs), _) => {
478                            let mut k_path = child_path.clone();
479                            k_path.push(0);
480                            return Err(EdnError::structure(
481                              format!("invalid list as record key: {zs:?}"),
482                              k_path,
483                              Some(node),
484                            ));
485                          }
486                        }
487                      } else {
488                        return Err(EdnError::structure(
489                          format!("expected pair of 2: {ys:?}"),
490                          child_path,
491                          Some(node),
492                        ));
493                      }
494                    }
495                  }
496                }
497                if entries.is_empty() {
498                  return Err(EdnError::structure("empty record is invalid", path.clone(), Some(node)));
499                }
500                Ok(Edn::Record(EdnRecordView {
501                  tag: name,
502                  pairs: entries,
503                }))
504              } else {
505                Err(EdnError::structure(
506                  "insufficient items for edn record",
507                  path.clone(),
508                  Some(node),
509                ))
510              }
511            }
512            "buf" => {
513              let mut ys: Vec<u8> = Vec::with_capacity(xs.len() - 1);
514              for (i, x) in xs.iter().enumerate().skip(1) {
515                if is_comment(x) {
516                  continue;
517                }
518                let mut child_path = path.clone();
519                child_path.push(i);
520                match x {
521                  Cirru::Leaf(y) => {
522                    if y.len() == 2 {
523                      match hex::decode(&(**y)) {
524                        Ok(b) => {
525                          if b.len() == 1 {
526                            ys.push(b[0])
527                          } else {
528                            return Err(EdnError::value(
529                              format!("hex for buffer might be too large, got: {b:?}"),
530                              child_path,
531                              Some(node),
532                            ));
533                          }
534                        }
535                        Err(e) => {
536                          return Err(EdnError::value(
537                            format!("expected length 2 hex string in buffer, got: {y} {e}"),
538                            child_path,
539                            Some(node),
540                          ));
541                        }
542                      }
543                    } else {
544                      return Err(EdnError::value(
545                        format!("expected length 2 hex string in buffer, got: {y}"),
546                        child_path,
547                        Some(node),
548                      ));
549                    }
550                  }
551                  _ => {
552                    return Err(EdnError::value(
553                      format!("expected hex string in buffer, got: {x}"),
554                      child_path,
555                      Some(node),
556                    ));
557                  }
558                }
559              }
560              Ok(Edn::Buffer(ys))
561            }
562            "atom" => {
563              if xs.len() == 2 {
564                let mut child_path = path.clone();
565                child_path.push(1);
566                Ok(Edn::Atom(Box::new(extract_cirru_edn_with_path(&xs[1], child_path)?)))
567              } else {
568                Err(EdnError::structure("missing edn atom value", path.clone(), Some(node)))
569              }
570            }
571            a => Err(EdnError::structure(
572              format!("invalid operator for edn: {a}"),
573              path.clone(),
574              Some(node),
575            )),
576          },
577          Cirru::List(a) => Err(EdnError::structure(
578            format!("invalid nodes for edn: {a:?}"),
579            path.clone(),
580            Some(node),
581          )),
582        }
583      }
584    }
585  }
586}
587
588fn is_comment(node: &Cirru) -> bool {
589  match node {
590    Cirru::Leaf(_) => false,
591    Cirru::List(xs) => xs.first() == Some(&Cirru::Leaf(";".into())),
592  }
593}
594
595fn assemble_cirru_node(data: &Edn) -> Cirru {
596  match data {
597    Edn::Nil => "nil".into(),
598    Edn::Bool(v) => v.to_string().as_str().into(),
599    Edn::Number(n) => n.to_string().as_str().into(),
600    Edn::Symbol(s) => format!("'{s}").as_str().into(),
601    Edn::Tag(s) => format!(":{s}").as_str().into(),
602    Edn::Str(s) => format!("|{s}").as_str().into(),
603    Edn::Quote(v) => Cirru::List(vec!["quote".into(), (*v).to_owned()]),
604    Edn::List(xs) => {
605      let mut ys: Vec<Cirru> = Vec::with_capacity(xs.len() + 1);
606      ys.push("[]".into());
607      for x in xs {
608        ys.push(assemble_cirru_node(x));
609      }
610      Cirru::List(ys)
611    }
612    Edn::Set(xs) => {
613      let mut ys: Vec<Cirru> = Vec::with_capacity(xs.len() + 1);
614      ys.push("#{}".into());
615      let mut items = xs.0.iter().collect::<Vec<_>>();
616      items.sort();
617      for x in items {
618        ys.push(assemble_cirru_node(x));
619      }
620      Cirru::List(ys)
621    }
622    Edn::Map(xs) => {
623      let mut ys: Vec<Cirru> = Vec::with_capacity(xs.len() + 1);
624      ys.push("{}".into());
625      let mut items = Vec::from_iter(xs.0.iter());
626      items.sort_by(|(a1, a2): &(&Edn, &Edn), (b1, b2): &(&Edn, &Edn)| {
627        match (a1.is_literal(), b1.is_literal(), a2.is_literal(), b2.is_literal()) {
628          (true, true, true, false) => Less,
629          (true, true, false, true) => Greater,
630          (true, false, ..) => Less,
631          (false, true, ..) => Greater,
632          _ => a1.cmp(b1),
633        }
634      });
635      for (k, v) in items {
636        ys.push(Cirru::List(vec![assemble_cirru_node(k), assemble_cirru_node(v)]))
637      }
638      Cirru::List(ys)
639    }
640    Edn::Record(EdnRecordView {
641      tag: name,
642      pairs: entries,
643    }) => {
644      let mut ys: Vec<Cirru> = Vec::with_capacity(entries.len() + 2);
645      ys.push("%{}".into());
646      ys.push(format!(":{name}").as_str().into());
647      let mut ordered_entries = entries.to_owned();
648      ordered_entries.sort_by(|(a1, a2), (b1, b2)| match (a2.is_literal(), b2.is_literal()) {
649        (true, false) => Less,
650        (false, true) => Greater,
651        _ => a1.cmp(b1),
652      });
653      for entry in ordered_entries {
654        let v = &entry.1;
655        ys.push(Cirru::List(vec![
656          format!(":{}", entry.0).as_str().into(),
657          assemble_cirru_node(v),
658        ]));
659      }
660
661      Cirru::List(ys)
662    }
663    Edn::Tuple(EdnTupleView { tag, enum_tag, extra }) => {
664      let mut ys: Vec<Cirru> = if let Some(et) = enum_tag {
665        vec!["%::".into(), assemble_cirru_node(et), assemble_cirru_node(tag)]
666      } else {
667        vec!["::".into(), assemble_cirru_node(tag)]
668      };
669      for item in extra {
670        ys.push(assemble_cirru_node(item))
671      }
672      Cirru::List(ys)
673    }
674    Edn::Buffer(buf) => {
675      let mut ys: Vec<Cirru> = Vec::with_capacity(buf.len() + 1);
676      ys.push("buf".into());
677      for b in buf {
678        ys.push(hex::encode(vec![b.to_owned()]).as_str().into());
679      }
680      Cirru::List(ys)
681    }
682    Edn::AnyRef(..) => unreachable!("AnyRef is not serializable"),
683    Edn::Atom(v) => {
684      let ys = vec!["atom".into(), assemble_cirru_node(v)];
685      Cirru::List(ys)
686    }
687  }
688}
689
690/// Generate formatted string from Edn data.
691///
692/// This function converts an Edn value into its Cirru syntax representation.
693///
694/// # Arguments
695///
696/// * `data` - The Edn value to format
697/// * `use_inline` - Whether to use inline formatting (more compact) or multiline formatting
698///
699/// # Returns
700///
701/// * `Result<String, String>` - Returns the formatted string on success, or an error message on failure
702///
703/// # Examples
704///
705/// ```
706/// use cirru_edn::{Edn, format};
707///
708/// let data = Edn::Number(42.0);
709/// let result = format(&data, true).unwrap();
710/// assert_eq!(result.trim(), "do 42");
711///
712/// // Format a list with inline style
713/// let data = Edn::List(vec![
714///     Edn::Number(1.0),
715///     Edn::Number(2.0),
716///     Edn::Number(3.0),
717/// ].into());
718/// let result = format(&data, true).unwrap();
719/// // Output: ([] 1 2 3)
720///
721/// // Format a map with multiline style
722/// let data = Edn::map_from_iter([
723///     (Edn::tag("name"), Edn::str("Alice")),
724///     (Edn::tag("age"), Edn::Number(30.0)),
725/// ]);
726/// let result = format(&data, false).unwrap();
727/// ```
728///
729/// # Notes
730///
731/// - AnyRef values cannot be formatted and will cause an error
732/// - The function automatically wraps single literals in `do` expressions
733/// - Inline formatting produces more compact output, while multiline formatting is more readable
734pub fn format(data: &Edn, use_inline: bool) -> Result<String, String> {
735  match data.cirru() {
736    Cirru::Leaf(s) => cirru_parser::format(&[vec!["do", &*s].into()], use_inline.into()),
737    Cirru::List(xs) => cirru_parser::format(&[(Cirru::List(xs))], use_inline.into()),
738  }
739}
740
741#[cfg(test)]
742mod tests {
743  use super::*;
744
745  #[test]
746  fn exposes_extract_api_for_cirru_ast() {
747    let node = Cirru::List(vec![Cirru::leaf("[]"), Cirru::leaf("1"), Cirru::leaf("2")]);
748    let value = extract_cirru_edn(&node).expect("should decode list from AST");
749    assert!(matches!(value, Edn::List(_)));
750  }
751
752  #[test]
753  fn exposes_short_cirru_method_on_edn() {
754    let value = Edn::map_from_iter([(Edn::tag("a"), Edn::Number(1.0))]);
755    let node = value.cirru();
756    let Cirru::List(items) = node else {
757      panic!("map should assemble into list node");
758    };
759    assert_eq!(items.first(), Some(&Cirru::leaf("{}")));
760  }
761}