use crate::error::Result;
use crate::parser::ast::{
AstNode, AstValue, Comment, Document, FormatMetadata, FormatStyle, Indentation, Key,
LineEnding, StringStyle, TableEntry,
};
use std::fmt::Write;
pub struct Serializer {
output: String,
indent_level: usize,
indentation: Indentation,
line_ending: LineEnding,
}
impl Serializer {
pub fn new() -> Self {
Self {
output: String::new(),
indent_level: 0,
indentation: Indentation::default(),
line_ending: LineEnding::default(),
}
}
pub fn with_options(indentation: Indentation, line_ending: LineEnding) -> Self {
Self {
output: String::new(),
indent_level: 0,
indentation,
line_ending,
}
}
pub fn serialize_document(&mut self, document: &Document) -> Result<String> {
self.output.clear();
self.output
.push_str(&document.root.format.leading_whitespace);
self.serialize_ast_node(&document.root)?;
self.output
.push_str(&document.root.format.trailing_whitespace);
Ok(self.output.clone())
}
fn serialize_table_entry(&mut self, entry: &TableEntry) -> Result<()> {
self.output.push_str(&entry.value.format.leading_whitespace);
for comment in &entry.comments.before {
self.serialize_comment(comment);
self.add_line_ending();
}
self.serialize_key(&entry.key);
if let FormatStyle::KeyValue { equals_spacing, .. } = &entry.value.format.format_style {
self.output.push_str(&equals_spacing.before);
self.output.push('=');
self.output.push_str(&equals_spacing.after);
} else {
self.output.push_str(" = ");
}
self.serialize_ast_node(&entry.value)?;
if let Some(ref comment) = entry.comments.inline {
self.output.push(' ');
self.serialize_comment(comment);
}
self.add_line_ending();
for comment in &entry.comments.after {
self.serialize_comment(comment);
self.add_line_ending();
}
self.output
.push_str(&entry.value.format.trailing_whitespace);
Ok(())
}
fn serialize_key(&mut self, key: &Key) {
for (i, segment) in key.segments.iter().enumerate() {
if i > 0 {
self.output.push('.');
}
if segment.quoted {
let quote_char = match segment.quote_style {
Some(StringStyle::Double) => '"',
Some(StringStyle::Single) => '\'',
_ => '"', };
self.output.push(quote_char);
self.output.push_str(&segment.name);
self.output.push(quote_char);
} else {
self.output.push_str(&segment.name);
}
}
}
fn serialize_ast_node(&mut self, node: &AstNode) -> Result<()> {
match &node.value {
AstValue::Null => self.output.push_str("null"),
AstValue::Bool(b) => self.output.push_str(&b.to_string()),
AstValue::Integer { raw, .. } => self.output.push_str(raw),
AstValue::Float { raw, .. } => self.output.push_str(raw),
AstValue::String {
value,
style,
has_escapes,
} => {
self.serialize_string(value, style, *has_escapes);
}
AstValue::Array { elements, .. } => {
self.serialize_array(elements, &node.format)?;
}
AstValue::Table { entries, inline } => {
if *inline {
self.serialize_inline_table(entries)?;
} else {
self.serialize_table(entries)?;
}
}
AstValue::FunctionCall { name, args } => {
self.serialize_function_call(name, args)?;
}
AstValue::Interpolation { path } => {
write!(self.output, "${{{path}}}").map_err(|e| {
crate::error::NomlError::validation(format!(
"Failed to write interpolation: {e}"
))
})?;
}
AstValue::Include { path } => {
write!(self.output, "include \"{path}\"").map_err(|e| {
crate::error::NomlError::validation(format!("Failed to write include: {e}"))
})?;
}
AstValue::Native { type_name, args } => {
write!(self.output, "@{type_name}(").map_err(|e| {
crate::error::NomlError::validation(format!("Failed to write native type: {e}"))
})?;
for (i, arg) in args.iter().enumerate() {
if i > 0 {
self.output.push_str(", ");
}
self.serialize_ast_node(arg)?;
}
self.output.push(')');
}
}
Ok(())
}
fn serialize_string(&mut self, value: &str, style: &StringStyle, has_escapes: bool) {
match style {
StringStyle::Double => {
self.output.push('"');
if has_escapes {
self.escape_string(value, '"');
} else {
self.output.push_str(value);
}
self.output.push('"');
}
StringStyle::Single => {
self.output.push('\'');
if has_escapes {
self.escape_string(value, '\'');
} else {
self.output.push_str(value);
}
self.output.push('\'');
}
StringStyle::TripleDouble => {
self.output.push_str("\"\"\"");
self.output.push_str(value);
self.output.push_str("\"\"\"");
}
StringStyle::TripleSingle => {
self.output.push_str("'''");
self.output.push_str(value);
self.output.push_str("'''");
}
StringStyle::Raw { hashes } => {
self.output.push('r');
for _ in 0..*hashes {
self.output.push('#');
}
self.output.push('"');
self.output.push_str(value);
self.output.push('"');
for _ in 0..*hashes {
self.output.push('#');
}
}
}
}
fn escape_string(&mut self, value: &str, quote_char: char) {
for ch in value.chars() {
match ch {
'\n' => self.output.push_str("\\n"),
'\t' => self.output.push_str("\\t"),
'\r' => self.output.push_str("\\r"),
'\\' => self.output.push_str("\\\\"),
'"' if quote_char == '"' => self.output.push_str("\\\""),
'\'' if quote_char == '\'' => self.output.push_str("\\'"),
c => self.output.push(c),
}
}
}
fn serialize_array(&mut self, elements: &[AstNode], format: &FormatMetadata) -> Result<()> {
self.output.push('[');
if let FormatStyle::Array {
multiline,
trailing_comma,
bracket_spacing,
} = &format.format_style
{
self.output.push_str(&bracket_spacing.after_open);
if *multiline {
self.add_line_ending();
self.indent_level += 1;
for (i, element) in elements.iter().enumerate() {
self.add_indentation();
self.serialize_ast_node(element)?;
if i < elements.len() - 1 || *trailing_comma {
self.output.push(',');
}
self.add_line_ending();
}
self.indent_level -= 1;
self.add_indentation();
} else {
for (i, element) in elements.iter().enumerate() {
if i > 0 {
self.output.push_str(", ");
}
self.serialize_ast_node(element)?;
}
if *trailing_comma && !elements.is_empty() {
self.output.push(',');
}
}
self.output.push_str(&bracket_spacing.before_close);
} else {
for (i, element) in elements.iter().enumerate() {
if i > 0 {
self.output.push_str(", ");
}
self.serialize_ast_node(element)?;
}
}
self.output.push(']');
Ok(())
}
fn serialize_inline_table(&mut self, entries: &[TableEntry]) -> Result<()> {
self.output.push_str("{ ");
for (i, entry) in entries.iter().enumerate() {
if i > 0 {
self.output.push_str(", ");
}
self.serialize_key(&entry.key);
self.output.push_str(" = ");
self.serialize_ast_node(&entry.value)?;
}
self.output.push_str(" }");
Ok(())
}
fn serialize_table(&mut self, entries: &[TableEntry]) -> Result<()> {
for entry in entries {
self.serialize_table_entry(entry)?;
}
Ok(())
}
fn serialize_function_call(&mut self, name: &str, args: &[AstNode]) -> Result<()> {
write!(self.output, "{name}(").map_err(|e| {
crate::error::NomlError::validation(format!("Failed to write function call: {e}"))
})?;
for (i, arg) in args.iter().enumerate() {
if i > 0 {
self.output.push_str(", ");
}
self.serialize_ast_node(arg)?;
}
self.output.push(')');
Ok(())
}
fn serialize_comment(&mut self, comment: &Comment) {
self.output.push('#');
if !comment.text.is_empty() {
self.output.push(' ');
self.output.push_str(&comment.text);
}
}
fn add_line_ending(&mut self) {
match self.line_ending {
LineEnding::Unix => self.output.push('\n'),
LineEnding::Windows => self.output.push_str("\r\n"),
LineEnding::Mac => self.output.push('\r'),
}
}
fn add_indentation(&mut self) {
let indent_str = if self.indentation.use_tabs {
"\t".repeat(self.indent_level)
} else {
" ".repeat(self.indent_level * self.indentation.size)
};
self.output.push_str(&indent_str);
}
}
impl Default for Serializer {
fn default() -> Self {
Self::new()
}
}
pub fn serialize_document(document: &Document) -> Result<String> {
let mut serializer = Serializer::new();
serializer.serialize_document(document)
}
pub fn serialize_document_with_options(
document: &Document,
indentation: Indentation,
line_ending: LineEnding,
) -> Result<String> {
let mut serializer = Serializer::with_options(indentation, line_ending);
serializer.serialize_document(document)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::ast::*;
#[test]
fn test_serialize_simple_values() {
let mut serializer = Serializer::new();
let null_node = AstNode::new(AstValue::Null, Span::default());
serializer.serialize_ast_node(&null_node).unwrap();
assert_eq!(serializer.output, "null");
serializer.output.clear();
let bool_node = AstNode::new(AstValue::Bool(true), Span::default());
serializer.serialize_ast_node(&bool_node).unwrap();
assert_eq!(serializer.output, "true");
}
#[test]
fn test_serialize_string_with_escapes() {
let mut serializer = Serializer::new();
let string_node = AstNode::new(
AstValue::String {
value: "hello\nworld".to_string(),
style: StringStyle::Double,
has_escapes: true,
},
Span::default(),
);
serializer.serialize_ast_node(&string_node).unwrap();
assert_eq!(serializer.output, "\"hello\\nworld\"");
}
#[test]
fn test_serialize_raw_string() {
let mut serializer = Serializer::new();
let string_node = AstNode::new(
AstValue::String {
value: "no\\escapes".to_string(),
style: StringStyle::Raw { hashes: 0 },
has_escapes: false,
},
Span::default(),
);
serializer.serialize_ast_node(&string_node).unwrap();
assert_eq!(serializer.output, "r\"no\\escapes\"");
}
}