rtoon/encode/
mod.rs

1pub mod primitives;
2pub mod writer;
3use indexmap::IndexMap;
4
5use crate::{
6    constants::MAX_DEPTH,
7    error::{
8        ToonError,
9        ToonResult,
10    },
11    types::{
12        EncodeOptions,
13        IntoJsonValue,
14        JsonValue as Value,
15    },
16    utils::{
17        normalize,
18        validation::validate_depth,
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 rtoon::{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::<(), rtoon::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)?;
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 rtoon::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::<(), rtoon::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 rtoon::{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::<(), rtoon::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 rtoon::{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::<(), rtoon::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        if depth > 0 {
186            writer.write_indent(depth)?;
187        }
188
189        let value = &obj[*key];
190
191        match value {
192            Value::Array(arr) => {
193                write_array(writer, Some(key), arr, depth)?;
194            }
195            Value::Object(nested_obj) => {
196                writer.write_key(key)?;
197                writer.write_char(':')?;
198                writer.write_newline()?;
199                write_object(writer, nested_obj, depth + 1)?;
200            }
201            _ => {
202                writer.write_key(key)?;
203                writer.write_char(':')?;
204                writer.write_char(' ')?;
205                write_primitive_value(writer, value)?;
206            }
207        }
208    }
209
210    Ok(())
211}
212
213fn write_array(
214    writer: &mut writer::Writer,
215    key: Option<&str>,
216    arr: &[Value],
217    depth: usize,
218) -> ToonResult<()> {
219    validate_depth(depth, MAX_DEPTH)?;
220
221    if arr.is_empty() {
222        writer.write_empty_array_with_key(key)?;
223        return Ok(());
224    }
225
226    // Choose encoding format: tabular > primitive inline > nested list
227    if let Some(keys) = is_tabular_array(arr) {
228        encode_tabular_array(writer, key, arr, &keys, depth)?;
229    } else if is_primitive_array(arr) {
230        encode_primitive_array(writer, key, arr, depth)?;
231    } else {
232        encode_nested_array(writer, key, arr, depth)?;
233    }
234
235    Ok(())
236}
237
238/// Check if an array can be encoded as tabular format (uniform objects with
239/// primitive values).
240fn is_tabular_array(arr: &[Value]) -> Option<Vec<String>> {
241    if arr.is_empty() {
242        return None;
243    }
244
245    let first = arr.first()?;
246    if !first.is_object() {
247        return None;
248    }
249
250    let first_obj = first.as_object()?;
251    let keys: Vec<String> = first_obj.keys().cloned().collect();
252
253    // All values must be primitives for tabular format
254    for value in first_obj.values() {
255        if !is_primitive(value) {
256            return None;
257        }
258    }
259
260    // All objects must have the same keys and primitive values
261    for val in arr.iter().skip(1) {
262        if let Some(obj) = val.as_object() {
263            let obj_keys: Vec<String> = obj.keys().cloned().collect();
264            if keys != obj_keys {
265                return None;
266            }
267            for value in obj.values() {
268                if !is_primitive(value) {
269                    return None;
270                }
271            }
272        } else {
273            return None;
274        }
275    }
276
277    Some(keys)
278}
279
280/// Check if a value is a primitive (not array or object).
281fn is_primitive(value: &Value) -> bool {
282    matches!(
283        value,
284        Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_)
285    )
286}
287
288/// Check if all array elements are primitives.
289fn is_primitive_array(arr: &[Value]) -> bool {
290    arr.iter().all(is_primitive)
291}
292
293fn encode_primitive_array(
294    writer: &mut writer::Writer,
295    key: Option<&str>,
296    arr: &[Value],
297    depth: usize,
298) -> ToonResult<()> {
299    writer.write_array_header(key, arr.len(), None, depth)?;
300    writer.write_char(' ')?;
301
302    for (i, val) in arr.iter().enumerate() {
303        if i > 0 {
304            writer.write_delimiter()?;
305        }
306        write_primitive_value(writer, val)?;
307    }
308
309    Ok(())
310}
311
312fn write_primitive_value(writer: &mut writer::Writer, value: &Value) -> ToonResult<()> {
313    match value {
314        Value::Null => writer.write_str("null"),
315        Value::Bool(b) => writer.write_str(&b.to_string()),
316        Value::Number(n) => writer.write_str(&n.to_string()),
317        Value::String(s) => {
318            if writer.needs_quoting(s) {
319                writer.write_quoted_string(s)
320            } else {
321                writer.write_str(s)
322            }
323        }
324        _ => Err(ToonError::InvalidInput(
325            "Expected primitive value".to_string(),
326        )),
327    }
328}
329
330fn encode_tabular_array(
331    writer: &mut writer::Writer,
332    key: Option<&str>,
333    arr: &[Value],
334    keys: &[String],
335    depth: usize,
336) -> ToonResult<()> {
337    writer.write_array_header(key, arr.len(), Some(keys), depth)?;
338    writer.write_newline()?;
339
340    for (row_index, obj_val) in arr.iter().enumerate() {
341        if let Some(obj) = obj_val.as_object() {
342            writer.write_indent(depth + 1)?;
343
344            for (i, key) in keys.iter().enumerate() {
345                if i > 0 {
346                    writer.write_delimiter()?;
347                }
348
349                if let Some(val) = obj.get(key) {
350                    write_primitive_value(writer, val)?;
351                } else {
352                    writer.write_str("null")?;
353                }
354            }
355
356            if row_index < arr.len() - 1 {
357                writer.write_newline()?;
358            }
359        }
360    }
361
362    Ok(())
363}
364
365fn encode_nested_array(
366    writer: &mut writer::Writer,
367    key: Option<&str>,
368    arr: &[Value],
369    depth: usize,
370) -> ToonResult<()> {
371    writer.write_array_header(key, arr.len(), None, depth)?;
372    writer.write_newline()?;
373
374    for (i, val) in arr.iter().enumerate() {
375        writer.write_indent(depth + 1)?;
376        writer.write_char('-')?;
377        writer.write_char(' ')?;
378
379        match val {
380            Value::Array(inner_arr) => {
381                write_array(writer, None, inner_arr, depth + 1)?;
382            }
383            Value::Object(obj) => {
384                let keys: Vec<&String> = obj.keys().collect();
385                if let Some(first_key) = keys.first() {
386                    let first_val = &obj[*first_key];
387
388                    writer.write_key(first_key)?;
389                    writer.write_char(':')?;
390                    writer.write_char(' ')?;
391                    match first_val {
392                        Value::Array(arr) => {
393                            write_array(writer, None, arr, depth + 1)?;
394                        }
395                        Value::Object(nested_obj) => {
396                            writer.write_newline()?;
397                            write_object(writer, nested_obj, depth + 2)?;
398                        }
399                        _ => {
400                            write_primitive_value(writer, first_val)?;
401                        }
402                    }
403
404                    for key in keys.iter().skip(1) {
405                        writer.write_newline()?;
406                        writer.write_indent(depth + 2)?;
407                        writer.write_key(key)?;
408                        writer.write_char(':')?;
409                        writer.write_char(' ')?;
410
411                        let value = &obj[*key];
412                        match value {
413                            Value::Array(arr) => {
414                                write_array(writer, None, arr, depth + 2)?;
415                            }
416                            Value::Object(nested_obj) => {
417                                writer.write_newline()?;
418                                write_object(writer, nested_obj, depth + 3)?;
419                            }
420                            _ => {
421                                write_primitive_value(writer, value)?;
422                            }
423                        }
424                    }
425                }
426            }
427            _ => {
428                write_primitive_value(writer, val)?;
429            }
430        }
431
432        if i < arr.len() - 1 {
433            writer.write_newline()?;
434        }
435    }
436
437    Ok(())
438}
439
440#[cfg(test)]
441mod tests {
442    use serde_json::json;
443
444    use super::*;
445
446    #[test]
447    fn test_encode_null() {
448        let value = json!(null);
449        assert_eq!(encode_default(&value).unwrap(), "null");
450    }
451
452    #[test]
453    fn test_encode_bool() {
454        assert_eq!(encode_default(&json!(true)).unwrap(), "true");
455        assert_eq!(encode_default(&json!(false)).unwrap(), "false");
456    }
457
458    #[test]
459    fn test_encode_number() {
460        assert_eq!(encode_default(&json!(42)).unwrap(), "42");
461        assert_eq!(encode_default(&json!(3.14)).unwrap(), "3.14");
462        assert_eq!(encode_default(&json!(-5)).unwrap(), "-5");
463    }
464
465    #[test]
466    fn test_encode_string() {
467        assert_eq!(encode_default(&json!("hello")).unwrap(), "hello");
468        assert_eq!(
469            encode_default(&json!("hello world")).unwrap(),
470            "\"hello world\""
471        );
472    }
473
474    #[test]
475    fn test_encode_simple_object() {
476        let obj = json!({"name": "Alice", "age": 30});
477        let result = encode_default(&obj).unwrap();
478        assert!(result.contains("name: Alice"));
479        assert!(result.contains("age: 30"));
480    }
481
482    #[test]
483    fn test_encode_primitive_array() {
484        let obj = json!({"tags": ["reading", "gaming", "coding"]});
485        let result = encode_default(&obj).unwrap();
486        assert_eq!(result, "tags[3]: reading,gaming,coding");
487    }
488
489    #[test]
490    fn test_encode_tabular_array() {
491        let obj = json!({
492            "users": [
493                {"id": 1, "name": "Alice"},
494                {"id": 2, "name": "Bob"}
495            ]
496        });
497        let result = encode_default(&obj).unwrap();
498        assert!(result.contains("users[2]{id,name}:"));
499        assert!(result.contains("1,Alice"));
500        assert!(result.contains("2,Bob"));
501    }
502
503    #[test]
504    fn test_encode_empty_array() {
505        let obj = json!({"items": []});
506        let result = encode_default(&obj).unwrap();
507        assert_eq!(result, "items[0]:");
508    }
509
510    #[test]
511    fn test_encode_nested_object() {
512        let obj = json!({
513            "user": {
514                "name": "Alice",
515                "age": 30
516            }
517        });
518        let result = encode_default(&obj).unwrap();
519        assert!(result.contains("user:"));
520        assert!(result.contains("name: Alice"));
521        assert!(result.contains("age: 30"));
522    }
523}