extern crate alloc;
use alloc::{string::String, vec::Vec};
use core::fmt::Write;
use facet_format::{FormatSerializer, ScalarValue, SerializeError};
#[derive(Debug, Clone, Default)]
pub struct SerializeOptions {
pub inline_tables: bool,
}
impl SerializeOptions {
pub fn new() -> Self {
Self::default()
}
pub const fn inline_tables(mut self) -> Self {
self.inline_tables = true;
self
}
}
#[derive(Debug)]
pub struct TomlSerializeError {
msg: String,
}
impl core::fmt::Display for TomlSerializeError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(&self.msg)
}
}
impl std::error::Error for TomlSerializeError {}
#[derive(Debug, Clone)]
enum Ctx {
Root { first: bool },
#[allow(dead_code)]
Table { first: bool, path: Vec<String> },
InlineTable { first: bool },
Array { first: bool },
}
pub struct TomlSerializer {
out: String,
stack: Vec<Ctx>,
#[allow(dead_code)]
options: SerializeOptions,
#[allow(dead_code)]
current_path: Vec<String>,
}
impl TomlSerializer {
pub fn new() -> Self {
Self::with_options(SerializeOptions::default())
}
pub const fn with_options(options: SerializeOptions) -> Self {
Self {
out: String::new(),
stack: Vec::new(),
options,
current_path: Vec::new(),
}
}
pub fn finish(self) -> String {
self.out
}
#[allow(dead_code)]
fn is_inline_context(&self) -> bool {
matches!(
self.stack.last(),
Some(Ctx::InlineTable { .. }) | Some(Ctx::Array { .. })
)
}
fn write_toml_string(&mut self, s: &str) {
self.out.push('"');
for c in s.chars() {
match c {
'"' => self.out.push_str(r#"\""#),
'\\' => self.out.push_str(r"\\"),
'\n' => self.out.push_str(r"\n"),
'\r' => self.out.push_str(r"\r"),
'\t' => self.out.push_str(r"\t"),
c if c.is_control() => {
write!(self.out, "\\u{:04X}", c as u32).unwrap();
}
c => self.out.push(c),
}
}
self.out.push('"');
}
}
impl Default for TomlSerializer {
fn default() -> Self {
Self::new()
}
}
impl FormatSerializer for TomlSerializer {
type Error = TomlSerializeError;
fn begin_struct(&mut self) -> Result<(), Self::Error> {
match self.stack.last_mut() {
None => {
self.stack.push(Ctx::Root { first: true });
Ok(())
}
Some(Ctx::InlineTable { .. }) | Some(Ctx::Array { .. }) => {
self.out.push_str("{ ");
self.stack.push(Ctx::InlineTable { first: true });
Ok(())
}
Some(Ctx::Root { .. }) | Some(Ctx::Table { .. }) => {
self.out.push_str("{ ");
self.stack.push(Ctx::InlineTable { first: true });
Ok(())
}
}
}
fn field_key(&mut self, key: &str) -> Result<(), Self::Error> {
match self.stack.last_mut() {
Some(Ctx::Root { first }) | Some(Ctx::Table { first, .. }) => {
if !*first {
self.out.push('\n');
}
*first = false;
if key
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '-')
{
self.out.push_str(key);
} else {
self.write_toml_string(key);
}
self.out.push_str(" = ");
Ok(())
}
Some(Ctx::InlineTable { first }) => {
if !*first {
self.out.push_str(", ");
}
*first = false;
if key
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '-')
{
self.out.push_str(key);
} else {
self.write_toml_string(key);
}
self.out.push_str(" = ");
Ok(())
}
_ => Err(TomlSerializeError {
msg: "field_key called outside of a struct context".into(),
}),
}
}
fn end_struct(&mut self) -> Result<(), Self::Error> {
match self.stack.pop() {
Some(Ctx::Root { .. }) => {
if !self.out.is_empty() && !self.out.ends_with('\n') {
self.out.push('\n');
}
Ok(())
}
Some(Ctx::InlineTable { .. }) => {
self.out.push_str(" }");
Ok(())
}
Some(Ctx::Table { .. }) => {
Ok(())
}
_ => Err(TomlSerializeError {
msg: "end_struct called without matching begin_struct".into(),
}),
}
}
fn begin_seq(&mut self) -> Result<(), Self::Error> {
self.out.push('[');
self.stack.push(Ctx::Array { first: true });
Ok(())
}
fn end_seq(&mut self) -> Result<(), Self::Error> {
match self.stack.pop() {
Some(Ctx::Array { .. }) => {
self.out.push(']');
Ok(())
}
_ => Err(TomlSerializeError {
msg: "end_seq called without matching begin_seq".into(),
}),
}
}
fn scalar(&mut self, scalar: ScalarValue<'_>) -> Result<(), Self::Error> {
if let Some(Ctx::Array { first }) = self.stack.last_mut() {
if !*first {
self.out.push_str(", ");
}
*first = false;
}
match scalar {
ScalarValue::Null | ScalarValue::Unit => {
return Err(TomlSerializeError {
msg: "TOML does not support null values".into(),
});
}
ScalarValue::Bool(v) => {
self.out.push_str(if v { "true" } else { "false" });
}
ScalarValue::Char(c) => {
self.write_toml_string(&c.to_string());
}
ScalarValue::I64(v) => {
#[cfg(feature = "fast")]
self.out.push_str(itoa::Buffer::new().format(v));
#[cfg(not(feature = "fast"))]
write!(self.out, "{}", v).unwrap();
}
ScalarValue::U64(v) => {
#[cfg(feature = "fast")]
self.out.push_str(itoa::Buffer::new().format(v));
#[cfg(not(feature = "fast"))]
write!(self.out, "{}", v).unwrap();
}
ScalarValue::I128(v) => {
#[cfg(feature = "fast")]
self.out.push_str(itoa::Buffer::new().format(v));
#[cfg(not(feature = "fast"))]
write!(self.out, "{}", v).unwrap();
}
ScalarValue::U128(v) => {
#[cfg(feature = "fast")]
self.out.push_str(itoa::Buffer::new().format(v));
#[cfg(not(feature = "fast"))]
write!(self.out, "{}", v).unwrap();
}
ScalarValue::F64(v) => {
if v.is_nan() {
self.out.push_str("nan");
} else if v.is_infinite() {
if v.is_sign_positive() {
self.out.push_str("inf");
} else {
self.out.push_str("-inf");
}
} else {
#[cfg(feature = "fast")]
self.out.push_str(zmij::Buffer::new().format(v));
#[cfg(not(feature = "fast"))]
write!(self.out, "{}", v).unwrap();
}
}
ScalarValue::Str(s) => {
self.write_toml_string(&s);
}
ScalarValue::Bytes(_) => {
return Err(TomlSerializeError {
msg: "TOML does not natively support byte arrays".into(),
});
}
}
Ok(())
}
}
pub fn to_vec<'facet, T>(value: &T) -> Result<Vec<u8>, SerializeError<TomlSerializeError>>
where
T: facet_core::Facet<'facet>,
{
let mut ser = TomlSerializer::new();
facet_format::serialize_root(&mut ser, facet_reflect::Peek::new(value))?;
Ok(ser.finish().into_bytes())
}
pub fn to_string<'facet, T>(value: &T) -> Result<String, SerializeError<TomlSerializeError>>
where
T: facet_core::Facet<'facet>,
{
let mut ser = TomlSerializer::new();
facet_format::serialize_root(&mut ser, facet_reflect::Peek::new(value))?;
Ok(ser.finish())
}
pub fn to_string_with_options<'facet, T>(
value: &T,
options: &SerializeOptions,
) -> Result<String, SerializeError<TomlSerializeError>>
where
T: facet_core::Facet<'facet>,
{
let mut ser = TomlSerializer::with_options(options.clone());
facet_format::serialize_root(&mut ser, facet_reflect::Peek::new(value))?;
Ok(ser.finish())
}