facet_format_toml/
serializer.rs

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