facet_kdl/
serializer.rs

1//! KDL serialization implementation using FormatSerializer trait.
2
3extern crate alloc;
4
5use alloc::string::String;
6use alloc::vec::Vec;
7
8use facet_core::Facet;
9use facet_format::{FormatSerializer, ScalarValue, SerializeError, serialize_root};
10use facet_reflect::Peek;
11
12/// Error type for KDL serialization.
13#[derive(Debug)]
14pub struct KdlSerializeError {
15    msg: &'static str,
16}
17
18impl core::fmt::Display for KdlSerializeError {
19    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
20        f.write_str(self.msg)
21    }
22}
23
24impl std::error::Error for KdlSerializeError {}
25
26/// Context for tracking serialization state.
27#[derive(Debug, Clone)]
28enum Ctx {
29    /// At the root level - next struct becomes root element
30    Root {
31        /// Name of the root element (from struct_metadata)
32        name: Option<String>,
33    },
34    /// In a struct/node - fields become children
35    Struct {
36        /// Node name
37        name: String,
38        /// Whether we've written the opening brace
39        opened_brace: bool,
40        /// Pending properties (kdl::property fields)
41        properties: Vec<(String, String)>,
42        /// Pending arguments (kdl::argument fields)
43        arguments: Vec<String>,
44    },
45    /// In a sequence - items become child nodes named "item"
46    Seq {
47        /// The wrapper node name (from pending field, e.g., "triple")
48        wrapper_name: String,
49        /// Whether we've written the opening `wrapper {`
50        opened: bool,
51    },
52}
53
54/// KDL serializer implementing FormatSerializer.
55pub struct KdlSerializer {
56    out: Vec<u8>,
57    stack: Vec<Ctx>,
58    pending_field: Option<String>,
59    pending_is_property: bool,
60    pending_is_argument: bool,
61    pending_is_child: bool,
62    indent_level: usize,
63}
64
65impl KdlSerializer {
66    /// Create a new KDL serializer.
67    pub fn new() -> Self {
68        Self {
69            out: Vec::new(),
70            stack: vec![Ctx::Root { name: None }],
71            pending_field: None,
72            pending_is_property: false,
73            pending_is_argument: false,
74            pending_is_child: false,
75            indent_level: 0,
76        }
77    }
78
79    /// Consume the serializer and return the output bytes.
80    pub fn finish(self) -> Vec<u8> {
81        self.out
82    }
83
84    fn write_indent(&mut self) {
85        for _ in 0..self.indent_level {
86            self.out.extend_from_slice(b"    ");
87        }
88    }
89
90    fn scalar_to_string(&self, scalar: &ScalarValue<'_>) -> String {
91        match scalar {
92            ScalarValue::Null => "#null".to_string(),
93            ScalarValue::Bool(true) => "#true".to_string(),
94            ScalarValue::Bool(false) => "#false".to_string(),
95            ScalarValue::Char(c) => {
96                let mut result = String::with_capacity(3);
97                result.push('"');
98                result.push(*c);
99                result.push('"');
100                result
101            }
102            ScalarValue::I64(n) => {
103                #[cfg(feature = "fast")]
104                return itoa::Buffer::new().format(*n).to_string();
105                #[cfg(not(feature = "fast"))]
106                n.to_string()
107            }
108            ScalarValue::U64(n) => {
109                #[cfg(feature = "fast")]
110                return itoa::Buffer::new().format(*n).to_string();
111                #[cfg(not(feature = "fast"))]
112                n.to_string()
113            }
114            ScalarValue::I128(n) => {
115                #[cfg(feature = "fast")]
116                return itoa::Buffer::new().format(*n).to_string();
117                #[cfg(not(feature = "fast"))]
118                n.to_string()
119            }
120            ScalarValue::U128(n) => {
121                #[cfg(feature = "fast")]
122                return itoa::Buffer::new().format(*n).to_string();
123                #[cfg(not(feature = "fast"))]
124                n.to_string()
125            }
126            ScalarValue::F64(n) => {
127                if n.is_nan() {
128                    "#nan".to_string()
129                } else if n.is_infinite() {
130                    if *n > 0.0 {
131                        "#inf".to_string()
132                    } else {
133                        "#-inf".to_string()
134                    }
135                } else {
136                    #[cfg(feature = "fast")]
137                    return zmij::Buffer::new().format(*n).to_string();
138                    #[cfg(not(feature = "fast"))]
139                    n.to_string()
140                }
141            }
142            ScalarValue::Str(s) | ScalarValue::StringlyTyped(s) => {
143                // Return with quotes and proper escaping
144                let mut result = String::with_capacity(s.len() + 2);
145                result.push('"');
146                for c in s.chars() {
147                    match c {
148                        '"' => result.push_str("\\\""),
149                        '\\' => result.push_str("\\\\"),
150                        '\n' => result.push_str("\\n"),
151                        '\r' => result.push_str("\\r"),
152                        '\t' => result.push_str("\\t"),
153                        '\u{0008}' => result.push_str("\\b"), // backspace
154                        '\u{000C}' => result.push_str("\\f"), // form feed
155                        c if c.is_control() => {
156                            // Other control characters as unicode escapes
157                            result.push_str(&format!("\\u{{{:04X}}}", c as u32));
158                        }
159                        _ => result.push(c),
160                    }
161                }
162                result.push('"');
163                result
164            }
165            ScalarValue::Bytes(_) => {
166                // KDL doesn't have native bytes support
167                "\"<bytes>\"".to_string()
168            }
169        }
170    }
171
172    /// Ensure the current struct has an opened brace (for adding children).
173    fn ensure_struct_opened(&mut self) {
174        if let Some(Ctx::Struct {
175            name,
176            opened_brace,
177            properties,
178            arguments,
179        }) = self.stack.last_mut()
180            && !*opened_brace
181        {
182            // Write node name
183            self.out.extend_from_slice(name.as_bytes());
184
185            // Write arguments first
186            for arg in arguments.drain(..) {
187                self.out.push(b' ');
188                self.out.extend_from_slice(arg.as_bytes());
189            }
190
191            // Write properties
192            for (k, v) in properties.drain(..) {
193                self.out.push(b' ');
194                self.out.extend_from_slice(k.as_bytes());
195                self.out.push(b'=');
196                self.out.extend_from_slice(v.as_bytes());
197            }
198
199            // Open brace for children
200            self.out.extend_from_slice(b" {");
201            *opened_brace = true;
202            self.indent_level += 1;
203        }
204    }
205
206    /// Ensure the current sequence wrapper is opened.
207    fn ensure_seq_opened(&mut self) {
208        // Check if we need to open, and get the wrapper name
209        let needs_open = matches!(self.stack.last(), Some(Ctx::Seq { opened: false, .. }));
210
211        if needs_open
212            && let Some(Ctx::Seq {
213                wrapper_name,
214                opened,
215            }) = self.stack.last_mut()
216        {
217            let name = wrapper_name.clone();
218            *opened = true;
219            // Now do the writing without borrowing stack
220            self.out.push(b'\n');
221            self.write_indent();
222            self.out.extend_from_slice(name.as_bytes());
223            self.out.extend_from_slice(b" {");
224            self.indent_level += 1;
225        }
226    }
227}
228
229impl Default for KdlSerializer {
230    fn default() -> Self {
231        Self::new()
232    }
233}
234
235impl FormatSerializer for KdlSerializer {
236    type Error = KdlSerializeError;
237
238    fn struct_metadata(&mut self, shape: &facet_core::Shape) -> Result<(), Self::Error> {
239        // Get the element name (respecting rename attribute, otherwise lowercase type name)
240        let element_name = shape
241            .get_builtin_attr_value::<&str>("rename")
242            .map(|s| s.to_string())
243            .unwrap_or_else(|| to_kebab_case(shape.type_identifier));
244
245        // If this is the root, save the name
246        if let Some(Ctx::Root { name }) = self.stack.last_mut() {
247            *name = Some(element_name);
248        }
249
250        Ok(())
251    }
252
253    fn begin_struct(&mut self) -> Result<(), Self::Error> {
254        // Determine what context we're in
255        enum Action {
256            Root(Option<String>),
257            NestedStruct,
258            SeqItem,
259            NoStack,
260        }
261
262        let action = match self.stack.last_mut() {
263            Some(Ctx::Root { name }) => Action::Root(name.take()),
264            Some(Ctx::Struct { .. }) => Action::NestedStruct,
265            Some(Ctx::Seq { .. }) => Action::SeqItem,
266            None => Action::NoStack,
267        };
268
269        let node_name = match action {
270            Action::Root(name) => name.unwrap_or_else(|| "root".to_string()),
271            Action::NestedStruct => {
272                // Need to ensure parent is opened first
273                self.ensure_struct_opened();
274                self.out.push(b'\n');
275                self.write_indent();
276                // Nested struct - use pending field name
277                self.pending_field
278                    .take()
279                    .unwrap_or_else(|| "node".to_string())
280            }
281            Action::SeqItem => {
282                // Struct inside a sequence - ensure seq wrapper is opened first
283                self.ensure_seq_opened();
284                self.out.push(b'\n');
285                self.write_indent();
286                // Use "item" as the default node name for struct items in sequences
287                "item".to_string()
288            }
289            Action::NoStack => "root".to_string(),
290        };
291
292        self.stack.push(Ctx::Struct {
293            name: node_name,
294            opened_brace: false,
295            properties: Vec::new(),
296            arguments: Vec::new(),
297        });
298
299        Ok(())
300    }
301
302    fn field_key(&mut self, key: &str) -> Result<(), Self::Error> {
303        self.pending_field = Some(key.to_string());
304        // NOTE: Do NOT reset field type flags here - they are set by field_metadata()
305        // which is called BEFORE field_key(). Resetting them would lose the metadata.
306        Ok(())
307    }
308
309    fn end_struct(&mut self) -> Result<(), Self::Error> {
310        match self.stack.pop() {
311            Some(Ctx::Struct {
312                name,
313                opened_brace,
314                arguments,
315                properties,
316            }) => {
317                if opened_brace {
318                    // Had children - close the brace
319                    self.indent_level = self.indent_level.saturating_sub(1);
320                    self.out.push(b'\n');
321                    self.write_indent();
322                    self.out.push(b'}');
323                } else {
324                    // No children - write node with args/props only
325                    self.out.extend_from_slice(name.as_bytes());
326                    for arg in arguments {
327                        self.out.push(b' ');
328                        self.out.extend_from_slice(arg.as_bytes());
329                    }
330                    for (k, v) in properties {
331                        self.out.push(b' ');
332                        self.out.extend_from_slice(k.as_bytes());
333                        self.out.push(b'=');
334                        self.out.extend_from_slice(v.as_bytes());
335                    }
336                }
337                Ok(())
338            }
339            Some(Ctx::Root { name }) => {
340                // Root struct that was never started - write empty node
341                if let Some(n) = name {
342                    self.out.extend_from_slice(n.as_bytes());
343                }
344                Ok(())
345            }
346            _ => Err(KdlSerializeError {
347                msg: "end_struct without matching begin_struct",
348            }),
349        }
350    }
351
352    fn begin_seq(&mut self) -> Result<(), Self::Error> {
353        // Check if we're inside a sequence - if so, this is a nested sequence that
354        // should be wrapped in an "item" node
355        let is_nested_seq = matches!(self.stack.last(), Some(Ctx::Seq { .. }));
356
357        if is_nested_seq {
358            // For nested sequences, open the parent seq first, then wrap in "item { }"
359            self.ensure_seq_opened();
360            self.out.push(b'\n');
361            self.write_indent();
362            self.out.extend_from_slice(b"item {");
363            self.indent_level += 1;
364
365            // Push a Seq context for the inner sequence items
366            self.stack.push(Ctx::Seq {
367                wrapper_name: "item".to_string(), // Already wrote this
368                opened: true,                     // Already opened
369            });
370        } else if self.pending_is_child {
371            // kdl::children - items should be emitted directly as children
372            // without a wrapper node. Just ensure parent struct is opened.
373            self.ensure_struct_opened();
374
375            // Use a special Seq context that won't write a wrapper
376            self.stack.push(Ctx::Seq {
377                wrapper_name: String::new(), // No wrapper
378                opened: true,                // Already "opened" (no wrapper to open)
379            });
380
381            // Clear the pending field - we don't need the field name
382            self.pending_field = None;
383        } else {
384            // Get wrapper name from pending field
385            let wrapper_name = self
386                .pending_field
387                .take()
388                .unwrap_or_else(|| "items".to_string());
389
390            // If we're in a struct, ensure parent brace is opened
391            self.ensure_struct_opened();
392
393            self.stack.push(Ctx::Seq {
394                wrapper_name,
395                opened: false,
396            });
397        }
398        Ok(())
399    }
400
401    fn end_seq(&mut self) -> Result<(), Self::Error> {
402        match self.stack.pop() {
403            Some(Ctx::Seq {
404                opened,
405                wrapper_name,
406            }) => {
407                // Only close brace if we actually wrote a wrapper
408                // (kdl::children has empty wrapper_name and doesn't write a wrapper)
409                if opened && !wrapper_name.is_empty() {
410                    // Close the wrapper brace
411                    self.indent_level = self.indent_level.saturating_sub(1);
412                    self.out.push(b'\n');
413                    self.write_indent();
414                    self.out.push(b'}');
415                }
416                Ok(())
417            }
418            _ => Err(KdlSerializeError {
419                msg: "end_seq without matching begin_seq",
420            }),
421        }
422    }
423
424    fn scalar(&mut self, scalar: ScalarValue<'_>) -> Result<(), Self::Error> {
425        let value_str = self.scalar_to_string(&scalar);
426
427        match self.stack.last_mut() {
428            Some(Ctx::Struct {
429                opened_brace,
430                properties,
431                arguments,
432                ..
433            }) => {
434                if self.pending_is_property {
435                    // KDL property: buffer it
436                    if let Some(key) = self.pending_field.take() {
437                        properties.push((key, value_str));
438                    }
439                } else if self.pending_is_argument {
440                    // KDL argument: buffer it
441                    arguments.push(value_str);
442                    self.pending_field = None;
443                } else if self.pending_is_child || *opened_brace {
444                    // Child node with scalar value
445                    // Ensure struct is opened
446                    if !*opened_brace {
447                        self.ensure_struct_opened();
448                    }
449                    self.out.push(b'\n');
450                    self.write_indent();
451                    if let Some(key) = self.pending_field.take() {
452                        self.out.extend_from_slice(key.as_bytes());
453                        self.out.push(b' ');
454                    }
455                    self.out.extend_from_slice(value_str.as_bytes());
456                } else {
457                    // Default for fields without attributes: emit as child node
458                    self.ensure_struct_opened();
459                    self.out.push(b'\n');
460                    self.write_indent();
461                    if let Some(key) = self.pending_field.take() {
462                        self.out.extend_from_slice(key.as_bytes());
463                        self.out.push(b' ');
464                    }
465                    self.out.extend_from_slice(value_str.as_bytes());
466                }
467            }
468            Some(Ctx::Seq { .. }) => {
469                // Sequence item - ensure wrapper is opened, then write item node
470                self.ensure_seq_opened();
471                self.out.push(b'\n');
472                self.write_indent();
473                self.out.extend_from_slice(b"item ");
474                self.out.extend_from_slice(value_str.as_bytes());
475            }
476            Some(Ctx::Root { .. }) | None => {
477                // Top level scalar - write as value node
478                self.out.extend_from_slice(b"value ");
479                self.out.extend_from_slice(value_str.as_bytes());
480            }
481        }
482
483        self.pending_field = None;
484        Ok(())
485    }
486
487    fn field_metadata(&mut self, field: &facet_reflect::FieldItem) -> Result<(), Self::Error> {
488        // For flattened map entries (field is None), treat as properties
489        let Some(field_def) = field.field else {
490            self.pending_is_property = true;
491            self.pending_is_argument = false;
492            self.pending_is_child = false;
493            return Ok(());
494        };
495
496        // Check for kdl-specific attributes
497        self.pending_is_property = field_def.get_attr(Some("kdl"), "property").is_some();
498        self.pending_is_argument = field_def.get_attr(Some("kdl"), "argument").is_some()
499            || field_def.get_attr(Some("kdl"), "arguments").is_some();
500        self.pending_is_child = field_def.get_attr(Some("kdl"), "child").is_some()
501            || field_def.get_attr(Some("kdl"), "children").is_some();
502        Ok(())
503    }
504}
505
506/// Convert a PascalCase type name to a lowercase name suitable for KDL nodes.
507fn to_kebab_case(s: &str) -> String {
508    // Simple approach: just lowercase the whole thing
509    s.to_lowercase()
510}
511
512/// Serialize a value to KDL bytes.
513pub fn to_vec<'facet, T>(value: &T) -> Result<Vec<u8>, SerializeError<KdlSerializeError>>
514where
515    T: Facet<'facet> + ?Sized,
516{
517    let mut serializer = KdlSerializer::new();
518    serialize_root(&mut serializer, Peek::new(value))?;
519    Ok(serializer.finish())
520}
521
522/// Serialize a value to a KDL string.
523pub fn to_string<'facet, T>(value: &T) -> Result<String, SerializeError<KdlSerializeError>>
524where
525    T: Facet<'facet> + ?Sized,
526{
527    let bytes = to_vec(value)?;
528    Ok(String::from_utf8(bytes).expect("KDL output should always be valid UTF-8"))
529}