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