toon_rust/
encode.rs

1//! Encoding TOON format from JSON values
2
3use crate::error::Error;
4use crate::options::EncodeOptions;
5use serde_json::Value;
6
7/// Encode a JSON value to TOON format
8///
9/// # Arguments
10///
11/// * `value` - The JSON value to encode
12/// * `options` - Optional encoding options
13///
14/// # Returns
15///
16/// A `Result` containing the TOON-formatted string or an error
17pub fn encode(value: &Value, options: Option<&EncodeOptions>) -> Result<String, Error> {
18    let default_opts = EncodeOptions::default();
19    let opts = options.unwrap_or(&default_opts);
20    let mut output = String::new();
21    encode_value(value, &mut output, 0, opts)?;
22    Ok(output)
23}
24
25fn encode_value(
26    value: &Value,
27    output: &mut String,
28    indent_level: usize,
29    options: &EncodeOptions,
30) -> Result<(), Error> {
31    match value {
32        Value::Null => {
33            // Null values are typically omitted or represented as empty
34        }
35        Value::Bool(b) => {
36            output.push_str(if *b { "true" } else { "false" });
37        }
38        Value::Number(n) => {
39            if let Some(i) = n.as_i64() {
40                output.push_str(&i.to_string());
41            } else if let Some(f) = n.as_f64() {
42                output.push_str(&f.to_string());
43            } else {
44                return Err(Error::Serialization("Invalid number".to_string()));
45            }
46        }
47        Value::String(s) => {
48            encode_string(s, output, options.get_delimiter());
49        }
50        Value::Array(arr) => {
51            encode_array(arr, output, indent_level, options)?;
52        }
53        Value::Object(obj) => {
54            encode_object(obj, output, indent_level, options)?;
55        }
56    }
57    Ok(())
58}
59
60fn encode_string(s: &str, output: &mut String, delimiter: char) {
61    // Check if we need to quote the string
62    let needs_quoting = s.contains(delimiter)
63        || s.contains(' ')
64        || s.contains('\n')
65        || s.contains('\t')
66        || s == "true"
67        || s == "false"
68        || s == "null"
69        || s.parse::<f64>().is_ok();
70
71    if needs_quoting {
72        output.push('"');
73        for ch in s.chars() {
74            match ch {
75                '"' => output.push_str("\\\""),
76                '\\' => output.push_str("\\\\"),
77                '\n' => output.push_str("\\n"),
78                '\r' => output.push_str("\\r"),
79                '\t' => output.push_str("\\t"),
80                _ => output.push(ch),
81            }
82        }
83        output.push('"');
84    } else {
85        output.push_str(s);
86    }
87}
88
89fn encode_array(
90    arr: &[Value],
91    output: &mut String,
92    indent_level: usize,
93    options: &EncodeOptions,
94) -> Result<(), Error> {
95    if arr.is_empty() {
96        output.push_str("[0]:");
97        return Ok(());
98    }
99
100    // Check if array contains uniform objects (tabular format)
101    if let Some(keys) = check_uniform_objects(arr) {
102        // For root-level arrays, include the header
103        let length_marker = options
104            .length_marker
105            .map(|m| format!("{m}"))
106            .unwrap_or_default();
107        output.push_str(&format!("[{}{}]", length_marker, arr.len()));
108        output.push('{');
109        output.push_str(&keys.join(&options.get_delimiter().to_string()));
110        output.push_str("}:\n");
111        encode_tabular_array_rows(arr, keys, output, indent_level, options)?;
112        return Ok(());
113    }
114
115    // Check if all elements are primitives (inline format)
116    if arr.iter().all(is_primitive) {
117        encode_inline_array(arr, output, options)?;
118        return Ok(());
119    }
120
121    // Otherwise, use list format
122    encode_list_array(arr, output, indent_level, options)?;
123    Ok(())
124}
125
126fn is_primitive(value: &Value) -> bool {
127    matches!(
128        value,
129        Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_)
130    )
131}
132
133fn check_uniform_objects(arr: &[Value]) -> Option<Vec<String>> {
134    if arr.is_empty() {
135        return None;
136    }
137
138    // Get keys from first object (preserve order)
139    let first = arr[0].as_object()?;
140    let keys: Vec<String> = first.keys().cloned().collect();
141    if keys.is_empty() {
142        return None;
143    }
144
145    // Check if all objects have the same keys (order doesn't matter for this check)
146    for item in arr.iter().skip(1) {
147        let obj = item.as_object()?;
148        let item_keys: std::collections::HashSet<String> = obj.keys().cloned().collect();
149        let first_keys: std::collections::HashSet<String> = keys.iter().cloned().collect();
150        if item_keys != first_keys {
151            return None;
152        }
153    }
154
155    Some(keys)
156}
157
158fn encode_tabular_array_rows(
159    arr: &[Value],
160    keys: Vec<String>,
161    output: &mut String,
162    indent_level: usize,
163    options: &EncodeOptions,
164) -> Result<(), Error> {
165    let indent = options.get_indent();
166    let indent_str = " ".repeat(indent_level * indent);
167    let delimiter = options.get_delimiter();
168
169    // Write rows (header already written by caller)
170    for item in arr {
171        output.push_str(&indent_str);
172        output.push_str(&" ".repeat(indent));
173        let obj = item
174            .as_object()
175            .ok_or_else(|| Error::Serialization("Expected object in tabular array".to_string()))?;
176
177        let mut first = true;
178        for key in &keys {
179            if !first {
180                output.push(delimiter);
181            }
182            let value = obj
183                .get(key)
184                .ok_or_else(|| Error::Serialization(format!("Missing key: {key}")))?;
185            encode_primitive_value(value, output, delimiter)?;
186            first = false;
187        }
188        output.push('\n');
189    }
190
191    Ok(())
192}
193
194fn encode_primitive_value(
195    value: &Value,
196    output: &mut String,
197    delimiter: char,
198) -> Result<(), Error> {
199    match value {
200        Value::Null => {}
201        Value::Bool(b) => {
202            output.push_str(if *b { "true" } else { "false" });
203        }
204        Value::Number(n) => {
205            if let Some(i) = n.as_i64() {
206                output.push_str(&i.to_string());
207            } else if let Some(f) = n.as_f64() {
208                output.push_str(&f.to_string());
209            } else {
210                return Err(Error::Serialization("Invalid number".to_string()));
211            }
212        }
213        Value::String(s) => {
214            encode_string(s, output, delimiter);
215        }
216        _ => {
217            return Err(Error::Serialization(
218                "Non-primitive value in tabular array".to_string(),
219            ));
220        }
221    }
222    Ok(())
223}
224
225fn encode_inline_array(
226    arr: &[Value],
227    output: &mut String,
228    options: &EncodeOptions,
229) -> Result<(), Error> {
230    let length_marker = options
231        .length_marker
232        .map(|m| format!("{m}"))
233        .unwrap_or_default();
234    output.push_str(&format!("[{}{}]:", length_marker, arr.len()));
235
236    let delimiter = options.get_delimiter();
237    let mut first = true;
238    for item in arr {
239        if !first {
240            output.push(delimiter);
241        }
242        match item {
243            Value::Null => {}
244            Value::Bool(b) => {
245                output.push_str(if *b { "true" } else { "false" });
246            }
247            Value::Number(n) => {
248                if let Some(i) = n.as_i64() {
249                    output.push_str(&i.to_string());
250                } else if let Some(f) = n.as_f64() {
251                    output.push_str(&f.to_string());
252                }
253            }
254            Value::String(s) => {
255                encode_string(s, output, delimiter);
256            }
257            _ => {
258                return Err(Error::Serialization(
259                    "Non-primitive in inline array".to_string(),
260                ));
261            }
262        }
263        first = false;
264    }
265
266    Ok(())
267}
268
269fn encode_list_array(
270    arr: &[Value],
271    output: &mut String,
272    indent_level: usize,
273    options: &EncodeOptions,
274) -> Result<(), Error> {
275    let indent = options.get_indent();
276    let indent_str = " ".repeat(indent_level * indent);
277
278    for item in arr {
279        output.push_str(&indent_str);
280        output.push_str(&" ".repeat(indent));
281        output.push_str("- ");
282        // For objects in list arrays, encode them inline as key: value
283        match item {
284            Value::Object(obj) => {
285                let mut first = true;
286                for (key, val) in obj {
287                    if !first {
288                        output.push(' ');
289                    }
290                    output.push_str(key);
291                    output.push_str(": ");
292                    encode_primitive_value(val, output, options.get_delimiter())?;
293                    first = false;
294                }
295            }
296            _ => {
297                encode_value(item, output, indent_level + 1, options)?;
298            }
299        }
300        output.push('\n');
301    }
302
303    Ok(())
304}
305
306fn encode_object(
307    obj: &serde_json::Map<String, Value>,
308    output: &mut String,
309    indent_level: usize,
310    options: &EncodeOptions,
311) -> Result<(), Error> {
312    if obj.is_empty() {
313        return Ok(());
314    }
315
316    let indent = options.get_indent();
317    let indent_str = " ".repeat(indent_level * indent);
318
319    let mut first = true;
320    for (key, value) in obj {
321        if !first {
322            output.push('\n');
323        }
324        output.push_str(&indent_str);
325        output.push_str(key);
326
327        match value {
328            Value::Array(arr) => {
329                // For arrays, check the format and encode appropriately
330                if arr.is_empty() {
331                    output.push_str("[0]:");
332                } else if let Some(keys) = check_uniform_objects(arr) {
333                    // Tabular array - output on same line: key[N]{...}:
334                    let length_marker = options
335                        .length_marker
336                        .map(|m| format!("{m}"))
337                        .unwrap_or_default();
338                    output.push_str(&format!("[{}{}]", length_marker, arr.len()));
339                    output.push('{');
340                    output.push_str(&keys.join(&options.get_delimiter().to_string()));
341                    output.push_str("}:\n");
342                    // Now output the rows
343                    encode_tabular_array_rows(arr, keys, output, indent_level, options)?;
344                } else if arr.iter().all(is_primitive) {
345                    // Inline array - output on same line: key[N]: value1,value2
346                    let length_marker = options
347                        .length_marker
348                        .map(|m| format!("{m}"))
349                        .unwrap_or_default();
350                    output.push_str(&format!("[{}{}]:", length_marker, arr.len()));
351                    let delimiter = options.get_delimiter();
352                    let mut first = true;
353                    for item in arr {
354                        if !first {
355                            output.push(delimiter);
356                        }
357                        encode_primitive_value(item, output, delimiter)?;
358                        first = false;
359                    }
360                } else {
361                    // List array - output on same line: key[N]:
362                    let length_marker = options
363                        .length_marker
364                        .map(|m| format!("{m}"))
365                        .unwrap_or_default();
366                    output.push_str(&format!("[{}{}]:", length_marker, arr.len()));
367                    output.push('\n');
368                    encode_list_array(arr, output, indent_level, options)?;
369                }
370            }
371            Value::Object(_) => {
372                output.push_str(": ");
373                output.push('\n');
374                encode_value(value, output, indent_level + 1, options)?;
375            }
376            _ => {
377                output.push_str(": ");
378                encode_value(value, output, indent_level, options)?;
379            }
380        }
381        first = false;
382    }
383
384    Ok(())
385}