1extern crate alloc;
2
3use alloc::{string::String, vec::Vec};
4use core::fmt::Write;
5
6use facet_format::{FormatSerializer, ScalarValue, SerializeError};
7
8#[derive(Debug, Clone, Default)]
10pub struct SerializeOptions {
11 pub inline_tables: bool,
13}
14
15impl SerializeOptions {
16 pub fn new() -> Self {
18 Self::default()
19 }
20
21 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 Root { first: bool },
45 #[allow(dead_code)]
48 Table { first: bool, path: Vec<String> },
49 InlineTable { first: bool },
51 Array { first: bool },
53}
54
55pub struct TomlSerializer {
57 out: String,
58 stack: Vec<Ctx>,
59 #[allow(dead_code)]
61 options: SerializeOptions,
62 #[allow(dead_code)]
64 current_path: Vec<String>,
65}
66
67impl TomlSerializer {
68 pub fn new() -> Self {
70 Self::with_options(SerializeOptions::default())
71 }
72
73 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 pub fn finish(self) -> String {
85 self.out
86 }
87
88 #[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 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 self.stack.push(Ctx::Root { first: true });
132 Ok(())
133 }
134 Some(Ctx::InlineTable { .. }) | Some(Ctx::Array { .. }) => {
135 self.out.push_str("{ ");
137 self.stack.push(Ctx::InlineTable { first: true });
138 Ok(())
139 }
140 Some(Ctx::Root { .. }) | Some(Ctx::Table { .. }) => {
141 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 if !*first {
155 self.out.push('\n');
156 }
157 *first = false;
158
159 if key
161 .chars()
162 .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
163 {
164 self.out.push_str(key);
166 } else {
167 self.write_toml_string(key);
169 }
170 self.out.push_str(" = ");
171 Ok(())
172 }
173 Some(Ctx::InlineTable { first }) => {
174 if !*first {
176 self.out.push_str(", ");
177 }
178 *first = false;
179
180 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 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 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 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 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
314pub 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
324pub 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
334pub 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}