use alloc::string::String;
use core::fmt::Write;
use crate::{
ast::{
AnonStructExpr, Attribute, Comment, Document, Expr, FieldsBody, Ident, MapEntry, MapExpr,
OptionExpr, SeqExpr, SeqItem, StructBody, StructExpr, StructField, Trivia, TupleBody,
TupleElement, TupleExpr,
},
error::Result,
};
pub fn serialize_document(doc: &Document<'_>) -> Result<String> {
let mut output = String::new();
let mut ser = AstSerializer::new(&mut output, &doc.source);
ser.write_document(doc)?;
Ok(output)
}
pub fn serialize_document_to<W: Write>(writer: W, doc: &Document<'_>) -> Result<()> {
let mut ser = AstSerializer::new(writer, &doc.source);
ser.write_document(doc)
}
struct AstSerializer<'a, W: Write> {
writer: W,
source: &'a str,
}
impl<'a, W: Write> AstSerializer<'a, W> {
fn new(writer: W, source: &'a str) -> Self {
Self { writer, source }
}
fn write_document(&mut self, doc: &Document<'_>) -> Result<()> {
self.write_trivia(&doc.leading)?;
for attr in &doc.attributes {
self.write_attribute(attr)?;
}
self.write_trivia(&doc.pre_value)?;
if let Some(ref value) = doc.value {
self.write_expr(value)?;
}
self.write_trivia(&doc.trailing)?;
Ok(())
}
fn write_trivia(&mut self, trivia: &Trivia<'_>) -> Result<()> {
if let Some(ref span) = trivia.span {
let text = span.slice(self.source);
self.writer.write_str(text)?;
} else {
self.writer.write_str(&trivia.whitespace)?;
for comment in &trivia.comments {
self.write_comment(comment)?;
}
}
Ok(())
}
fn write_comment(&mut self, comment: &Comment<'_>) -> Result<()> {
self.writer.write_str(&comment.text)?;
Ok(())
}
fn write_attribute(&mut self, attr: &Attribute<'_>) -> Result<()> {
self.write_trivia(&attr.leading)?;
let text = attr.span.slice(self.source);
self.writer.write_str(text)?;
Ok(())
}
fn write_expr(&mut self, expr: &Expr<'_>) -> Result<()> {
match expr {
Expr::Unit(u) => {
let text = u.span.slice(self.source);
self.writer.write_str(text)?;
}
Expr::Bool(b) => {
let text = b.span.slice(self.source);
self.writer.write_str(text)?;
}
Expr::Char(c) => {
self.writer.write_str(&c.raw)?;
}
Expr::Byte(b) => {
self.writer.write_str(&b.raw)?;
}
Expr::Number(n) => {
self.writer.write_str(&n.raw)?;
}
Expr::String(s) => {
self.writer.write_str(&s.raw)?;
}
Expr::Bytes(b) => {
self.writer.write_str(&b.raw)?;
}
Expr::Option(opt) => self.write_option(opt)?,
Expr::Seq(seq) => self.write_seq(seq)?,
Expr::Map(map) => self.write_map(map)?,
Expr::Tuple(tuple) => self.write_tuple(tuple)?,
Expr::AnonStruct(s) => self.write_anon_struct(s)?,
Expr::Struct(s) => self.write_struct(s)?,
Expr::Error(_) => {
return Err(crate::error::Error::new(crate::error::ErrorKind::Message(
"cannot serialize error expression".to_string(),
)));
}
}
Ok(())
}
fn write_option(&mut self, opt: &OptionExpr<'_>) -> Result<()> {
if let Some(inner) = &opt.value {
self.writer.write_str("Some")?;
let open_text = inner.open_paren.slice(self.source);
self.writer.write_str(open_text)?;
self.write_trivia(&inner.leading)?;
self.write_expr(&inner.expr)?;
self.write_trivia(&inner.trailing)?;
let close_text = inner.close_paren.slice(self.source);
self.writer.write_str(close_text)?;
} else {
let text = opt.span.slice(self.source);
self.writer.write_str(text)?;
}
Ok(())
}
fn write_seq(&mut self, seq: &SeqExpr<'_>) -> Result<()> {
let open_text = seq.open_bracket.slice(self.source);
self.writer.write_str(open_text)?;
self.write_trivia(&seq.leading)?;
for item in &seq.items {
self.write_seq_item(item)?;
}
self.write_trivia(&seq.trailing)?;
let close_text = seq.close_bracket.slice(self.source);
self.writer.write_str(close_text)?;
Ok(())
}
fn write_seq_item(&mut self, item: &SeqItem<'_>) -> Result<()> {
self.write_trivia(&item.leading)?;
self.write_expr(&item.expr)?;
self.write_trivia(&item.trailing)?;
if let Some(ref comma) = item.comma {
let text = comma.slice(self.source);
self.writer.write_str(text)?;
}
Ok(())
}
fn write_map(&mut self, map: &MapExpr<'_>) -> Result<()> {
let open_text = map.open_brace.slice(self.source);
self.writer.write_str(open_text)?;
self.write_trivia(&map.leading)?;
for entry in &map.entries {
self.write_map_entry(entry)?;
}
self.write_trivia(&map.trailing)?;
let close_text = map.close_brace.slice(self.source);
self.writer.write_str(close_text)?;
Ok(())
}
fn write_map_entry(&mut self, entry: &MapEntry<'_>) -> Result<()> {
self.write_trivia(&entry.leading)?;
self.write_expr(&entry.key)?;
self.write_trivia(&entry.pre_colon)?;
let colon_text = entry.colon.slice(self.source);
self.writer.write_str(colon_text)?;
self.write_trivia(&entry.post_colon)?;
self.write_expr(&entry.value)?;
self.write_trivia(&entry.trailing)?;
if let Some(ref comma) = entry.comma {
let text = comma.slice(self.source);
self.writer.write_str(text)?;
}
Ok(())
}
fn write_tuple(&mut self, tuple: &TupleExpr<'_>) -> Result<()> {
let open_text = tuple.open_paren.slice(self.source);
self.writer.write_str(open_text)?;
self.write_trivia(&tuple.leading)?;
for elem in &tuple.elements {
self.write_tuple_element(elem)?;
}
self.write_trivia(&tuple.trailing)?;
let close_text = tuple.close_paren.slice(self.source);
self.writer.write_str(close_text)?;
Ok(())
}
fn write_tuple_element(&mut self, elem: &TupleElement<'_>) -> Result<()> {
self.write_trivia(&elem.leading)?;
self.write_expr(&elem.expr)?;
self.write_trivia(&elem.trailing)?;
if let Some(ref comma) = elem.comma {
let text = comma.slice(self.source);
self.writer.write_str(text)?;
}
Ok(())
}
fn write_anon_struct(&mut self, s: &AnonStructExpr<'_>) -> Result<()> {
let open_text = s.open_paren.slice(self.source);
self.writer.write_str(open_text)?;
self.write_trivia(&s.leading)?;
for field in &s.fields {
self.write_struct_field(field)?;
}
self.write_trivia(&s.trailing)?;
let close_text = s.close_paren.slice(self.source);
self.writer.write_str(close_text)?;
Ok(())
}
fn write_struct(&mut self, s: &StructExpr<'_>) -> Result<()> {
self.write_ident(&s.name)?;
self.write_trivia(&s.pre_body)?;
if let Some(ref body) = s.body {
match body {
StructBody::Tuple(tuple) => self.write_tuple_body(tuple)?,
StructBody::Fields(fields) => self.write_fields_body(fields)?,
}
}
Ok(())
}
fn write_ident(&mut self, ident: &Ident<'_>) -> Result<()> {
let text = ident.span.slice(self.source);
self.writer.write_str(text)?;
Ok(())
}
fn write_tuple_body(&mut self, tuple: &TupleBody<'_>) -> Result<()> {
let open_text = tuple.open_paren.slice(self.source);
self.writer.write_str(open_text)?;
self.write_trivia(&tuple.leading)?;
for elem in &tuple.elements {
self.write_tuple_element(elem)?;
}
self.write_trivia(&tuple.trailing)?;
let close_text = tuple.close_paren.slice(self.source);
self.writer.write_str(close_text)?;
Ok(())
}
fn write_fields_body(&mut self, fields: &FieldsBody<'_>) -> Result<()> {
let open_text = fields.open_brace.slice(self.source);
self.writer.write_str(open_text)?;
self.write_trivia(&fields.leading)?;
for field in &fields.fields {
self.write_struct_field(field)?;
}
self.write_trivia(&fields.trailing)?;
let close_text = fields.close_brace.slice(self.source);
self.writer.write_str(close_text)?;
Ok(())
}
fn write_struct_field(&mut self, field: &StructField<'_>) -> Result<()> {
self.write_trivia(&field.leading)?;
self.write_ident(&field.name)?;
self.write_trivia(&field.pre_colon)?;
let colon_text = field.colon.slice(self.source);
self.writer.write_str(colon_text)?;
self.write_trivia(&field.post_colon)?;
self.write_expr(&field.value)?;
self.write_trivia(&field.trailing)?;
if let Some(ref comma) = field.comma {
let text = comma.slice(self.source);
self.writer.write_str(text)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::parse_document;
fn assert_round_trip(source: &str) {
let doc = parse_document(source).unwrap();
let output = serialize_document(&doc).unwrap();
assert_eq!(output, source, "Round-trip failed for: {source:?}");
}
#[test]
fn round_trip_integer() {
assert_round_trip("42");
}
#[test]
fn round_trip_negative() {
assert_round_trip("-42");
}
#[test]
fn round_trip_hex() {
assert_round_trip("0xFF");
}
#[test]
fn round_trip_float() {
assert_round_trip("3.14");
}
#[test]
fn round_trip_string() {
assert_round_trip(r#""hello""#);
}
#[test]
fn round_trip_raw_string() {
assert_round_trip(r##"r#"raw string"#"##);
}
#[test]
fn round_trip_bytes() {
assert_round_trip(r#"b"hello""#);
}
#[test]
fn round_trip_raw_bytes() {
assert_round_trip(r#"br"hello""#);
}
#[test]
fn round_trip_raw_bytes_with_hash() {
assert_round_trip(r##"br#"hello"#"##);
}
#[test]
fn round_trip_char() {
assert_round_trip("'a'");
}
#[test]
fn round_trip_bool() {
assert_round_trip("true");
assert_round_trip("false");
}
#[test]
fn round_trip_unit() {
assert_round_trip("()");
}
#[test]
fn round_trip_none() {
assert_round_trip("None");
}
#[test]
fn round_trip_some() {
assert_round_trip("Some(42)");
}
#[test]
fn round_trip_seq() {
assert_round_trip("[1, 2, 3]");
}
#[test]
fn round_trip_seq_trailing_comma() {
assert_round_trip("[1, 2, 3,]");
}
#[test]
fn round_trip_empty_seq() {
assert_round_trip("[]");
}
#[test]
fn round_trip_map() {
assert_round_trip(r#"{"a": 1, "b": 2}"#);
}
#[test]
fn round_trip_tuple() {
assert_round_trip("(1, 2, 3)");
}
#[test]
fn round_trip_struct() {
assert_round_trip("Point(1, 2)");
}
#[test]
fn round_trip_struct_fields() {
assert_round_trip("Point(x: 1, y: 2)");
}
#[test]
fn round_trip_with_comments() {
assert_round_trip("// header\n42");
}
#[test]
fn round_trip_block_comment() {
assert_round_trip("/* comment */ 42");
}
#[test]
fn round_trip_whitespace() {
assert_round_trip(" 42 ");
}
#[test]
fn round_trip_multiline() {
assert_round_trip("[\n 1,\n 2,\n 3\n]");
}
#[test]
fn round_trip_complex() {
let source = r#"// Configuration file
Config(
name: "test",
// Port number
port: 8080,
enabled: true,
tags: ["web", "api"],
)"#;
assert_round_trip(source);
}
#[test]
fn round_trip_attribute() {
assert_round_trip(r"#![enable(unwrap_newtypes)] 42");
}
#[test]
fn round_trip_empty_document() {
assert_round_trip("");
}
#[test]
fn round_trip_comment_only() {
assert_round_trip("// just a comment\n");
}
#[test]
fn round_trip_anon_struct_simple() {
assert_round_trip(r#"(name: "test", value: 42)"#);
}
#[test]
fn round_trip_anon_struct_single_field() {
assert_round_trip("(x: 1)");
}
#[test]
fn round_trip_anon_struct_trailing_comma() {
assert_round_trip("(x: 1, y: 2,)");
}
#[test]
fn round_trip_anon_struct_nested() {
assert_round_trip("(outer: (inner: 42))");
}
#[test]
fn round_trip_anon_struct_with_whitespace() {
assert_round_trip("( x : 1 , y : 2 )");
}
#[test]
fn round_trip_anon_struct_multiline() {
assert_round_trip("(\n x: 1,\n y: 2\n)");
}
#[test]
fn round_trip_anon_struct_with_comments() {
assert_round_trip("(\n // comment\n x: 1\n)");
}
#[test]
fn round_trip_anon_struct_complex() {
let source = r#"(
name: "test",
// config section
config: (
enabled: true,
values: [1, 2, 3]
),
optional: Some("value")
)"#;
assert_round_trip(source);
}
#[test]
fn round_trip_anon_struct_vs_tuple() {
assert_round_trip("(x: 1)");
assert_round_trip("(1, 2, 3)");
assert_round_trip("(x)");
}
}