Skip to main content

toon_format_rs/
serializer.rs

1use crate::error::Result;
2use crate::value::Value;
3use indexmap::IndexMap;
4use std::io::Write;
5
6/// Options for serializing TOON
7#[derive(Debug, Clone)]
8pub struct SerializeOptions {
9    /// Number of spaces per indentation level (default: 2)
10    pub indent_size: usize,
11    /// Default delimiter for inline arrays (default: comma)
12    pub delimiter: Delimiter,
13    /// Whether to use tabular format for uniform arrays of objects
14    pub use_tabular: bool,
15    /// Maximum number of primitive items before switching to expanded array
16    pub max_inline_items: usize,
17}
18
19impl Default for SerializeOptions {
20    fn default() -> Self {
21        SerializeOptions {
22            indent_size: 2,
23            delimiter: Delimiter::Comma,
24            use_tabular: true,
25            max_inline_items: 10,
26        }
27    }
28}
29
30/// Delimiter types for arrays
31#[derive(Debug, Clone, Copy, PartialEq)]
32pub enum Delimiter {
33    /// Comma (,)
34    Comma,
35    /// Tab (\t)
36    Tab,
37    /// Pipe (|)
38    Pipe,
39}
40
41impl Delimiter {
42    /// Returns the delimiter character
43    pub fn as_char(&self) -> char {
44        match self {
45            Delimiter::Comma => ',',
46            Delimiter::Tab => '\t',
47            Delimiter::Pipe => '|',
48        }
49    }
50
51    /// Returns the delimiter as a string
52    pub fn as_str(&self) -> &'static str {
53        match self {
54            Delimiter::Comma => ",",
55            Delimiter::Tab => "\t",
56            Delimiter::Pipe => "|",
57        }
58    }
59}
60
61/// Serializes a Value into TOON format
62pub struct Serializer<'a> {
63    options: &'a SerializeOptions,
64    output: Vec<u8>,
65}
66
67impl<'a> Serializer<'a> {
68    /// Creates a new serializer with the given options
69    pub fn new(options: &'a SerializeOptions) -> Self {
70        Serializer {
71            options,
72            output: Vec::new(),
73        }
74    }
75
76    /// Serializes the value and returns the TOON string
77    pub fn serialize(mut self, value: &Value) -> Result<String> {
78        self.write_value(value, 0)?;
79        String::from_utf8(self.output).map_err(|_| {
80            crate::error::Error::InvalidUtf8
81        })
82    }
83
84    /// Writes a value at the given indentation depth
85    fn write_value(&mut self, value: &Value, depth: usize) -> Result<()> {
86        match value {
87            Value::Null => {
88                write!(self.output, "null")?;
89            }
90            Value::Bool(true) => {
91                write!(self.output, "true")?;
92            }
93            Value::Bool(false) => {
94                write!(self.output, "false")?;
95            }
96            Value::Number(n) => {
97                write!(self.output, "{}", n)?;
98            }
99            Value::String(s) => {
100                self.write_string(s)?;
101            }
102            Value::Array(arr) => {
103                self.write_array(arr, depth)?;
104            }
105            Value::Object(obj) => {
106                self.write_object(obj, depth)?;
107            }
108        }
109        Ok(())
110    }
111
112    /// Writes a string with quoting only when necessary
113    fn write_string(&mut self, s: &str) -> Result<()> {
114        if self.needs_quoting(s) {
115            write!(self.output, "\"{}\"", self.escape_string(s))?;
116        } else {
117            write!(self.output, "{}", s)?;
118        }
119        Ok(())
120    }
121
122    /// Checks if a string needs quoting
123    fn needs_quoting(&self, s: &str) -> bool {
124        if s.is_empty() {
125            return true;
126        }
127
128        // Check if it looks like a number, boolean, or null
129        if s == "true" || s == "false" || s == "null" {
130            return true;
131        }
132
133        if self.is_numeric(s) {
134            return true;
135        }
136
137        // Check for special characters or spaces
138        let delimiter = self.options.delimiter.as_char();
139        let chars_to_quote = [':', ',', '[', ']', '{', '}', '\n', '\r', '\t', '"', '\'', delimiter];
140
141        s.starts_with(' ')
142            || s.ends_with(' ')
143            || s.starts_with('\t')
144            || s.ends_with('\t')
145            || s.chars().any(|c| chars_to_quote.contains(&c))
146    }
147
148    /// Checks if a string looks like a number
149    fn is_numeric(&self, s: &str) -> bool {
150        if s.is_empty() {
151            return false;
152        }
153
154        let mut chars = s.chars().peekable();
155
156        // Optional sign
157        if let Some('-') = chars.peek() {
158            chars.next();
159        }
160
161        // Integer part
162        let mut has_digits = false;
163        while let Some(c) = chars.peek() {
164            if c.is_ascii_digit() {
165                has_digits = true;
166                chars.next();
167            } else {
168                break;
169            }
170        }
171
172        if !has_digits {
173            return false;
174        }
175
176        // Optional decimal part
177        if let Some('.') = chars.peek() {
178            chars.next();
179            let mut frac_digits = false;
180            while let Some(c) = chars.peek() {
181                if c.is_ascii_digit() {
182                    frac_digits = true;
183                    chars.next();
184                } else {
185                    break;
186                }
187            }
188            if !frac_digits {
189                return false;
190            }
191        }
192
193        // Optional exponent
194        if let Some('e') | Some('E') = chars.peek() {
195            chars.next();
196            if let Some('+') | Some('-') = chars.peek() {
197                chars.next();
198            }
199            let mut exp_digits = false;
200            while let Some(c) = chars.peek() {
201                if c.is_ascii_digit() {
202                    exp_digits = true;
203                    chars.next();
204                } else {
205                    break;
206                }
207            }
208            if !exp_digits {
209                return false;
210            }
211        }
212
213        chars.peek().is_none()
214    }
215
216    /// Escapes special characters in a string
217    fn escape_string(&self, s: &str) -> String {
218        s.replace('\\', "\\\\")
219            .replace('"', "\\\"")
220            .replace('\n', "\\n")
221            .replace('\r', "\\r")
222            .replace('\t', "\\t")
223    }
224
225    /// Writes an array
226    fn write_array(&mut self, arr: &[Value], depth: usize) -> Result<()> {
227        if arr.is_empty() {
228            // Write empty array as []
229            write!(self.output, "[]")?;
230            return Ok(());
231        }
232
233        // Check if all elements are objects with the same keys (uniform)
234        if self.options.use_tabular && self.is_uniform_object_array(arr) {
235            self.write_tabular_array(arr, depth)?;
236        } else if arr.len() <= self.options.max_inline_items
237            && arr.iter().all(|v| self.is_primitive(v))
238        {
239            // Inline primitive array
240            self.write_inline_primitive_array(arr)?;
241        } else {
242            // Expanded array with hyphens
243            self.write_expanded_array(arr, depth)?;
244        }
245
246        Ok(())
247    }
248
249    /// Checks if all elements are objects with identical keys
250    fn is_uniform_object_array(&self, arr: &[Value]) -> bool {
251        if arr.len() < 2 {
252            return false;
253        }
254
255        let first = match &arr[0] {
256            Value::Object(obj) => obj,
257            _ => return false,
258        };
259
260        let keys: Vec<_> = first.keys().collect();
261
262        for item in arr.iter().skip(1) {
263            match item {
264                Value::Object(obj) => {
265                    if obj.len() != keys.len() {
266                        return false;
267                    }
268                    for (i, key) in keys.iter().enumerate() {
269                        if obj.keys().nth(i) != Some(*key) {
270                            return false;
271                        }
272                    }
273                }
274                _ => return false,
275            }
276        }
277
278        true
279    }
280
281    /// Checks if a value is a primitive (not object or array)
282    fn is_primitive(&self, value: &Value) -> bool {
283        !matches!(value, Value::Object(_) | Value::Array(_))
284    }
285
286    /// Writes a tabular array
287    fn write_tabular_array(&mut self, arr: &[Value], depth: usize) -> Result<()> {
288        self.write_expanded_array(arr, depth)
289    }
290
291    /// Writes an expanded array with hyphens
292    fn write_expanded_array(&mut self, arr: &[Value], depth: usize) -> Result<()> {
293        let indent = self.make_indent(depth);
294
295        for item in arr {
296            write!(self.output, "\n{}", indent)?;
297            write!(self.output, "- ")?;
298            self.write_value_inline(item)?;
299        }
300        Ok(())
301    }
302
303    /// Writes an inline primitive array
304    fn write_inline_primitive_array(&mut self, arr: &[Value]) -> Result<()> {
305        let delim = self.options.delimiter.as_str();
306
307        for (i, item) in arr.iter().enumerate() {
308            if i > 0 {
309                write!(self.output, "{}", delim)?;
310            }
311            self.write_value_inline(item)?;
312        }
313        Ok(())
314    }
315
316    /// Writes a value inline (without indentation)
317    fn write_value_inline(&mut self, value: &Value) -> Result<()> {
318        match value {
319            Value::Null => {
320                write!(self.output, "null")?;
321            }
322            Value::Bool(true) => {
323                write!(self.output, "true")?;
324            }
325            Value::Bool(false) => {
326                write!(self.output, "false")?;
327            }
328            Value::Number(n) => {
329                write!(self.output, "{}", n)?;
330            }
331            Value::String(s) => {
332                self.write_string(s)?;
333            }
334            Value::Array(arr) => {
335                write!(self.output, "[")?;
336                for (i, item) in arr.iter().enumerate() {
337                    if i > 0 {
338                        write!(self.output, ", ")?;
339                    }
340                    self.write_value_inline(item)?;
341                }
342                write!(self.output, "]")?;
343            }
344            Value::Object(obj) => {
345                write!(self.output, "{{")?;
346                for (i, (k, v)) in obj.iter().enumerate() {
347                    if i > 0 {
348                        write!(self.output, ", ")?;
349                    }
350                    self.write_string(k)?;
351                    write!(self.output, ": ")?;
352                    self.write_value_inline(v)?;
353                }
354                write!(self.output, "}}")?;
355            }
356        }
357        Ok(())
358    }
359
360    /// Writes an object
361    fn write_object(&mut self, obj: &IndexMap<String, Value>, depth: usize) -> Result<()> {
362        if obj.is_empty() {
363            return Ok(());
364        }
365
366        let indent = self.make_indent(depth);
367
368        for (i, (key, value)) in obj.iter().enumerate() {
369            if i > 0 || depth > 0 {
370                write!(self.output, "\n")?;
371            }
372            write!(self.output, "{}", indent)?;
373
374            // Check if value is a uniform array of objects (for tabular format)
375            let is_tabular = if let Value::Array(arr) = value {
376                self.options.use_tabular && self.is_uniform_object_array(arr)
377            } else {
378                false
379            };
380
381            self.write_string(key)?;
382
383            if is_tabular {
384                if let Value::Array(arr) = value {
385                    self.write_tabular_array_header(arr, depth)?;
386                }
387            } else {
388                write!(self.output, ":")?;
389
390                if let Value::Array(arr) = value {
391                    if arr.is_empty() {
392                        write!(self.output, " []")?;
393                    } else if arr.len() <= self.options.max_inline_items
394                        && arr.iter().all(|v| self.is_primitive(v))
395                    {
396                        // Inline array header
397                        write!(self.output, " [{}]: ", arr.len())?;
398                        self.write_inline_primitive_array(arr)?;
399                    } else {
400                        self.write_expanded_array_value(arr, depth + 1)?;
401                    }
402                } else if value.is_object() {
403                    self.write_nested_object(value, depth + 1)?;
404                } else {
405                    write!(self.output, " ")?;
406                    self.write_value_inline(value)?;
407                }
408            }
409        }
410
411        Ok(())
412    }
413
414    /// Writes a tabular array header and rows
415    fn write_tabular_array_header(&mut self, arr: &[Value], depth: usize) -> Result<()> {
416        let first = arr[0].as_object().unwrap();
417        let keys: Vec<_> = first.keys().cloned().collect();
418        let length = arr.len();
419
420        write!(self.output, "[{}]{{", length)?;
421        for (i, key) in keys.iter().enumerate() {
422            if i > 0 {
423                write!(self.output, ",")?;
424            }
425            write!(self.output, "{}", key)?;
426        }
427        write!(self.output, "}}:")?;
428
429        // Write rows
430        let row_indent = self.make_indent(depth + 1);
431        let delim = self.options.delimiter.as_str();
432
433        for row in arr {
434            write!(self.output, "\n{}", row_indent)?;
435            let obj = row.as_object().unwrap();
436            for (i, key) in keys.iter().enumerate() {
437                if i > 0 {
438                    write!(self.output, "{}", delim)?;
439                }
440                let val = obj.get(key).unwrap();
441                self.write_value_inline(val)?;
442            }
443        }
444        Ok(())
445    }
446
447    /// Writes an expanded array as a value (with proper indentation)
448    fn write_expanded_array_value(&mut self, arr: &[Value], depth: usize) -> Result<()> {
449        let indent = self.make_indent(depth);
450
451        for item in arr {
452            write!(self.output, "\n{}", indent)?;
453            write!(self.output, "- ")?;
454            self.write_value_inline(item)?;
455        }
456        Ok(())
457    }
458
459    /// Writes a nested object
460    fn write_nested_object(&mut self, value: &Value, depth: usize) -> Result<()> {
461        if let Value::Object(obj) = value {
462            let indent = self.make_indent(depth);
463
464            for (_i, (key, val)) in obj.iter().enumerate() {
465                write!(self.output, "\n{}", indent)?;
466                self.write_string(key)?;
467                write!(self.output, ":")?;
468
469                if val.is_object() {
470                    self.write_nested_object(val, depth + 1)?;
471                } else if let Value::Array(arr) = val {
472                    if arr.is_empty() {
473                        write!(self.output, " []")?;
474                    } else if arr.len() <= self.options.max_inline_items
475                        && arr.iter().all(|v| self.is_primitive(v))
476                    {
477                        write!(self.output, " [{}]: ", arr.len())?;
478                        self.write_inline_primitive_array(arr)?;
479                    } else {
480                        self.write_expanded_array_value(arr, depth + 1)?;
481                    }
482                } else {
483                    write!(self.output, " ")?;
484                    self.write_value_inline(val)?;
485                }
486            }
487        }
488        Ok(())
489    }
490
491    /// Creates an indentation string
492    fn make_indent(&self, depth: usize) -> String {
493        " ".repeat(depth * self.options.indent_size)
494    }
495}
496
497/// Serializes a Value into TOON string with default options
498pub fn to_string(value: &Value) -> Result<String> {
499    let options = SerializeOptions::default();
500    let serializer = Serializer::new(&options);
501    serializer.serialize(value)
502}
503
504/// Serializes a Value into TOON string with custom options
505pub fn to_string_pretty(value: &Value, options: &SerializeOptions) -> Result<String> {
506    let serializer = Serializer::new(options);
507    serializer.serialize(value)
508}
509
510#[cfg(test)]
511mod tests {
512    use super::*;
513    use crate::value::Value;
514    use indexmap::IndexMap;
515
516    #[test]
517    fn test_serialize_simple_object() {
518        let mut obj = IndexMap::new();
519        obj.insert("id".to_string(), Value::integer(123));
520        obj.insert("name".to_string(), Value::string("Alice"));
521        obj.insert("active".to_string(), Value::Bool(true));
522
523        let value = Value::Object(obj);
524        let result = to_string(&value).unwrap();
525
526        assert!(result.contains("id: 123"));
527        assert!(result.contains("name: Alice"));
528        assert!(result.contains("active: true"));
529    }
530
531    #[test]
532    fn test_serialize_array_inline() {
533        let value = Value::Array(vec![
534            Value::string("foo"),
535            Value::string("bar"),
536            Value::string("baz"),
537        ]);
538
539        let result = to_string(&value).unwrap();
540        assert_eq!(result, "foo,bar,baz");
541    }
542
543    #[test]
544    fn test_serialize_tabular_array() {
545        let mut row1 = IndexMap::new();
546        row1.insert("id".to_string(), Value::integer(1));
547        row1.insert("name".to_string(), Value::string("Alice"));
548        row1.insert("role".to_string(), Value::string("admin"));
549
550        let mut row2 = IndexMap::new();
551        row2.insert("id".to_string(), Value::integer(2));
552        row2.insert("name".to_string(), Value::string("Bob"));
553        row2.insert("role".to_string(), Value::string("user"));
554
555        let mut obj = IndexMap::new();
556        obj.insert(
557            "users".to_string(),
558            Value::Array(vec![Value::Object(row1), Value::Object(row2)]),
559        );
560
561        let value = Value::Object(obj);
562        let result = to_string(&value).unwrap();
563
564        assert!(result.contains("users[2]{id,name,role}:"));
565        assert!(result.contains("1,Alice,admin"));
566        assert!(result.contains("2,Bob,user"));
567    }
568
569    #[test]
570    fn test_string_quoting() {
571        let mut obj = IndexMap::new();
572        obj.insert("msg".to_string(), Value::string("hello, world"));
573        obj.insert("path".to_string(), Value::string("/home/user"));
574
575        let value = Value::Object(obj);
576        let result = to_string(&value).unwrap();
577
578        assert!(result.contains("\"hello, world\""));
579        assert!(result.contains("path: /home/user"));
580    }
581
582    #[test]
583    fn test_serialize_nested_object() {
584        let mut inner = IndexMap::new();
585        inner.insert("id".to_string(), Value::integer(1));
586        inner.insert("name".to_string(), Value::string("Alice"));
587
588        let mut obj = IndexMap::new();
589        obj.insert("user".to_string(), Value::Object(inner));
590
591        let value = Value::Object(obj);
592        let result = to_string(&value).unwrap();
593
594        assert!(result.contains("user:"));
595        assert!(result.contains("  id: 1"));
596        assert!(result.contains("  name: Alice"));
597    }
598}