Skip to main content

yaml_lib/stringify/
serializer.rs

1//! Custom Serializer Trait & Implementations
2//!
3//! Defines traits and implementations for custom serialization logic for YAML nodes.
4//! Allows users to provide custom serializers for specific node types and output formats.
5//!
6//! Copyright (c) 2026 YAML Library Developers
7
8use alloc::boxed::Box;
9use alloc::string::String;
10use alloc::vec::Vec;
11
12use crate::io::traits::IDestination;
13use crate::nodes::node::Node;
14use crate::stringify::format::{FormatContext, FormatOptions};
15
16/// Result type for serialization operations
17pub type SerializeResult = Result<String, String>;
18
19/// Result type for streaming serialization operations
20pub type StreamResult = Result<(), String>;
21
22/// Trait for custom node serializers
23pub trait Serializer {
24    /// Check if this serializer can handle the given node
25    fn can_serialize(&self, node: &Node) -> bool;
26
27    /// Serialize the node to a string
28    fn serialize(
29        &self,
30        node: &Node,
31        options: &FormatOptions,
32        context: &FormatContext,
33    ) -> SerializeResult;
34
35    /// Get serializer priority (higher = checked first)
36    fn priority(&self) -> i32 {
37        0
38    }
39}
40
41/// Trait implemented by concrete format writers (JSON, XML, TOML, bencode).
42///
43/// This trait focuses on **how** individual node types are emitted, while the
44/// traversal over the `Node` tree is handled by `walk_node` below. Backends
45/// that previously performed their own recursive descent can now delegate that
46/// logic to the shared walker while keeping their output identical.
47pub trait FormatWriter {
48    /// Underlying output destination
49    #[allow(dead_code)]
50    fn dest(&mut self) -> &mut dyn IDestination;
51
52    /// Emit a YAML `null` value
53    fn write_null(&mut self) -> StreamResult;
54
55    /// Emit a boolean value
56    fn write_bool(&mut self, value: bool) -> StreamResult;
57
58    /// Emit a numeric node
59    fn write_number(&mut self, num: &crate::nodes::node::Numeric) -> StreamResult;
60
61    /// Emit a string scalar
62    fn write_string(&mut self, value: &str) -> StreamResult;
63
64    /// Emit an array-like collection
65    fn start_array(&mut self, len: usize) -> StreamResult;
66    fn array_value_separator(&mut self, index: usize) -> StreamResult;
67    fn end_array(&mut self) -> StreamResult;
68
69    /// Emit a set-like collection; many formats treat sets like arrays but
70    /// some (e.g. XML) may want a different representation.
71    fn start_set(&mut self, len: usize) -> StreamResult;
72    fn set_value_separator(&mut self, index: usize) -> StreamResult;
73    fn end_set(&mut self) -> StreamResult;
74
75    /// Emit a mapping/dictionary-like collection
76    fn start_mapping(&mut self, len: usize) -> StreamResult;
77    fn write_mapping_key(&mut self, key: &Node) -> StreamResult;
78    fn mapping_key_value_separator(&mut self) -> StreamResult;
79    fn mapping_entry_separator(&mut self, index: usize) -> StreamResult;
80    fn end_mapping(&mut self) -> StreamResult;
81
82    /// Emit a standalone comment node if the format supports it. The default
83    /// implementation is a no-op so formats that ignore comments can skip it.
84    fn write_comment(&mut self, _comment: &str) -> StreamResult {
85        Ok(())
86    }
87
88    /// Begin a logical document wrapper when serializing `Node::Document` or
89    /// `Node::Documents` containers. Most formats either inline or treat these
90    /// as arrays and can leave the default no-op implementation.
91    fn start_document(&mut self, _index: usize, _total: usize) -> StreamResult {
92        Ok(())
93    }
94
95    /// End a logical document wrapper.
96    fn end_document(&mut self, _index: usize, _total: usize) -> StreamResult {
97        Ok(())
98    }
99}
100
101/// Shared recursive walker over `Node` trees.
102///
103/// This function centralizes the traversal logic used by all stringify
104/// backends. Format-specific behavior is expressed through the `FormatWriter`
105/// trait; the order in which nodes are visited matches the legacy per-backend
106/// implementations so existing byte-level expectations remain valid.
107pub fn walk_node<W: FormatWriter>(writer: &mut W, node: &Node) -> StreamResult {
108    use crate::nodes::node::Node as N;
109
110    match node {
111        N::None => writer.write_null(),
112        N::Boolean(b) => writer.write_bool(*b),
113        N::Str(s, _qt, _style) => writer.write_string(s),
114        N::Number(num) => writer.write_number(num),
115        N::Array(items) => {
116            writer.start_array(items.len())?;
117            for (i, it) in items.iter().enumerate() {
118                if i > 0 {
119                    writer.array_value_separator(i)?;
120                }
121                walk_node(writer, it)?;
122            }
123            writer.end_array()
124        }
125        N::Set(items) => {
126            writer.start_set(items.len())?;
127            for (i, it) in items.iter().enumerate() {
128                if i > 0 {
129                    writer.set_value_separator(i)?;
130                }
131                walk_node(writer, it)?;
132            }
133            writer.end_set()
134        }
135        N::Mapping(pairs) => {
136            writer.start_mapping(pairs.len())?;
137            for (idx, (k, v)) in pairs.iter().enumerate() {
138                if idx > 0 {
139                    writer.mapping_entry_separator(idx)?;
140                }
141                writer.write_mapping_key(k)?;
142                writer.mapping_key_value_separator()?;
143                walk_node(writer, v)?;
144            }
145            writer.end_mapping()
146        }
147        N::Document(nodes) => {
148            if nodes.len() == 1 {
149                writer.start_document(0, 1)?;
150                walk_node(writer, &nodes[0])?;
151                writer.end_document(0, 1)
152            } else {
153                for (i, n) in nodes.iter().enumerate() {
154                    writer.start_document(i, nodes.len())?;
155                    walk_node(writer, n)?;
156                    writer.end_document(i, nodes.len())?;
157                }
158                Ok(())
159            }
160        }
161        N::Tagged(inner, _tag) => walk_node(writer, inner),
162        N::Anchored(inner, _name) => walk_node(writer, inner),
163        N::Alias(_name) => writer.write_null(),
164        N::Documents(docs) => {
165            for (i, d) in docs.iter().enumerate() {
166                writer.start_document(i, docs.len())?;
167                walk_node(writer, d)?;
168                writer.end_document(i, docs.len())?;
169            }
170            Ok(())
171        }
172        N::Comment(c) => writer.write_comment(c),
173    }
174}
175
176/// Serializer for custom tagged nodes
177pub struct TaggedSerializer {
178    tag: String,
179    serialize_fn: Box<dyn Fn(&Node, &FormatOptions, &FormatContext) -> SerializeResult>,
180}
181
182impl TaggedSerializer {
183    pub fn new<F>(tag: impl Into<String>, serialize_fn: F) -> Self
184    where
185        F: Fn(&Node, &FormatOptions, &FormatContext) -> SerializeResult + 'static,
186    {
187        Self {
188            tag: tag.into(),
189            serialize_fn: Box::new(serialize_fn),
190        }
191    }
192}
193
194impl Serializer for TaggedSerializer {
195    fn can_serialize(&self, node: &Node) -> bool {
196        match node {
197            Node::Tagged(_, tag) => tag == &self.tag,
198            _ => false,
199        }
200    }
201
202    fn serialize(
203        &self,
204        node: &Node,
205        options: &FormatOptions,
206        context: &FormatContext,
207    ) -> SerializeResult {
208        (self.serialize_fn)(node, options, context)
209    }
210
211    fn priority(&self) -> i32 {
212        10 // Tagged serializers get higher priority
213    }
214}
215
216/// Serializer for specific node types
217pub struct TypeSerializer {
218    serialize_fn: Box<dyn Fn(&Node, &FormatOptions, &FormatContext) -> SerializeResult>,
219    can_serialize_fn: Box<dyn Fn(&Node) -> bool>,
220}
221
222impl TypeSerializer {
223    pub fn new<F, C>(can_serialize: C, serialize_fn: F) -> Self
224    where
225        F: Fn(&Node, &FormatOptions, &FormatContext) -> SerializeResult + 'static,
226        C: Fn(&Node) -> bool + 'static,
227    {
228        Self {
229            serialize_fn: Box::new(serialize_fn),
230            can_serialize_fn: Box::new(can_serialize),
231        }
232    }
233}
234
235impl Serializer for TypeSerializer {
236    fn can_serialize(&self, node: &Node) -> bool {
237        (self.can_serialize_fn)(node)
238    }
239
240    fn serialize(
241        &self,
242        node: &Node,
243        options: &FormatOptions,
244        context: &FormatContext,
245    ) -> SerializeResult {
246        (self.serialize_fn)(node, options, context)
247    }
248}
249
250/// Registry for custom serializers
251pub struct SerializerRegistry {
252    serializers: Vec<Box<dyn Serializer>>,
253}
254
255impl SerializerRegistry {
256    pub fn new() -> Self {
257        Self {
258            serializers: alloc::vec::Vec::new(),
259        }
260    }
261
262    /// Register a custom serializer
263    pub fn register(&mut self, serializer: Box<dyn Serializer>) {
264        self.serializers.push(serializer);
265        // Sort by priority (descending)
266        self.serializers
267            .sort_by(|a, b| b.priority().cmp(&a.priority()));
268    }
269
270    /// Find a serializer for the given node
271    pub fn find_serializer(&self, node: &Node) -> Option<&dyn Serializer> {
272        self.serializers
273            .iter()
274            .find(|s| s.can_serialize(node))
275            .map(|s| s.as_ref())
276    }
277
278    /// Serialize using registered serializers, fallback to default
279    pub fn serialize<F>(
280        &self,
281        node: &Node,
282        options: &FormatOptions,
283        context: &FormatContext,
284        default: F,
285    ) -> SerializeResult
286    where
287        F: FnOnce(&Node, &FormatOptions, &FormatContext) -> SerializeResult,
288    {
289        if let Some(serializer) = self.find_serializer(node) {
290            serializer.serialize(node, options, context)
291        } else {
292            default(node, options, context)
293        }
294    }
295}
296
297impl Default for SerializerRegistry {
298    fn default() -> Self {
299        Self::new()
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306    use crate::nodes::node::Numeric;
307
308    #[test]
309    fn test_tagged_serializer() {
310        let serializer = TaggedSerializer::new("!custom", |node, _opts, _ctx| match node {
311            Node::Tagged(inner, _) => {
312                if let Node::Str(s, _, _) = &**inner {
313                    Ok(format!("CUSTOM: {}", s))
314                } else {
315                    Err("Expected string".to_string())
316                }
317            }
318            _ => Err("Not a tagged node".to_string()),
319        });
320
321        let node = Node::Tagged(Box::new(Node::from("test")), "!custom".to_string());
322
323        assert!(serializer.can_serialize(&node));
324
325        let opts = FormatOptions::default();
326        let ctx = FormatContext::new();
327        let result = serializer.serialize(&node, &opts, &ctx).unwrap();
328        assert_eq!(result, "CUSTOM: test");
329    }
330
331    #[test]
332    fn test_type_serializer() {
333        let serializer = TypeSerializer::new(
334            |node| matches!(node, Node::Number(Numeric::Integer(n)) if *n > 1000),
335            |node, _opts, _ctx| {
336                if let Node::Number(Numeric::Integer(n)) = node {
337                    Ok(format!("{}K", n / 1000))
338                } else {
339                    Err("Not a large integer".to_string())
340                }
341            },
342        );
343
344        let small = Node::Number(Numeric::Integer(100));
345        let large = Node::Number(Numeric::Integer(5000));
346
347        assert!(!serializer.can_serialize(&small));
348        assert!(serializer.can_serialize(&large));
349
350        let opts = FormatOptions::default();
351        let ctx = FormatContext::new();
352        let result = serializer.serialize(&large, &opts, &ctx).unwrap();
353        assert_eq!(result, "5K");
354    }
355
356    #[test]
357    fn test_serializer_registry() {
358        let mut registry = SerializerRegistry::new();
359
360        // Register a custom serializer for large integers
361        registry.register(Box::new(TypeSerializer::new(
362            |node| matches!(node, Node::Number(Numeric::Integer(n)) if *n > 1000),
363            |node, _opts, _ctx| {
364                if let Node::Number(Numeric::Integer(n)) = node {
365                    Ok(format!("{}K", n / 1000))
366                } else {
367                    Err("Not a large integer".to_string())
368                }
369            },
370        )));
371
372        let large = Node::Number(Numeric::Integer(5000));
373        let small = Node::Number(Numeric::Integer(100));
374
375        let opts = FormatOptions::default();
376        let ctx = FormatContext::new();
377
378        // Should use custom serializer
379        let result = registry
380            .serialize(&large, &opts, &ctx, |_, _, _| Ok("DEFAULT".to_string()))
381            .unwrap();
382        assert_eq!(result, "5K");
383
384        // Should use default
385        let result = registry
386            .serialize(&small, &opts, &ctx, |_, _, _| Ok("DEFAULT".to_string()))
387            .unwrap();
388        assert_eq!(result, "DEFAULT");
389    }
390
391    #[test]
392    fn test_priority_ordering() {
393        let mut registry = SerializerRegistry::new();
394
395        // Register low priority serializer
396        registry.register(Box::new(TypeSerializer::new(
397            |_| true,
398            |_, _, _| Ok("LOW".to_string()),
399        )));
400
401        // Register high priority tagged serializer
402        registry.register(Box::new(TaggedSerializer::new("!test", |_, _, _| {
403            Ok("HIGH".to_string())
404        })));
405
406        let node = Node::Tagged(Box::new(Node::from("value")), "!test".to_string());
407
408        let opts = FormatOptions::default();
409        let ctx = FormatContext::new();
410
411        // Should use high priority serializer
412        let result = registry
413            .serialize(&node, &opts, &ctx, |_, _, _| Ok("DEFAULT".to_string()))
414            .unwrap();
415        assert_eq!(result, "HIGH");
416    }
417}