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