toon_format/encode/
mod.rs

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