toon_format/encode/
mod.rs

1//! Encoder Implementation
2pub mod folding;
3pub mod primitives;
4pub mod writer;
5use indexmap::IndexMap;
6
7use crate::{
8    constants::MAX_DEPTH,
9    types::{
10        EncodeOptions,
11        IntoJsonValue,
12        JsonValue as Value,
13        KeyFoldingMode,
14        ToonError,
15        ToonResult,
16    },
17    utils::{
18        format_canonical_number,
19        normalize,
20        validation::validate_depth,
21        QuotingContext,
22    },
23};
24
25/// Encode any serializable value to TOON format.
26///
27/// This function accepts any type implementing `serde::Serialize`, including:
28/// - Custom structs with `#[derive(Serialize)]`
29/// - `serde_json::Value`
30/// - Built-in types (Vec, HashMap, etc.)
31///
32/// # Examples
33///
34/// **With custom structs:**
35/// ```
36/// use serde::Serialize;
37/// use toon_format::{
38///     encode,
39///     EncodeOptions,
40/// };
41///
42/// #[derive(Serialize)]
43/// struct User {
44///     name: String,
45///     age: u32,
46/// }
47///
48/// let user = User {
49///     name: "Alice".to_string(),
50///     age: 30,
51/// };
52/// let toon = encode(&user, &EncodeOptions::default())?;
53/// assert!(toon.contains("name: Alice"));
54/// # Ok::<(), toon_format::ToonError>(())
55/// ```
56///
57/// **With JSON values:**
58/// ```
59/// use toon_format::{encode, EncodeOptions, Delimiter};
60/// use serde_json::json;
61///
62/// let data = json!({"tags": ["a", "b", "c"]});
63/// let options = EncodeOptions::new().with_delimiter(Delimiter::Pipe);
64/// let toon = encode(&data, &options)?;
65/// assert!(toon.contains("|"));
66/// # Ok::<(), toon_format::ToonError>(())
67/// ```
68pub fn encode<T: serde::Serialize>(value: &T, options: &EncodeOptions) -> ToonResult<String> {
69    let json_value =
70        serde_json::to_value(value).map_err(|e| ToonError::SerializationError(e.to_string()))?;
71    let json_value: Value = json_value.into();
72    encode_impl(&json_value, options)
73}
74
75fn encode_impl(value: &Value, options: &EncodeOptions) -> ToonResult<String> {
76    let normalized: Value = normalize(value.clone());
77    let mut writer = writer::Writer::new(options.clone());
78
79    match &normalized {
80        Value::Array(arr) => {
81            write_array(&mut writer, None, arr, 0)?;
82        }
83        Value::Object(obj) => {
84            write_object(&mut writer, obj, 0)?;
85        }
86        _ => {
87            write_primitive_value(&mut writer, &normalized, QuotingContext::ObjectValue)?;
88        }
89    }
90
91    Ok(writer.finish())
92}
93
94/// Encode with default options (2-space indent, comma delimiter).
95///
96/// Works with any type implementing `serde::Serialize`.
97///
98/// # Examples
99///
100/// **With structs:**
101/// ```
102/// use serde::Serialize;
103/// use toon_format::encode_default;
104///
105/// #[derive(Serialize)]
106/// struct Person {
107///     name: String,
108///     age: u32,
109/// }
110///
111/// let person = Person {
112///     name: "Alice".to_string(),
113///     age: 30,
114/// };
115/// let toon = encode_default(&person)?;
116/// assert!(toon.contains("name: Alice"));
117/// # Ok::<(), toon_format::ToonError>(())
118/// ```
119///
120/// **With JSON values:**
121/// ```
122/// use toon_format::encode_default;
123/// use serde_json::json;
124///
125/// let data = json!({"tags": ["reading", "gaming", "coding"]});
126/// let toon = encode_default(&data)?;
127/// assert_eq!(toon, "tags[3]: reading,gaming,coding");
128/// # Ok::<(), toon_format::ToonError>(())
129/// ```
130pub fn encode_default<T: serde::Serialize>(value: &T) -> ToonResult<String> {
131    encode(value, &EncodeOptions::default())
132}
133
134/// Encode a JSON object to TOON format (errors if not an object).
135///
136/// This function accepts either `JsonValue` or `serde_json::Value` and converts
137/// automatically.
138///
139/// # Examples
140///
141/// ```
142/// use toon_format::{encode_object, EncodeOptions};
143/// use serde_json::json;
144///
145/// let data = json!({"name": "Alice", "age": 30});
146/// let toon = encode_object(&data, &EncodeOptions::default())?;
147/// assert!(toon.contains("name: Alice"));
148///
149/// // Will error if not an object
150/// assert!(encode_object(&json!(42), &EncodeOptions::default()).is_err());
151/// # Ok::<(), toon_format::ToonError>(())
152/// ```
153pub fn encode_object<V: IntoJsonValue>(value: V, options: &EncodeOptions) -> ToonResult<String> {
154    let json_value = value.into_json_value();
155    if !json_value.is_object() {
156        return Err(ToonError::TypeMismatch {
157            expected: "object".to_string(),
158            found: value_type_name(&json_value).to_string(),
159        });
160    }
161    encode_impl(&json_value, options)
162}
163
164/// Encode a JSON array to TOON format (errors if not an array).
165///
166/// This function accepts either `JsonValue` or `serde_json::Value` and converts
167/// automatically.
168///
169/// # Examples
170///
171/// ```
172/// use toon_format::{encode_array, EncodeOptions};
173/// use serde_json::json;
174///
175/// let data = json!(["a", "b", "c"]);
176/// let toon = encode_array(&data, &EncodeOptions::default())?;
177/// assert_eq!(toon, "[3]: a,b,c");
178///
179/// // Will error if not an array
180/// assert!(encode_array(&json!({"key": "value"}), &EncodeOptions::default()).is_err());
181/// # Ok::<(), toon_format::ToonError>(())
182/// ```
183pub fn encode_array<V: IntoJsonValue>(value: V, options: &EncodeOptions) -> ToonResult<String> {
184    let json_value = value.into_json_value();
185    if !json_value.is_array() {
186        return Err(ToonError::TypeMismatch {
187            expected: "array".to_string(),
188            found: value_type_name(&json_value).to_string(),
189        });
190    }
191    encode_impl(&json_value, options)
192}
193
194fn value_type_name(value: &Value) -> &'static str {
195    match value {
196        Value::Null => "null",
197        Value::Bool(_) => "boolean",
198        Value::Number(_) => "number",
199        Value::String(_) => "string",
200        Value::Array(_) => "array",
201        Value::Object(_) => "object",
202    }
203}
204
205fn write_object(
206    writer: &mut writer::Writer,
207    obj: &IndexMap<String, Value>,
208    depth: usize,
209) -> ToonResult<()> {
210    write_object_impl(writer, obj, depth, false)
211}
212
213fn write_object_impl(
214    writer: &mut writer::Writer,
215    obj: &IndexMap<String, Value>,
216    depth: usize,
217    disable_folding: bool,
218) -> ToonResult<()> {
219    validate_depth(depth, MAX_DEPTH)?;
220
221    let keys: Vec<&String> = obj.keys().collect();
222
223    for (i, key) in keys.iter().enumerate() {
224        if i > 0 {
225            writer.write_newline()?;
226        }
227
228        let value = &obj[*key];
229
230        // Check if this key-value pair can be folded (v1.5 feature)
231        // Don't fold if any sibling key is a dotted path starting with this key
232        // (e.g., don't fold inside "data" if "data.meta.items" exists as a sibling)
233        let has_conflicting_sibling = keys
234            .iter()
235            .any(|k| k.starts_with(&format!("{key}.")) || (k.contains('.') && k == key));
236
237        let folded = if !disable_folding
238            && writer.options.key_folding == KeyFoldingMode::Safe
239            && !has_conflicting_sibling
240        {
241            folding::analyze_foldable_chain(key, value, writer.options.flatten_depth, &keys)
242        } else {
243            None
244        };
245
246        if let Some(chain) = folded {
247            // Write folded key-value pair
248            if depth > 0 {
249                writer.write_indent(depth)?;
250            }
251
252            // Write the leaf value
253            match &chain.leaf_value {
254                Value::Array(arr) => {
255                    // For arrays, pass the folded key to write_array so it generates the header
256                    // correctly
257                    write_array(writer, Some(&chain.folded_key), arr, 0)?;
258                }
259                Value::Object(nested_obj) => {
260                    // Write the folded key (e.g., "a.b.c")
261                    writer.write_key(&chain.folded_key)?;
262                    writer.write_char(':')?;
263                    if !nested_obj.is_empty() {
264                        writer.write_newline()?;
265                        // After folding a chain, disable folding for the leaf object
266                        // This respects flattenDepth and prevents over-folding
267                        write_object_impl(writer, nested_obj, depth + 1, true)?;
268                    }
269                }
270                _ => {
271                    // Write the folded key (e.g., "a.b.c")
272                    writer.write_key(&chain.folded_key)?;
273                    writer.write_char(':')?;
274                    writer.write_char(' ')?;
275                    write_primitive_value(writer, &chain.leaf_value, QuotingContext::ObjectValue)?;
276                }
277            }
278        } else {
279            // Standard (non-folded) encoding
280            match value {
281                Value::Array(arr) => {
282                    write_array(writer, Some(key), arr, depth)?;
283                }
284                Value::Object(nested_obj) => {
285                    if depth > 0 {
286                        writer.write_indent(depth)?;
287                    }
288                    writer.write_key(key)?;
289                    writer.write_char(':')?;
290                    if !nested_obj.is_empty() {
291                        writer.write_newline()?;
292                        // If this key has a conflicting sibling, disable folding for its nested
293                        // objects
294                        let nested_disable_folding = disable_folding || has_conflicting_sibling;
295                        write_object_impl(writer, nested_obj, depth + 1, nested_disable_folding)?;
296                    }
297                }
298                _ => {
299                    if depth > 0 {
300                        writer.write_indent(depth)?;
301                    }
302                    writer.write_key(key)?;
303                    writer.write_char(':')?;
304                    writer.write_char(' ')?;
305                    write_primitive_value(writer, value, QuotingContext::ObjectValue)?;
306                }
307            }
308        }
309    }
310
311    Ok(())
312}
313
314fn write_array(
315    writer: &mut writer::Writer,
316    key: Option<&str>,
317    arr: &[Value],
318    depth: usize,
319) -> ToonResult<()> {
320    validate_depth(depth, MAX_DEPTH)?;
321
322    if arr.is_empty() {
323        writer.write_empty_array_with_key(key, depth)?;
324        return Ok(());
325    }
326
327    // Select format based on array content: tabular (uniform objects) > inline
328    // primitives > nested list
329    if let Some(keys) = is_tabular_array(arr) {
330        encode_tabular_array(writer, key, arr, &keys, depth)?;
331    } else if is_primitive_array(arr) {
332        encode_primitive_array(writer, key, arr, depth)?;
333    } else {
334        encode_nested_array(writer, key, arr, depth)?;
335    }
336
337    Ok(())
338}
339
340/// Check if an array can be encoded as tabular format (uniform objects with
341/// primitive values).
342fn is_tabular_array(arr: &[Value]) -> Option<Vec<String>> {
343    if arr.is_empty() {
344        return None;
345    }
346
347    let first = arr.first()?;
348    if !first.is_object() {
349        return None;
350    }
351
352    let first_obj = first.as_object()?;
353    let keys: Vec<String> = first_obj.keys().cloned().collect();
354
355    // First object must have only primitive values
356    for value in first_obj.values() {
357        if !is_primitive(value) {
358            return None;
359        }
360    }
361
362    // All remaining objects must match: same keys and all primitive values
363    for val in arr.iter().skip(1) {
364        if let Some(obj) = val.as_object() {
365            if obj.len() != keys.len() {
366                return None;
367            }
368            // Verify all keys from first object exist (order doesn't matter)
369            for key in &keys {
370                if !obj.contains_key(key) {
371                    return None;
372                }
373            }
374            // All values must be primitives
375            for value in obj.values() {
376                if !is_primitive(value) {
377                    return None;
378                }
379            }
380        } else {
381            return None;
382        }
383    }
384
385    Some(keys)
386}
387
388/// Check if a value is a primitive (not array or object).
389fn is_primitive(value: &Value) -> bool {
390    matches!(
391        value,
392        Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_)
393    )
394}
395
396/// Check if all array elements are primitives.
397fn is_primitive_array(arr: &[Value]) -> bool {
398    arr.iter().all(is_primitive)
399}
400
401fn encode_primitive_array(
402    writer: &mut writer::Writer,
403    key: Option<&str>,
404    arr: &[Value],
405    depth: usize,
406) -> ToonResult<()> {
407    writer.write_array_header(key, arr.len(), None, depth)?;
408    writer.write_char(' ')?;
409    // Set delimiter context for array values (affects quoting decisions)
410    writer.push_active_delimiter(writer.options.delimiter);
411
412    for (i, val) in arr.iter().enumerate() {
413        if i > 0 {
414            writer.write_delimiter()?;
415        }
416        write_primitive_value(writer, val, QuotingContext::ArrayValue)?;
417    }
418    writer.pop_active_delimiter();
419
420    Ok(())
421}
422
423fn write_primitive_value(
424    writer: &mut writer::Writer,
425    value: &Value,
426    context: QuotingContext,
427) -> ToonResult<()> {
428    match value {
429        Value::Null => writer.write_str("null"),
430        Value::Bool(b) => writer.write_str(&b.to_string()),
431        Value::Number(n) => {
432            // Format in canonical TOON form (no exponents, no trailing zeros)
433            let num_str = format_canonical_number(n);
434            writer.write_str(&num_str)
435        }
436        Value::String(s) => {
437            if writer.needs_quoting(s, context) {
438                writer.write_quoted_string(s)
439            } else {
440                writer.write_str(s)
441            }
442        }
443        _ => Err(ToonError::InvalidInput(
444            "Expected primitive value".to_string(),
445        )),
446    }
447}
448
449fn encode_tabular_array(
450    writer: &mut writer::Writer,
451    key: Option<&str>,
452    arr: &[Value],
453    keys: &[String],
454    depth: usize,
455) -> ToonResult<()> {
456    writer.write_array_header(key, arr.len(), Some(keys), depth)?;
457    writer.write_newline()?;
458
459    writer.push_active_delimiter(writer.options.delimiter);
460
461    // Write each row with values separated by delimiters
462    for (row_index, obj_val) in arr.iter().enumerate() {
463        if let Some(obj) = obj_val.as_object() {
464            writer.write_indent(depth + 1)?;
465
466            for (i, key) in keys.iter().enumerate() {
467                if i > 0 {
468                    writer.write_delimiter()?;
469                }
470
471                // Missing fields become null
472                if let Some(val) = obj.get(key) {
473                    write_primitive_value(writer, val, QuotingContext::ArrayValue)?;
474                } else {
475                    writer.write_str("null")?;
476                }
477            }
478
479            if row_index < arr.len() - 1 {
480                writer.write_newline()?;
481            }
482        }
483    }
484
485    Ok(())
486}
487
488fn encode_nested_array(
489    writer: &mut writer::Writer,
490    key: Option<&str>,
491    arr: &[Value],
492    depth: usize,
493) -> ToonResult<()> {
494    writer.write_array_header(key, arr.len(), None, depth)?;
495    writer.write_newline()?;
496    writer.push_active_delimiter(writer.options.delimiter);
497
498    for (i, val) in arr.iter().enumerate() {
499        writer.write_indent(depth + 1)?;
500        writer.write_char('-')?;
501        writer.write_char(' ')?;
502
503        match val {
504            Value::Array(inner_arr) => {
505                write_array(writer, None, inner_arr, depth + 1)?;
506            }
507            Value::Object(obj) => {
508                // Objects in list items: first field on same line as "- ", rest indented
509                let keys: Vec<&String> = obj.keys().collect();
510                if let Some(first_key) = keys.first() {
511                    let first_val = &obj[*first_key];
512
513                    match first_val {
514                        Value::Array(arr) => {
515                            // First field with array: key on "- " line, array follows
516                            writer.write_key(first_key)?;
517                            write_array(writer, None, arr, depth + 1)?;
518                        }
519                        Value::Object(nested_obj) => {
520                            writer.write_key(first_key)?;
521                            writer.write_char(':')?;
522                            if !nested_obj.is_empty() {
523                                writer.write_newline()?;
524                                write_object(writer, nested_obj, depth + 3)?;
525                            }
526                        }
527                        _ => {
528                            writer.write_key(first_key)?;
529                            writer.write_char(':')?;
530                            writer.write_char(' ')?;
531                            write_primitive_value(writer, first_val, QuotingContext::ObjectValue)?;
532                        }
533                    }
534
535                    // Remaining fields on separate lines with proper indentation
536                    for key in keys.iter().skip(1) {
537                        writer.write_newline()?;
538                        writer.write_indent(depth + 2)?;
539
540                        let value = &obj[*key];
541                        match value {
542                            Value::Array(arr) => {
543                                writer.write_key(key)?;
544                                write_array(writer, None, arr, depth + 1)?;
545                            }
546                            Value::Object(nested_obj) => {
547                                writer.write_key(key)?;
548                                writer.write_char(':')?;
549                                if !nested_obj.is_empty() {
550                                    writer.write_newline()?;
551                                    write_object(writer, nested_obj, depth + 3)?;
552                                }
553                            }
554                            _ => {
555                                writer.write_key(key)?;
556                                writer.write_char(':')?;
557                                writer.write_char(' ')?;
558                                write_primitive_value(writer, value, QuotingContext::ObjectValue)?;
559                            }
560                        }
561                    }
562                }
563            }
564            _ => {
565                write_primitive_value(writer, val, QuotingContext::ArrayValue)?;
566            }
567        }
568
569        if i < arr.len() - 1 {
570            writer.write_newline()?;
571        }
572    }
573    writer.pop_active_delimiter();
574
575    Ok(())
576}
577
578#[cfg(test)]
579mod tests {
580    use core::f64;
581
582    use serde_json::json;
583
584    use super::*;
585
586    #[test]
587    fn test_encode_null() {
588        let value = json!(null);
589        assert_eq!(encode_default(&value).unwrap(), "null");
590    }
591
592    #[test]
593    fn test_encode_bool() {
594        assert_eq!(encode_default(&json!(true)).unwrap(), "true");
595        assert_eq!(encode_default(&json!(false)).unwrap(), "false");
596    }
597
598    #[test]
599    fn test_encode_number() {
600        assert_eq!(encode_default(&json!(42)).unwrap(), "42");
601        assert_eq!(
602            encode_default(&json!(f64::consts::PI)).unwrap(),
603            "3.141592653589793"
604        );
605        assert_eq!(encode_default(&json!(-5)).unwrap(), "-5");
606    }
607
608    #[test]
609    fn test_encode_string() {
610        assert_eq!(encode_default(&json!("hello")).unwrap(), "hello");
611        assert_eq!(
612            encode_default(&json!("hello world")).unwrap(),
613            "hello world"
614        );
615    }
616
617    #[test]
618    fn test_encode_simple_object() {
619        let obj = json!({"name": "Alice", "age": 30});
620        let result = encode_default(&obj).unwrap();
621        assert!(result.contains("name: Alice"));
622        assert!(result.contains("age: 30"));
623    }
624
625    #[test]
626    fn test_encode_primitive_array() {
627        let obj = json!({"tags": ["reading", "gaming", "coding"]});
628        let result = encode_default(&obj).unwrap();
629        assert_eq!(result, "tags[3]: reading,gaming,coding");
630    }
631
632    #[test]
633    fn test_encode_tabular_array() {
634        let obj = json!({
635            "users": [
636                {"id": 1, "name": "Alice"},
637                {"id": 2, "name": "Bob"}
638            ]
639        });
640        let result = encode_default(&obj).unwrap();
641        assert!(result.contains("users[2]{id,name}:"));
642        assert!(result.contains("1,Alice"));
643        assert!(result.contains("2,Bob"));
644    }
645
646    #[test]
647    fn test_encode_empty_array() {
648        let obj = json!({"items": []});
649        let result = encode_default(&obj).unwrap();
650        assert_eq!(result, "items[0]:");
651    }
652
653    #[test]
654    fn test_encode_nested_object() {
655        let obj = json!({
656            "user": {
657                "name": "Alice",
658                "age": 30
659            }
660        });
661        let result = encode_default(&obj).unwrap();
662        assert!(result.contains("user:"));
663        assert!(result.contains("name: Alice"));
664        assert!(result.contains("age: 30"));
665    }
666}