Skip to main content

clojure_reader/
edn.rs

1//! An EDN reader/presenter in Rust.
2//!
3//! ## Implementations
4//! -  [`core::fmt::Display`] will output valid EDN for any Edn object. Alternate formatting
5//!    (`{edn:#}`) outputs indented EDN, with deeply nested subtrees falling back to compact
6//!    formatting to bound indentation overhead.
7//! -  With the `unstable` feature enabled, [`TryFrom`]<[`parse::Node`]> implemented for [`Edn`]
8//!    will convert the Node into an Edn
9//!
10//! ## Differences from Clojure
11//! -  Escape characters are not escaped.
12
13use alloc::boxed::Box;
14use alloc::collections::{BTreeMap, BTreeSet};
15use alloc::vec::Vec;
16use core::fmt;
17
18#[cfg(feature = "arbitrary-nums")]
19use bigdecimal::BigDecimal;
20#[cfg(feature = "arbitrary-nums")]
21use num_bigint::BigInt;
22#[cfg(feature = "floats")]
23use ordered_float::OrderedFloat;
24
25use crate::{error, parse};
26
27#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, Hash)]
28#[non_exhaustive]
29pub enum Edn<'e> {
30	Vector(Vec<Self>),
31	Set(BTreeSet<Self>),
32	Map(BTreeMap<Self, Self>),
33	List(Vec<Self>),
34	Key(&'e str),
35	Symbol(&'e str),
36	Str(&'e str),
37	Int(i64),
38	Tagged(&'e str, Box<Self>),
39	#[cfg(feature = "floats")]
40	Double(OrderedFloat<f64>),
41	Rational((i64, i64)),
42	#[cfg(feature = "arbitrary-nums")]
43	BigInt(BigInt),
44	#[cfg(feature = "arbitrary-nums")]
45	BigDec(BigDecimal),
46	Char(char),
47	Bool(bool),
48	Nil,
49}
50
51const SYMBOL_SPECIAL_CHARS: &str = ".*+!-_?$%&=<>:#";
52pub(crate) const MAX_PRETTY_DEPTH: usize = 42;
53
54fn is_symbol_char(c: char) -> bool {
55	c.is_alphanumeric() || SYMBOL_SPECIAL_CHARS.contains(c)
56}
57
58fn is_symbol_start(c: char) -> bool {
59	!c.is_numeric() && !matches!(c, ':' | '#') && is_symbol_char(c)
60}
61
62fn valid_symbol_part(part: &str) -> bool {
63	let mut chars = part.chars();
64	let Some(first) = chars.next() else { return false };
65	let second = chars.clone().next();
66
67	is_symbol_start(first)
68		&& !(matches!(first, '-' | '+' | '.') && second.is_some_and(char::is_numeric))
69		&& chars.all(is_symbol_char)
70}
71
72pub(crate) fn validate_tag(tag: &str, tag_span: parse::Span) -> error::Result<()> {
73	let tag = tag.strip_prefix(':').unwrap_or(tag);
74	let valid = tag.chars().next().is_some_and(char::is_alphabetic)
75		&& match tag.split_once('/') {
76			Some((prefix, name)) => {
77				!name.contains('/') && valid_symbol_part(prefix) && valid_symbol_part(name)
78			}
79			None => valid_symbol_part(tag),
80		};
81
82	if valid { Ok(()) } else { Err(error::Error::from_position(error::Code::InvalidTag, tag_span.0)) }
83}
84
85impl<'e> TryFrom<parse::Node<'e>> for Edn<'e> {
86	type Error = error::Error;
87	/// Elaborates a concrete [`Node`](parse::Node) into an abstract resolved [`Edn`]
88	///
89	/// ```
90	/// #[cfg(feature = "unstable")]
91	/// {
92	///   use clojure_reader::{parse, edn::Edn};
93	///
94	///   let edn: Edn = parse::Node::no_discards(parse::NodeKind::Nil, parse::Span::default())
95	///     .try_into()
96	///     .unwrap();
97	///
98	///   assert_eq!(edn, Edn::Nil);
99	/// }
100	/// ```
101	///
102	/// # Errors
103	///
104	/// See [`crate::error::Error`].
105	///
106	/// [HMDK]: error::Code::HashMapDuplicateKey
107	/// [SDK]: error::Code::SetDuplicateKey
108	/// [IT]: error::Code::InvalidTag
109	fn try_from(parse::Node { kind: value, .. }: parse::Node<'e>) -> error::Result<Self> {
110		use error::{Code, Error, Result};
111		use parse::NodeKind;
112
113		Ok(match value {
114			NodeKind::Vector(items, _) => {
115				Edn::Vector(items.into_iter().map(TryInto::try_into).collect::<Result<_>>()?)
116			}
117			NodeKind::Set(items, _) => {
118				let mut set = BTreeSet::new();
119				for node in items {
120					let position = node.span().1;
121					if !set.insert(node.try_into()?) {
122						return Err(Error::from_position(Code::SetDuplicateKey, position));
123					}
124				}
125				Edn::Set(set)
126			}
127			NodeKind::Map(entries, _) => {
128				let mut map = BTreeMap::new();
129				for (key, value) in entries {
130					let position = value.span().1;
131					if map.insert(key.try_into()?, value.try_into()?).is_some() {
132						return Err(Error::from_position(Code::HashMapDuplicateKey, position));
133					}
134				}
135				Edn::Map(map)
136			}
137			NodeKind::List(items, _) => {
138				Edn::List(items.into_iter().map(TryInto::try_into).collect::<Result<_>>()?)
139			}
140			NodeKind::Key(key) => Edn::Key(key),
141			NodeKind::Symbol(symbol) => Edn::Symbol(symbol),
142			NodeKind::Str(str) => Edn::Str(str),
143			NodeKind::Int(int) => Edn::Int(int),
144			NodeKind::Tagged(tag, tag_span, node) => {
145				validate_tag(tag, tag_span)?;
146				if tag.starts_with(':') && !matches!(&node.kind, NodeKind::Map(..)) {
147					return Err(Error::from_position(Code::InvalidTag, tag_span.0));
148				}
149				Edn::Tagged(tag, Box::new((*node).try_into()?))
150			}
151			#[cfg(feature = "floats")]
152			NodeKind::Double(double) => Edn::Double(double),
153			NodeKind::Rational(rational) => Edn::Rational(rational),
154			#[cfg(feature = "arbitrary-nums")]
155			NodeKind::BigInt(big_int) => Edn::BigInt(big_int),
156			#[cfg(feature = "arbitrary-nums")]
157			NodeKind::BigDec(big_dec) => Edn::BigDec(big_dec),
158			NodeKind::Char(ch) => Edn::Char(ch),
159			NodeKind::Bool(bool) => Edn::Bool(bool),
160			NodeKind::Nil => Edn::Nil,
161		})
162	}
163}
164
165/// Reads one object from the &str.
166///
167/// # Errors
168///
169/// See [`crate::error::Error`].
170pub fn read_string(edn: &str) -> Result<Edn<'_>, error::Error> {
171	Ok(parse::parse_as_edn(edn)?.0)
172}
173
174/// Reads the first object from the &str and the remaining unread &str.
175///
176/// # Errors
177///
178/// Default behavior of Clojure's `read` is to throw an error on EOF, unlike `read_string`.
179/// <https://clojure.github.io/tools.reader/#clojure.tools.reader.edn/read>
180///
181/// See [`crate::error::Error`].
182pub fn read(edn: &str) -> Result<(Edn<'_>, &str), error::Error> {
183	let (edn, remaining) = parse::parse_optional_edn(edn)?;
184	let Some(edn) = edn else {
185		return Err(error::Error {
186			code: error::Code::UnexpectedEOF,
187			line: None,
188			column: None,
189			ptr: None,
190		});
191	};
192	Ok((edn, remaining))
193}
194
195fn get_tag<'a>(tag: &'a str, key: &'a str) -> Option<&'a str> {
196	// Break out early if there's no namespaces
197	if !key.contains('/') {
198		return None;
199	}
200
201	// ignore the leading ':'
202	if !tag.starts_with(':') {
203		return None;
204	}
205	let tag = tag.get(1..)?;
206	Some(tag)
207}
208
209fn check_key<'a>(tag: &'a str, key: &'a str) -> &'a str {
210	// check if the Key starts with the saved Tag
211	if key.starts_with(tag) {
212		let (_, key) = key.rsplit_once(tag).expect("Tag must exist, because it starts with it.");
213
214		// ensure there's a '/' and strip it
215		if let Some(k) = key.strip_prefix('/') {
216			return k;
217		}
218	}
219	key
220}
221
222impl Edn<'_> {
223	pub fn get(&self, e: &Self) -> Option<&Self> {
224		if let Edn::Map(m) = self {
225			return m.get(e);
226		} else if let Edn::Tagged(tag, m) = self {
227			if let Edn::Key(key) = e {
228				let tag = get_tag(tag, key)?;
229				let key = check_key(tag, key);
230
231				return m.get(&Edn::Key(key));
232			}
233
234			// Cover cases where it's not a keyword
235			return m.get(e);
236		}
237		None
238	}
239	pub fn nth(&self, i: usize) -> Option<&Self> {
240		let vec = match self {
241			Edn::Vector(v) => v,
242			Edn::List(l) => l,
243			_ => return None,
244		};
245
246		vec.get(i)
247	}
248
249	pub fn contains(&self, e: &Self) -> bool {
250		match self {
251			Edn::Map(m) => m.contains_key(e),
252			Edn::Tagged(tag, m) => {
253				if let Edn::Key(key) = e {
254					let Some(tag) = get_tag(tag, key) else { return false };
255					let key = check_key(tag, key);
256
257					return m.contains(&Edn::Key(key));
258				}
259
260				// Cover cases where it's not a keyword
261				m.contains(e)
262			}
263			Edn::Vector(v) => v.contains(e),
264			Edn::Set(s) => s.contains(e),
265			Edn::List(l) => l.contains(e),
266			_ => false,
267		}
268	}
269}
270
271pub(crate) const fn char_to_edn(c: char) -> Option<&'static str> {
272	match c {
273		'\n' => Some("newline"),
274		'\r' => Some("return"),
275		' ' => Some("space"),
276		'\t' => Some("tab"),
277		_ => None,
278	}
279}
280
281fn write_indent(f: &mut fmt::Formatter<'_>, depth: usize) -> fmt::Result {
282	for _ in 0..depth {
283		f.write_str("\t")?;
284	}
285	Ok(())
286}
287
288impl Edn<'_> {
289	fn fmt_edn(&self, f: &mut fmt::Formatter<'_>, pretty: bool, depth: usize) -> fmt::Result {
290		let pretty = pretty && depth < MAX_PRETTY_DEPTH;
291		match self {
292			Self::Vector(v) => Self::fmt_sequence(f, "[", "]", v, pretty, depth),
293			Self::Set(s) => Self::fmt_sequence(f, "#{", "}", s, pretty, depth),
294			Self::Map(m) => {
295				f.write_str("{")?;
296				let mut entries = m.iter().peekable();
297				while let Some((key, value)) = entries.next() {
298					if pretty {
299						f.write_str("\n")?;
300						write_indent(f, depth + 1)?;
301					}
302					key.fmt_edn(f, pretty, depth + 1)?;
303					f.write_str(" ")?;
304					value.fmt_edn(f, pretty, depth + 1)?;
305					if entries.peek().is_some() {
306						if pretty {
307							f.write_str(",")?;
308						} else {
309							f.write_str(", ")?;
310						}
311					}
312				}
313				if pretty && !m.is_empty() {
314					f.write_str("\n")?;
315					write_indent(f, depth)?;
316				}
317				f.write_str("}")
318			}
319			Self::List(l) => Self::fmt_sequence(f, "(", ")", l, pretty, depth),
320			Self::Symbol(sy) => write!(f, "{sy}"),
321			Self::Tagged(t, value) => {
322				write!(f, "#{t} ")?;
323				value.fmt_edn(f, pretty, depth)
324			}
325			Self::Key(k) => write!(f, ":{k}"),
326			Self::Str(s) => write!(f, "\"{s}\""),
327			Self::Int(i) => write!(f, "{i}"),
328			#[cfg(feature = "floats")]
329			Self::Double(d) => write!(f, "{d}"),
330			#[cfg(feature = "arbitrary-nums")]
331			Self::BigInt(bi) => write!(f, "{bi}N"),
332			#[cfg(feature = "arbitrary-nums")]
333			Self::BigDec(bd) => write!(f, "{bd}M"),
334			Self::Rational((n, d)) => write!(f, "{n}/{d}"),
335			Self::Bool(b) => write!(f, "{b}"),
336			Self::Char(c) => {
337				f.write_str("\\")?;
338				if let Some(c) = char_to_edn(*c) {
339					return f.write_str(c);
340				}
341				write!(f, "{c}")
342			}
343			Self::Nil => f.write_str("nil"),
344		}
345	}
346
347	fn fmt_sequence<'a, I>(
348		f: &mut fmt::Formatter<'_>,
349		opener: &str,
350		closer: &str,
351		items: I,
352		pretty: bool,
353		depth: usize,
354	) -> fmt::Result
355	where
356		I: IntoIterator<Item = &'a Self>,
357		Self: 'a,
358	{
359		f.write_str(opener)?;
360		let mut items = items.into_iter().peekable();
361		let mut has_items = false;
362		while let Some(item) = items.next() {
363			has_items = true;
364			if pretty {
365				f.write_str("\n")?;
366				write_indent(f, depth + 1)?;
367			}
368			item.fmt_edn(f, pretty, depth + 1)?;
369			if !pretty && items.peek().is_some() {
370				f.write_str(" ")?;
371			}
372		}
373		if pretty && has_items {
374			f.write_str("\n")?;
375			write_indent(f, depth)?;
376		}
377		f.write_str(closer)
378	}
379}
380
381impl fmt::Display for Edn<'_> {
382	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
383		self.fmt_edn(f, f.alternate(), 0)
384	}
385}