Skip to main content

facet_toml/
serializer.rs

1extern crate alloc;
2
3use alloc::{string::String, vec::Vec};
4use core::fmt::Write;
5
6use facet_format::{FormatSerializer, ScalarValue, SerializeError};
7
8/// Options for TOML serialization.
9#[derive(Debug, Clone, Default)]
10pub struct SerializeOptions {
11    /// Whether to use inline tables for nested structures (default: false)
12    pub inline_tables: bool,
13}
14
15impl SerializeOptions {
16    /// Create new default options.
17    pub fn new() -> Self {
18        Self::default()
19    }
20
21    /// Enable inline tables for nested structures.
22    pub const fn inline_tables(mut self) -> Self {
23        self.inline_tables = true;
24        self
25    }
26}
27
28#[derive(Debug)]
29pub struct TomlSerializeError {
30    msg: String,
31}
32
33impl core::fmt::Display for TomlSerializeError {
34    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
35        f.write_str(&self.msg)
36    }
37}
38
39impl std::error::Error for TomlSerializeError {}
40
41#[derive(Debug, Clone)]
42enum Ctx {
43    /// Top-level table (root)
44    Root { first: bool },
45    /// Nested table (e.g., `[section]`)
46    /// Note: Currently unused. Will be used for pretty printing with table headers.
47    #[allow(dead_code)]
48    Table { first: bool, path: Vec<String> },
49    /// Inline table (e.g., `{ key = value }`)
50    InlineTable { first: bool },
51    /// Array (e.g., `[1, 2, 3]`)
52    Array { first: bool },
53}
54
55/// TOML serializer with configurable formatting options.
56pub struct TomlSerializer {
57    out: String,
58    stack: Vec<Ctx>,
59    /// Formatting options (currently unused, reserved for pretty printing)
60    #[allow(dead_code)]
61    options: SerializeOptions,
62    /// Current table path for dotted keys (reserved for pretty printing)
63    #[allow(dead_code)]
64    current_path: Vec<String>,
65}
66
67impl TomlSerializer {
68    /// Create a new TOML serializer with default options.
69    pub fn new() -> Self {
70        Self::with_options(SerializeOptions::default())
71    }
72
73    /// Create a new TOML serializer with the given options.
74    pub const fn with_options(options: SerializeOptions) -> Self {
75        Self {
76            out: String::new(),
77            stack: Vec::new(),
78            options,
79            current_path: Vec::new(),
80        }
81    }
82
83    /// Consume the serializer and return the output string.
84    pub fn finish(self) -> String {
85        self.out
86    }
87
88    /// Check if we're in an inline context (inline table or array)
89    /// Note: Reserved for pretty printing implementation.
90    #[allow(dead_code)]
91    fn is_inline_context(&self) -> bool {
92        matches!(
93            self.stack.last(),
94            Some(Ctx::InlineTable { .. }) | Some(Ctx::Array { .. })
95        )
96    }
97
98    /// Write a TOML string value with proper escaping
99    fn write_toml_string(&mut self, s: &str) {
100        self.out.push('"');
101        for c in s.chars() {
102            match c {
103                '"' => self.out.push_str(r#"\""#),
104                '\\' => self.out.push_str(r"\\"),
105                '\n' => self.out.push_str(r"\n"),
106                '\r' => self.out.push_str(r"\r"),
107                '\t' => self.out.push_str(r"\t"),
108                c if c.is_control() => {
109                    write!(self.out, "\\u{:04X}", c as u32).unwrap();
110                }
111                c => self.out.push(c),
112            }
113        }
114        self.out.push('"');
115    }
116}
117
118impl Default for TomlSerializer {
119    fn default() -> Self {
120        Self::new()
121    }
122}
123
124impl FormatSerializer for TomlSerializer {
125    type Error = TomlSerializeError;
126
127    fn begin_struct(&mut self) -> Result<(), Self::Error> {
128        match self.stack.last_mut() {
129            None => {
130                // Root level - just start tracking as root
131                self.stack.push(Ctx::Root { first: true });
132                Ok(())
133            }
134            Some(Ctx::InlineTable { .. }) | Some(Ctx::Array { .. }) => {
135                // We're in an inline context - use inline table syntax
136                self.out.push_str("{ ");
137                self.stack.push(Ctx::InlineTable { first: true });
138                Ok(())
139            }
140            Some(Ctx::Root { .. }) | Some(Ctx::Table { .. }) => {
141                // Nested table - will be handled via dotted keys or [table] headers
142                // For now, use inline table
143                self.out.push_str("{ ");
144                self.stack.push(Ctx::InlineTable { first: true });
145                Ok(())
146            }
147        }
148    }
149
150    fn field_key(&mut self, key: &str) -> Result<(), Self::Error> {
151        match self.stack.last_mut() {
152            Some(Ctx::Root { first }) | Some(Ctx::Table { first, .. }) => {
153                // Top-level or table field
154                if !*first {
155                    self.out.push('\n');
156                }
157                *first = false;
158
159                // Write the key
160                if key
161                    .chars()
162                    .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
163                {
164                    // Simple key
165                    self.out.push_str(key);
166                } else {
167                    // Quoted key
168                    self.write_toml_string(key);
169                }
170                self.out.push_str(" = ");
171                Ok(())
172            }
173            Some(Ctx::InlineTable { first }) => {
174                // Inline table field
175                if !*first {
176                    self.out.push_str(", ");
177                }
178                *first = false;
179
180                // Write the key
181                if key
182                    .chars()
183                    .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
184                {
185                    self.out.push_str(key);
186                } else {
187                    self.write_toml_string(key);
188                }
189                self.out.push_str(" = ");
190                Ok(())
191            }
192            _ => Err(TomlSerializeError {
193                msg: "field_key called outside of a struct context".into(),
194            }),
195        }
196    }
197
198    fn end_struct(&mut self) -> Result<(), Self::Error> {
199        match self.stack.pop() {
200            Some(Ctx::Root { .. }) => {
201                // Root table ends - add final newline if there's content
202                if !self.out.is_empty() && !self.out.ends_with('\n') {
203                    self.out.push('\n');
204                }
205                Ok(())
206            }
207            Some(Ctx::InlineTable { .. }) => {
208                self.out.push_str(" }");
209                Ok(())
210            }
211            Some(Ctx::Table { .. }) => {
212                // Nested table ends
213                Ok(())
214            }
215            _ => Err(TomlSerializeError {
216                msg: "end_struct called without matching begin_struct".into(),
217            }),
218        }
219    }
220
221    fn begin_seq(&mut self) -> Result<(), Self::Error> {
222        self.out.push('[');
223        self.stack.push(Ctx::Array { first: true });
224        Ok(())
225    }
226
227    fn end_seq(&mut self) -> Result<(), Self::Error> {
228        match self.stack.pop() {
229            Some(Ctx::Array { .. }) => {
230                self.out.push(']');
231                Ok(())
232            }
233            _ => Err(TomlSerializeError {
234                msg: "end_seq called without matching begin_seq".into(),
235            }),
236        }
237    }
238
239    fn scalar(&mut self, scalar: ScalarValue<'_>) -> Result<(), Self::Error> {
240        // Handle comma separator for arrays
241        if let Some(Ctx::Array { first }) = self.stack.last_mut() {
242            if !*first {
243                self.out.push_str(", ");
244            }
245            *first = false;
246        }
247
248        match scalar {
249            ScalarValue::Null | ScalarValue::Unit => {
250                // TOML doesn't have null - this is an error
251                return Err(TomlSerializeError {
252                    msg: "TOML does not support null values".into(),
253                });
254            }
255            ScalarValue::Bool(v) => {
256                self.out.push_str(if v { "true" } else { "false" });
257            }
258            ScalarValue::Char(c) => {
259                self.write_toml_string(&c.to_string());
260            }
261            ScalarValue::I64(v) => {
262                #[cfg(feature = "fast")]
263                self.out.push_str(itoa::Buffer::new().format(v));
264                #[cfg(not(feature = "fast"))]
265                write!(self.out, "{}", v).unwrap();
266            }
267            ScalarValue::U64(v) => {
268                #[cfg(feature = "fast")]
269                self.out.push_str(itoa::Buffer::new().format(v));
270                #[cfg(not(feature = "fast"))]
271                write!(self.out, "{}", v).unwrap();
272            }
273            ScalarValue::I128(v) => {
274                #[cfg(feature = "fast")]
275                self.out.push_str(itoa::Buffer::new().format(v));
276                #[cfg(not(feature = "fast"))]
277                write!(self.out, "{}", v).unwrap();
278            }
279            ScalarValue::U128(v) => {
280                #[cfg(feature = "fast")]
281                self.out.push_str(itoa::Buffer::new().format(v));
282                #[cfg(not(feature = "fast"))]
283                write!(self.out, "{}", v).unwrap();
284            }
285            ScalarValue::F64(v) => {
286                if v.is_nan() {
287                    self.out.push_str("nan");
288                } else if v.is_infinite() {
289                    if v.is_sign_positive() {
290                        self.out.push_str("inf");
291                    } else {
292                        self.out.push_str("-inf");
293                    }
294                } else {
295                    #[cfg(feature = "fast")]
296                    self.out.push_str(zmij::Buffer::new().format(v));
297                    #[cfg(not(feature = "fast"))]
298                    write!(self.out, "{}", v).unwrap();
299                }
300            }
301            ScalarValue::Str(s) => {
302                self.write_toml_string(&s);
303            }
304            ScalarValue::Bytes(_) => {
305                return Err(TomlSerializeError {
306                    msg: "TOML does not natively support byte arrays".into(),
307                });
308            }
309        }
310        Ok(())
311    }
312}
313
314/// Serialize a value to TOML bytes
315pub fn to_vec<'facet, T>(value: &T) -> Result<Vec<u8>, SerializeError<TomlSerializeError>>
316where
317    T: facet_core::Facet<'facet>,
318{
319    let mut ser = TomlSerializer::new();
320    facet_format::serialize_root(&mut ser, facet_reflect::Peek::new(value))?;
321    Ok(ser.finish().into_bytes())
322}
323
324/// Serialize a value to a TOML string
325pub fn to_string<'facet, T>(value: &T) -> Result<String, SerializeError<TomlSerializeError>>
326where
327    T: facet_core::Facet<'facet>,
328{
329    let mut ser = TomlSerializer::new();
330    facet_format::serialize_root(&mut ser, facet_reflect::Peek::new(value))?;
331    Ok(ser.finish())
332}
333
334/// Serialize a value to a TOML string with custom options.
335pub fn to_string_with_options<'facet, T>(
336    value: &T,
337    options: &SerializeOptions,
338) -> Result<String, SerializeError<TomlSerializeError>>
339where
340    T: facet_core::Facet<'facet>,
341{
342    let mut ser = TomlSerializer::with_options(options.clone());
343    facet_format::serialize_root(&mut ser, facet_reflect::Peek::new(value))?;
344    Ok(ser.finish())
345}