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                    write_array(writer, Some(key), arr, depth)?;
252                }
253                Value::Object(nested_obj) => {
254                    if depth > 0 {
255                        writer.write_indent(depth)?;
256                    }
257                    writer.write_key(key)?;
258                    writer.write_char(':')?;
259                    if !nested_obj.is_empty() {
260                        writer.write_newline()?;
261                        // If this key has a conflicting sibling, disable folding for its nested
262                        // objects
263                        let nested_disable_folding = disable_folding || has_conflicting_sibling;
264                        write_object_impl(writer, nested_obj, depth + 1, nested_disable_folding)?;
265                    }
266                }
267                _ => {
268                    if depth > 0 {
269                        writer.write_indent(depth)?;
270                    }
271                    writer.write_key(key)?;
272                    writer.write_char(':')?;
273                    writer.write_char(' ')?;
274                    write_primitive_value(writer, value, QuotingContext::ObjectValue)?;
275                }
276            }
277        }
278    }
279
280    Ok(())
281}
282
283fn write_array(
284    writer: &mut writer::Writer,
285    key: Option<&str>,
286    arr: &[Value],
287    depth: usize,
288) -> ToonResult<()> {
289    validate_depth(depth, MAX_DEPTH)?;
290
291    if arr.is_empty() {
292        writer.write_empty_array_with_key(key, depth)?;
293        return Ok(());
294    }
295
296    // Select format based on array content: tabular (uniform objects) > inline
297    // primitives > nested list
298    if let Some(keys) = is_tabular_array(arr) {
299        encode_tabular_array(writer, key, arr, &keys, depth)?;
300    } else if is_primitive_array(arr) {
301        encode_primitive_array(writer, key, arr, depth)?;
302    } else {
303        encode_nested_array(writer, key, arr, depth)?;
304    }
305
306    Ok(())
307}
308
309/// Check if an array can be encoded as tabular format (uniform objects with
310/// primitive values).
311fn is_tabular_array(arr: &[Value]) -> Option<Vec<String>> {
312    if arr.is_empty() {
313        return None;
314    }
315
316    let first = arr.first()?;
317    if !first.is_object() {
318        return None;
319    }
320
321    let first_obj = first.as_object()?;
322    let keys: Vec<String> = first_obj.keys().cloned().collect();
323
324    // First object must have only primitive values
325    for value in first_obj.values() {
326        if !is_primitive(value) {
327            return None;
328        }
329    }
330
331    // All remaining objects must match: same keys and all primitive values
332    for val in arr.iter().skip(1) {
333        if let Some(obj) = val.as_object() {
334            if obj.len() != keys.len() {
335                return None;
336            }
337            // Verify all keys from first object exist (order doesn't matter)
338            for key in &keys {
339                if !obj.contains_key(key) {
340                    return None;
341                }
342            }
343            // All values must be primitives
344            for value in obj.values() {
345                if !is_primitive(value) {
346                    return None;
347                }
348            }
349        } else {
350            return None;
351        }
352    }
353
354    Some(keys)
355}
356
357/// Check if a value is a primitive (not array or object).
358fn is_primitive(value: &Value) -> bool {
359    matches!(
360        value,
361        Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_)
362    )
363}
364
365/// Check if all array elements are primitives.
366fn is_primitive_array(arr: &[Value]) -> bool {
367    arr.iter().all(is_primitive)
368}
369
370fn encode_primitive_array(
371    writer: &mut writer::Writer,
372    key: Option<&str>,
373    arr: &[Value],
374    depth: usize,
375) -> ToonResult<()> {
376    writer.write_array_header(key, arr.len(), None, depth)?;
377    writer.write_char(' ')?;
378    // Set delimiter context for array values (affects quoting decisions)
379    writer.push_active_delimiter(writer.options.delimiter);
380
381    for (i, val) in arr.iter().enumerate() {
382        if i > 0 {
383            writer.write_delimiter()?;
384        }
385        write_primitive_value(writer, val, QuotingContext::ArrayValue)?;
386    }
387    writer.pop_active_delimiter();
388
389    Ok(())
390}
391
392fn write_primitive_value(
393    writer: &mut writer::Writer,
394    value: &Value,
395    context: QuotingContext,
396) -> ToonResult<()> {
397    match value {
398        Value::Null => writer.write_str("null"),
399        Value::Bool(b) => writer.write_str(&b.to_string()),
400        Value::Number(n) => {
401            // Format in canonical TOON form (no exponents, no trailing zeros)
402            let num_str = format_canonical_number(n);
403            writer.write_str(&num_str)
404        }
405        Value::String(s) => {
406            if writer.needs_quoting(s, context) {
407                writer.write_quoted_string(s)
408            } else {
409                writer.write_str(s)
410            }
411        }
412        _ => Err(ToonError::InvalidInput(
413            "Expected primitive value".to_string(),
414        )),
415    }
416}
417
418fn encode_tabular_array(
419    writer: &mut writer::Writer,
420    key: Option<&str>,
421    arr: &[Value],
422    keys: &[String],
423    depth: usize,
424) -> ToonResult<()> {
425    writer.write_array_header(key, arr.len(), Some(keys), depth)?;
426    writer.write_newline()?;
427
428    writer.push_active_delimiter(writer.options.delimiter);
429
430    // Write each row with values separated by delimiters
431    for (row_index, obj_val) in arr.iter().enumerate() {
432        if let Some(obj) = obj_val.as_object() {
433            writer.write_indent(depth + 1)?;
434
435            for (i, key) in keys.iter().enumerate() {
436                if i > 0 {
437                    writer.write_delimiter()?;
438                }
439
440                // Missing fields become null
441                if let Some(val) = obj.get(key) {
442                    write_primitive_value(writer, val, QuotingContext::ArrayValue)?;
443                } else {
444                    writer.write_str("null")?;
445                }
446            }
447
448            if row_index < arr.len() - 1 {
449                writer.write_newline()?;
450            }
451        }
452    }
453
454    Ok(())
455}
456
457fn encode_nested_array(
458    writer: &mut writer::Writer,
459    key: Option<&str>,
460    arr: &[Value],
461    depth: usize,
462) -> ToonResult<()> {
463    writer.write_array_header(key, arr.len(), None, depth)?;
464    writer.write_newline()?;
465    writer.push_active_delimiter(writer.options.delimiter);
466
467    for (i, val) in arr.iter().enumerate() {
468        writer.write_indent(depth + 1)?;
469        writer.write_char('-')?;
470        writer.write_char(' ')?;
471
472        match val {
473            Value::Array(inner_arr) => {
474                write_array(writer, None, inner_arr, depth + 1)?;
475            }
476            Value::Object(obj) => {
477                // Objects in list items: first field on same line as "- ", rest indented
478                let keys: Vec<&String> = obj.keys().collect();
479                if let Some(first_key) = keys.first() {
480                    let first_val = &obj[*first_key];
481
482                    match first_val {
483                        Value::Array(arr) => {
484                            // First field with array: key on "- " line, array follows
485                            writer.write_key(first_key)?;
486                            write_array(writer, None, arr, depth + 1)?;
487                        }
488                        Value::Object(nested_obj) => {
489                            writer.write_key(first_key)?;
490                            writer.write_char(':')?;
491                            if !nested_obj.is_empty() {
492                                writer.write_newline()?;
493                                write_object(writer, nested_obj, depth + 3)?;
494                            }
495                        }
496                        _ => {
497                            writer.write_key(first_key)?;
498                            writer.write_char(':')?;
499                            writer.write_char(' ')?;
500                            write_primitive_value(writer, first_val, QuotingContext::ObjectValue)?;
501                        }
502                    }
503
504                    // Remaining fields on separate lines with proper indentation
505                    for key in keys.iter().skip(1) {
506                        writer.write_newline()?;
507                        writer.write_indent(depth + 2)?;
508
509                        let value = &obj[*key];
510                        match value {
511                            Value::Array(arr) => {
512                                writer.write_key(key)?;
513                                write_array(writer, None, arr, depth + 1)?;
514                            }
515                            Value::Object(nested_obj) => {
516                                writer.write_key(key)?;
517                                writer.write_char(':')?;
518                                if !nested_obj.is_empty() {
519                                    writer.write_newline()?;
520                                    write_object(writer, nested_obj, depth + 3)?;
521                                }
522                            }
523                            _ => {
524                                writer.write_key(key)?;
525                                writer.write_char(':')?;
526                                writer.write_char(' ')?;
527                                write_primitive_value(writer, value, QuotingContext::ObjectValue)?;
528                            }
529                        }
530                    }
531                }
532            }
533            _ => {
534                write_primitive_value(writer, val, QuotingContext::ArrayValue)?;
535            }
536        }
537
538        if i < arr.len() - 1 {
539            writer.write_newline()?;
540        }
541    }
542    writer.pop_active_delimiter();
543
544    Ok(())
545}
546
547#[cfg(test)]
548mod tests {
549    use core::f64;
550
551    use serde_json::json;
552
553    use super::*;
554
555    #[test]
556    fn test_encode_null() {
557        let value = json!(null);
558        assert_eq!(encode_default(&value).unwrap(), "null");
559    }
560
561    #[test]
562    fn test_encode_bool() {
563        assert_eq!(encode_default(json!(true)).unwrap(), "true");
564        assert_eq!(encode_default(json!(false)).unwrap(), "false");
565    }
566
567    #[test]
568    fn test_encode_number() {
569        assert_eq!(encode_default(json!(42)).unwrap(), "42");
570        assert_eq!(
571            encode_default(json!(f64::consts::PI)).unwrap(),
572            "3.141592653589793"
573        );
574        assert_eq!(encode_default(json!(-5)).unwrap(), "-5");
575    }
576
577    #[test]
578    fn test_encode_string() {
579        assert_eq!(encode_default(json!("hello")).unwrap(), "hello");
580        assert_eq!(encode_default(json!("hello world")).unwrap(), "hello world");
581    }
582
583    #[test]
584    fn test_encode_simple_object() {
585        let obj = json!({"name": "Alice", "age": 30});
586        let result = encode_default(&obj).unwrap();
587        assert!(result.contains("name: Alice"));
588        assert!(result.contains("age: 30"));
589    }
590
591    #[test]
592    fn test_encode_primitive_array() {
593        let obj = json!({"tags": ["reading", "gaming", "coding"]});
594        let result = encode_default(&obj).unwrap();
595        assert_eq!(result, "tags[3]: reading,gaming,coding");
596    }
597
598    #[test]
599    fn test_encode_tabular_array() {
600        let obj = json!({
601            "users": [
602                {"id": 1, "name": "Alice"},
603                {"id": 2, "name": "Bob"}
604            ]
605        });
606        let result = encode_default(&obj).unwrap();
607        assert!(result.contains("users[2]{id,name}:"));
608        assert!(result.contains("1,Alice"));
609        assert!(result.contains("2,Bob"));
610    }
611
612    #[test]
613    fn test_encode_empty_array() {
614        let obj = json!({"items": []});
615        let result = encode_default(&obj).unwrap();
616        assert_eq!(result, "items[0]:");
617    }
618
619    #[test]
620    fn test_encode_nested_object() {
621        let obj = json!({
622            "user": {
623                "name": "Alice",
624                "age": 30
625            }
626        });
627        let result = encode_default(&obj).unwrap();
628        assert!(result.contains("user:"));
629        assert!(result.contains("name: Alice"));
630        assert!(result.contains("age: 30"));
631    }
632}