use alloc::borrow::ToOwned;
use alloc::format;
use alloc::string::{String, ToString};
use crate::error::Error;
use crate::value::{Table, Value};
const MAX_DEPTH: usize = 128;
pub(crate) fn to_string(value: &Value) -> Result<String, Error> {
let mut ser = Serializer::new(false);
ser.serialize_value(value, &[])?;
Ok(ser.output)
}
pub(crate) fn to_inline_string(value: &Value) -> Result<String, Error> {
let mut ser = Serializer::new(false);
ser.write_inline_value(value)?;
Ok(ser.output)
}
pub(crate) fn to_string_pretty(value: &Value) -> Result<String, Error> {
let mut ser = Serializer::new(true);
ser.serialize_value(value, &[])?;
Ok(ser.output)
}
struct Serializer {
output: String,
pretty: bool,
depth: usize,
}
impl Serializer {
fn new(pretty: bool) -> Self {
Self {
output: String::new(),
pretty,
depth: 0,
}
}
fn enter_depth(&mut self) -> Result<(), Error> {
self.depth += 1;
if self.depth > MAX_DEPTH {
return Err(Error::serialize(format!(
"nesting depth exceeds maximum of {MAX_DEPTH}"
)));
}
Ok(())
}
fn leave_depth(&mut self) {
self.depth -= 1;
}
fn serialize_value(&mut self, value: &Value, path: &[String]) -> Result<(), Error> {
match value {
Value::Table(table) => self.serialize_table(table, path),
_ => Err(Error::serialize("top-level value must be a table")),
}
}
fn serialize_table(&mut self, table: &Table, path: &[String]) -> Result<(), Error> {
self.enter_depth()?;
for (key, value) in table {
if is_inline_value(value) {
self.write_key(key);
self.output.push_str(" = ");
self.write_inline_value(value)?;
self.output.push('\n');
}
}
for (key, value) in table {
if let Value::Table(sub_table) = value {
let mut sub_path = path.to_vec();
sub_path.push(key.clone());
if self.pretty && !self.output.is_empty() && !self.output.ends_with("\n\n") {
self.output.push('\n');
}
self.output.push('[');
self.write_path(&sub_path);
self.output.push_str("]\n");
self.serialize_table(sub_table, &sub_path)?;
}
}
for (key, value) in table {
if let Value::Array(array) = value
&& is_array_of_tables(array)
{
let mut sub_path = path.to_vec();
sub_path.push(key.clone());
for element in array {
if let Value::Table(sub_table) = element {
if self.pretty && !self.output.is_empty() && !self.output.ends_with("\n\n")
{
self.output.push('\n');
}
self.output.push_str("[[");
self.write_path(&sub_path);
self.output.push_str("]]\n");
self.serialize_table(sub_table, &sub_path)?;
}
}
}
}
self.leave_depth();
Ok(())
}
fn write_key(&mut self, key: &str) {
if needs_quoting(key) {
self.write_quoted_key(key);
} else {
self.output.push_str(key);
}
}
fn write_quoted_key(&mut self, key: &str) {
self.output.push('"');
write_escaped_string(&mut self.output, key);
self.output.push('"');
}
fn write_path(&mut self, path: &[String]) {
for (i, part) in path.iter().enumerate() {
if i > 0 {
self.output.push('.');
}
self.write_key(part);
}
}
fn write_inline_value(&mut self, value: &Value) -> Result<(), Error> {
match value {
Value::String(s) => {
self.output.push('"');
write_escaped_string(&mut self.output, s);
self.output.push('"');
}
Value::Integer(n) => {
self.output.push_str(&n.to_string());
}
Value::Float(f) => {
self.write_float(*f);
}
Value::Boolean(b) => {
self.output.push_str(if *b { "true" } else { "false" });
}
Value::Datetime(dt) => {
self.output.push_str(&dt.to_string());
}
Value::Array(arr) => {
self.write_inline_array(arr)?;
}
Value::Table(table) => {
self.write_inline_table(table)?;
}
}
Ok(())
}
fn write_float(&mut self, f: f64) {
if f.is_nan() {
self.output.push_str("nan");
} else if f.is_infinite() {
if f.is_sign_positive() {
self.output.push_str("inf");
} else {
self.output.push_str("-inf");
}
} else {
let s = format!("{f}");
self.output.push_str(&s);
if !s.contains('.') && !s.contains('e') && !s.contains('E') {
self.output.push_str(".0");
}
}
}
fn write_inline_array(&mut self, array: &[Value]) -> Result<(), Error> {
self.enter_depth()?;
self.output.push('[');
for (i, value) in array.iter().enumerate() {
if i > 0 {
self.output.push_str(", ");
}
self.write_inline_value(value)?;
}
self.output.push(']');
self.leave_depth();
Ok(())
}
fn write_inline_table(&mut self, table: &Table) -> Result<(), Error> {
self.enter_depth()?;
self.output.push('{');
for (i, (key, value)) in table.iter().enumerate() {
if i > 0 {
self.output.push_str(", ");
}
self.write_key(key);
self.output.push_str(" = ");
self.write_inline_value(value)?;
}
self.output.push('}');
self.leave_depth();
Ok(())
}
}
fn is_inline_value(value: &Value) -> bool {
match value {
Value::Table(_) => false,
Value::Array(arr) => !is_array_of_tables(arr),
_ => true,
}
}
fn is_array_of_tables(array: &[Value]) -> bool {
!array.is_empty() && array.iter().all(|v| v.is_table())
}
pub(crate) fn needs_quoting(key: &str) -> bool {
key.is_empty()
|| !key
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
}
pub(crate) fn format_key(key: &str) -> String {
if needs_quoting(key) {
let mut out = String::with_capacity(key.len() + 2);
out.push('"');
write_escaped_string(&mut out, key);
out.push('"');
out
} else {
key.to_owned()
}
}
pub(crate) fn write_escaped_string(output: &mut String, s: &str) {
for ch in s.chars() {
match ch {
'\u{0008}' => output.push_str("\\b"),
'\t' => output.push_str("\\t"),
'\n' => output.push_str("\\n"),
'\u{000C}' => output.push_str("\\f"),
'\r' => output.push_str("\\r"),
'"' => output.push_str("\\\""),
'\\' => output.push_str("\\\\"),
c if c.is_control() => {
let code = c as u32;
if code <= 0xFFFF {
output.push_str(&format!("\\u{code:04X}"));
} else {
output.push_str(&format!("\\U{code:08X}"));
}
}
c => output.push(c),
}
}
}