Skip to main content

toon/encode/
encoders.rs

1use std::collections::HashSet;
2
3use crate::encode::folding::try_fold_key_chain;
4use crate::encode::normalize::{
5    is_array_of_arrays, is_array_of_objects, is_array_of_primitives, is_empty_object,
6    is_json_primitive,
7};
8use crate::encode::primitives::{
9    encode_and_join_primitives, encode_key, encode_primitive, format_header,
10};
11use crate::options::ResolvedEncodeOptions;
12use crate::shared::constants::{DOT, LIST_ITEM_MARKER, LIST_ITEM_PREFIX};
13use crate::{JsonArray, JsonObject, JsonPrimitive, JsonValue};
14
15#[must_use]
16pub fn encode_json_value(value: &JsonValue, options: &ResolvedEncodeOptions) -> Vec<String> {
17    let estimated_lines = estimate_line_count(value);
18    let mut out = Vec::with_capacity(estimated_lines);
19    match value {
20        JsonValue::Primitive(primitive) => {
21            let encoded = encode_primitive(primitive, options.delimiter);
22            if !encoded.is_empty() {
23                out.push(encoded);
24            }
25        }
26        JsonValue::Array(items) => {
27            encode_array_lines(None, items, 0, options, &mut out);
28        }
29        JsonValue::Object(entries) => {
30            encode_object_lines(entries, 0, options, None, None, None, &mut out);
31        }
32    }
33    out
34}
35
36fn encode_object_lines(
37    value: &JsonObject,
38    depth: usize,
39    options: &ResolvedEncodeOptions,
40    root_literal_keys: Option<&HashSet<String>>,
41    path_prefix: Option<&str>,
42    remaining_depth: Option<usize>,
43    out: &mut Vec<String>,
44) {
45    // Collect keys as references to avoid cloning
46    let keys: Vec<&str> = value.iter().map(|(key, _)| key.as_str()).collect();
47
48    let mut root_literal_set = HashSet::new();
49    let root_literal_keys = if depth == 0 && root_literal_keys.is_none() {
50        for key in &keys {
51            if key.contains(DOT) {
52                root_literal_set.insert((*key).to_string());
53            }
54        }
55        Some(&root_literal_set)
56    } else {
57        root_literal_keys
58    };
59
60    let effective_flatten_depth = remaining_depth.unwrap_or(options.flatten_depth);
61
62    for (key, val) in value {
63        encode_key_value_pair_lines(
64            key,
65            val,
66            depth,
67            options,
68            &keys,
69            root_literal_keys,
70            path_prefix,
71            effective_flatten_depth,
72            out,
73        );
74    }
75}
76
77#[allow(clippy::too_many_arguments)]
78fn encode_key_value_pair_lines(
79    key: &str,
80    value: &JsonValue,
81    depth: usize,
82    options: &ResolvedEncodeOptions,
83    siblings: &[&str],
84    root_literal_keys: Option<&HashSet<String>>,
85    path_prefix: Option<&str>,
86    flatten_depth: usize,
87    out: &mut Vec<String>,
88) {
89    let current_path =
90        path_prefix.map_or_else(|| key.to_string(), |prefix| format!("{prefix}{DOT}{key}"));
91
92    if let Some(folded) = try_fold_key_chain(
93        key,
94        value,
95        siblings,
96        options,
97        root_literal_keys,
98        path_prefix,
99        flatten_depth,
100    ) {
101        let encoded_key = encode_key(&folded.folded_key);
102
103        if folded.remainder.is_none() {
104            match folded.leaf_value {
105                JsonValue::Primitive(primitive) => {
106                    let encoded = encode_primitive(&primitive, options.delimiter);
107                    out.push(indented_key_value_line(
108                        depth,
109                        &encoded_key,
110                        &encoded,
111                        options.indent,
112                    ));
113                    return;
114                }
115                JsonValue::Array(items) => {
116                    encode_array_lines(Some(&folded.folded_key), &items, depth, options, out);
117                    return;
118                }
119                JsonValue::Object(entries) => {
120                    if is_empty_object(&entries) {
121                        out.push(indented_key_colon_line(depth, &encoded_key, options.indent));
122                        return;
123                    }
124                }
125            }
126        }
127
128        if let Some(JsonValue::Object(entries)) = folded.remainder {
129            out.push(indented_key_colon_line(depth, &encoded_key, options.indent));
130            let remaining_depth = flatten_depth.saturating_sub(folded.segment_count);
131            let folded_path = if let Some(prefix) = path_prefix {
132                format!("{prefix}{DOT}{}", folded.folded_key)
133            } else {
134                folded.folded_key.clone()
135            };
136            encode_object_lines(
137                &entries,
138                depth + 1,
139                options,
140                root_literal_keys,
141                Some(&folded_path),
142                Some(remaining_depth),
143                out,
144            );
145            return;
146        }
147    }
148
149    let encoded_key = encode_key(key);
150
151    match value {
152        JsonValue::Primitive(primitive) => {
153            let encoded = encode_primitive(primitive, options.delimiter);
154            out.push(indented_key_value_line(
155                depth,
156                &encoded_key,
157                &encoded,
158                options.indent,
159            ));
160        }
161        JsonValue::Array(items) => {
162            encode_array_lines(Some(key), items, depth, options, out);
163        }
164        JsonValue::Object(entries) => {
165            out.push(indented_key_colon_line(depth, &encoded_key, options.indent));
166            if !is_empty_object(entries) {
167                encode_object_lines(
168                    entries,
169                    depth + 1,
170                    options,
171                    root_literal_keys,
172                    Some(&current_path),
173                    Some(flatten_depth),
174                    out,
175                );
176            }
177        }
178    }
179}
180
181fn encode_array_lines(
182    key: Option<&str>,
183    value: &JsonArray,
184    depth: usize,
185    options: &ResolvedEncodeOptions,
186    out: &mut Vec<String>,
187) {
188    if value.is_empty() {
189        let header = format_header(0, key, None, options.delimiter);
190        out.push(indented_line(depth, &header, options.indent));
191        return;
192    }
193
194    if is_array_of_primitives(value) {
195        let array_line = encode_inline_array_line(value, options.delimiter, key);
196        out.push(indented_line(depth, &array_line, options.indent));
197        return;
198    }
199
200    if is_array_of_arrays(value) {
201        let all_primitive_arrays = value.iter().all(|item| match item {
202            JsonValue::Array(items) => is_array_of_primitives(items),
203            _ => false,
204        });
205        if all_primitive_arrays {
206            encode_array_of_arrays_as_list_items_lines(key, value, depth, options, out);
207            return;
208        }
209    }
210
211    if is_array_of_objects(value) {
212        if let Some(header) = extract_tabular_header(value) {
213            encode_array_of_objects_as_tabular_lines(key, value, &header, depth, options, out);
214        } else {
215            encode_mixed_array_as_list_items_lines(key, value, depth, options, out);
216        }
217        return;
218    }
219
220    encode_mixed_array_as_list_items_lines(key, value, depth, options, out);
221}
222
223fn encode_array_of_arrays_as_list_items_lines(
224    key: Option<&str>,
225    values: &JsonArray,
226    depth: usize,
227    options: &ResolvedEncodeOptions,
228    out: &mut Vec<String>,
229) {
230    let header = format_header(values.len(), key, None, options.delimiter);
231    out.push(indented_line(depth, &header, options.indent));
232
233    for item in values {
234        if let JsonValue::Array(items) = item {
235            let line = encode_inline_array_line(items, options.delimiter, None);
236            out.push(indented_list_item(depth + 1, &line, options.indent));
237        }
238    }
239}
240
241fn encode_inline_array_line(values: &JsonArray, delimiter: char, key: Option<&str>) -> String {
242    let primitives: Vec<JsonPrimitive> = values
243        .iter()
244        .filter_map(|item| match item {
245            JsonValue::Primitive(primitive) => Some(primitive.clone()),
246            _ => None,
247        })
248        .collect();
249    let header = format_header(values.len(), key, None, delimiter);
250    if primitives.is_empty() {
251        return header;
252    }
253    let joined = encode_and_join_primitives(&primitives, delimiter);
254    // Build "header joined" without format!
255    let mut out = String::with_capacity(header.len() + 1 + joined.len());
256    out.push_str(&header);
257    out.push(' ');
258    out.push_str(&joined);
259    out
260}
261
262fn encode_array_of_objects_as_tabular_lines(
263    key: Option<&str>,
264    rows: &JsonArray,
265    header: &[String],
266    depth: usize,
267    options: &ResolvedEncodeOptions,
268    out: &mut Vec<String>,
269) {
270    let formatted_header = format_header(rows.len(), key, Some(header), options.delimiter);
271    out.push(indented_line(depth, &formatted_header, options.indent));
272    write_tabular_rows_lines(rows, header, depth + 1, options, out);
273}
274
275fn write_tabular_rows_lines(
276    rows: &JsonArray,
277    header: &[String],
278    depth: usize,
279    options: &ResolvedEncodeOptions,
280    out: &mut Vec<String>,
281) {
282    for row in rows {
283        if let JsonValue::Object(entries) = row {
284            let mut values = Vec::with_capacity(header.len());
285            for key in header {
286                let value = object_get(entries, key).expect("tabular header missing key");
287                if let JsonValue::Primitive(primitive) = value {
288                    values.push(primitive.clone());
289                } else {
290                    panic!("tabular row contains non-primitive value");
291                }
292            }
293            let joined = encode_and_join_primitives(&values, options.delimiter);
294            out.push(indented_line(depth, &joined, options.indent));
295        }
296    }
297}
298
299fn extract_tabular_header(rows: &JsonArray) -> Option<Vec<String>> {
300    if rows.is_empty() {
301        return None;
302    }
303
304    let JsonValue::Object(first) = &rows[0] else {
305        return None;
306    };
307
308    if first.is_empty() {
309        return None;
310    }
311
312    let header: Vec<String> = first.iter().map(|(key, _)| key.clone()).collect();
313    if is_tabular_array(rows, &header) {
314        Some(header)
315    } else {
316        None
317    }
318}
319
320fn is_tabular_array(rows: &JsonArray, header: &[String]) -> bool {
321    for row in rows {
322        let JsonValue::Object(entries) = row else {
323            return false;
324        };
325
326        if entries.len() != header.len() {
327            return false;
328        }
329
330        for key in header {
331            let Some(value) = object_get(entries, key) else {
332                return false;
333            };
334            if !is_json_primitive(value) {
335                return false;
336            }
337        }
338    }
339    true
340}
341
342fn encode_mixed_array_as_list_items_lines(
343    key: Option<&str>,
344    items: &JsonArray,
345    depth: usize,
346    options: &ResolvedEncodeOptions,
347    out: &mut Vec<String>,
348) {
349    let header = format_header(items.len(), key, None, options.delimiter);
350    out.push(indented_line(depth, &header, options.indent));
351
352    for item in items {
353        encode_list_item_value_lines(item, depth + 1, options, out);
354    }
355}
356
357fn encode_object_as_list_item_lines(
358    obj: &JsonObject,
359    depth: usize,
360    options: &ResolvedEncodeOptions,
361    out: &mut Vec<String>,
362) {
363    if obj.is_empty() {
364        out.push(indented_line(depth, LIST_ITEM_MARKER, options.indent));
365        return;
366    }
367
368    let first = obj[0].clone();
369    let rest = if obj.len() > 1 {
370        obj[1..].to_vec()
371    } else {
372        Vec::new()
373    };
374    let (first_key, first_value) = first;
375
376    if let JsonValue::Array(items) = &first_value {
377        if is_array_of_objects(items) {
378            if let Some(header) = extract_tabular_header(items) {
379                let formatted = format_header(
380                    items.len(),
381                    Some(&first_key),
382                    Some(&header),
383                    options.delimiter,
384                );
385                out.push(indented_list_item(depth, &formatted, options.indent));
386                write_tabular_rows_lines(items, &header, depth + 2, options, out);
387                if !rest.is_empty() {
388                    encode_object_lines(&rest, depth + 1, options, None, None, None, out);
389                }
390                return;
391            }
392        }
393    }
394
395    let encoded_key = encode_key(&first_key);
396
397    match first_value {
398        JsonValue::Primitive(primitive) => {
399            let encoded = encode_primitive(&primitive, options.delimiter);
400            out.push(indented_list_item_key_value(
401                depth,
402                &encoded_key,
403                &encoded,
404                options.indent,
405            ));
406        }
407        JsonValue::Array(items) => {
408            if items.is_empty() {
409                let header = format_header(0, None, None, options.delimiter);
410                out.push(indented_list_item_key_header(
411                    depth,
412                    &encoded_key,
413                    &header,
414                    options.indent,
415                ));
416            } else if is_array_of_primitives(&items) {
417                let line = encode_inline_array_line(&items, options.delimiter, None);
418                out.push(indented_list_item_key_header(
419                    depth,
420                    &encoded_key,
421                    &line,
422                    options.indent,
423                ));
424            } else {
425                let header = format_header(items.len(), None, None, options.delimiter);
426                out.push(indented_list_item_key_header(
427                    depth,
428                    &encoded_key,
429                    &header,
430                    options.indent,
431                ));
432                for item in &items {
433                    encode_list_item_value_lines(item, depth + 2, options, out);
434                }
435            }
436        }
437        JsonValue::Object(entries) => {
438            out.push(indented_list_item_key_colon(
439                depth,
440                &encoded_key,
441                options.indent,
442            ));
443            if !is_empty_object(&entries) {
444                encode_object_lines(&entries, depth + 2, options, None, None, None, out);
445            }
446        }
447    }
448
449    if !rest.is_empty() {
450        encode_object_lines(&rest, depth + 1, options, None, None, None, out);
451    }
452}
453
454fn encode_list_item_value_lines(
455    value: &JsonValue,
456    depth: usize,
457    options: &ResolvedEncodeOptions,
458    out: &mut Vec<String>,
459) {
460    match value {
461        JsonValue::Primitive(primitive) => {
462            let encoded = encode_primitive(primitive, options.delimiter);
463            out.push(indented_list_item(depth, &encoded, options.indent));
464        }
465        JsonValue::Array(items) => {
466            if is_array_of_primitives(items) {
467                let line = encode_inline_array_line(items, options.delimiter, None);
468                out.push(indented_list_item(depth, &line, options.indent));
469            } else {
470                let header = format_header(items.len(), None, None, options.delimiter);
471                out.push(indented_list_item(depth, &header, options.indent));
472                for item in items {
473                    encode_list_item_value_lines(item, depth + 1, options, out);
474                }
475            }
476        }
477        JsonValue::Object(entries) => {
478            encode_object_as_list_item_lines(entries, depth, options, out);
479        }
480    }
481}
482
483fn object_get<'a>(entries: &'a JsonObject, key: &str) -> Option<&'a JsonValue> {
484    entries.iter().find(|(k, _)| k == key).map(|(_, v)| v)
485}
486
487fn indented_line(depth: usize, content: &str, indent_size: usize) -> String {
488    // Use saturating arithmetic to prevent overflow with deeply nested structures
489    let indent_chars = indent_size.saturating_mul(depth);
490    let capacity = indent_chars.saturating_add(content.len());
491    let mut out = String::with_capacity(capacity);
492    for _ in 0..indent_chars {
493        out.push(' ');
494    }
495    out.push_str(content);
496    out
497}
498
499/// Build "key: value" line directly without intermediate format! allocation
500fn indented_key_value_line(depth: usize, key: &str, value: &str, indent_size: usize) -> String {
501    // Use saturating arithmetic to prevent overflow with deeply nested structures
502    let indent_chars = indent_size.saturating_mul(depth);
503    // key + ": " + value
504    let capacity = indent_chars
505        .saturating_add(key.len())
506        .saturating_add(2)
507        .saturating_add(value.len());
508    let mut out = String::with_capacity(capacity);
509    for _ in 0..indent_chars {
510        out.push(' ');
511    }
512    out.push_str(key);
513    out.push_str(": ");
514    out.push_str(value);
515    out
516}
517
518/// Build "key:" line directly without intermediate format! allocation
519fn indented_key_colon_line(depth: usize, key: &str, indent_size: usize) -> String {
520    // Use saturating arithmetic to prevent overflow with deeply nested structures
521    let indent_chars = indent_size.saturating_mul(depth);
522    // key + ":"
523    let capacity = indent_chars.saturating_add(key.len()).saturating_add(1);
524    let mut out = String::with_capacity(capacity);
525    for _ in 0..indent_chars {
526        out.push(' ');
527    }
528    out.push_str(key);
529    out.push(':');
530    out
531}
532
533fn indented_list_item(depth: usize, content: &str, indent_size: usize) -> String {
534    // Use saturating arithmetic to prevent overflow with deeply nested structures
535    let indent_chars = indent_size.saturating_mul(depth);
536    let prefix_len = LIST_ITEM_PREFIX.len();
537    let capacity = indent_chars
538        .saturating_add(prefix_len)
539        .saturating_add(content.len());
540    let mut out = String::with_capacity(capacity);
541    for _ in 0..indent_chars {
542        out.push(' ');
543    }
544    out.push_str(LIST_ITEM_PREFIX);
545    out.push_str(content);
546    out
547}
548
549/// Build "- key: value" list item directly without intermediate format! allocation
550fn indented_list_item_key_value(
551    depth: usize,
552    key: &str,
553    value: &str,
554    indent_size: usize,
555) -> String {
556    // Use saturating arithmetic to prevent overflow with deeply nested structures
557    let indent_chars = indent_size.saturating_mul(depth);
558    let prefix_len = LIST_ITEM_PREFIX.len();
559    // "- " + key + ": " + value
560    let capacity = indent_chars
561        .saturating_add(prefix_len)
562        .saturating_add(key.len())
563        .saturating_add(2)
564        .saturating_add(value.len());
565    let mut out = String::with_capacity(capacity);
566    for _ in 0..indent_chars {
567        out.push(' ');
568    }
569    out.push_str(LIST_ITEM_PREFIX);
570    out.push_str(key);
571    out.push_str(": ");
572    out.push_str(value);
573    out
574}
575
576/// Build "- key:" list item directly without intermediate format! allocation
577fn indented_list_item_key_colon(depth: usize, key: &str, indent_size: usize) -> String {
578    // Use saturating arithmetic to prevent overflow with deeply nested structures
579    let indent_chars = indent_size.saturating_mul(depth);
580    let prefix_len = LIST_ITEM_PREFIX.len();
581    // "- " + key + ":"
582    let capacity = indent_chars
583        .saturating_add(prefix_len)
584        .saturating_add(key.len())
585        .saturating_add(1);
586    let mut out = String::with_capacity(capacity);
587    for _ in 0..indent_chars {
588        out.push(' ');
589    }
590    out.push_str(LIST_ITEM_PREFIX);
591    out.push_str(key);
592    out.push(':');
593    out
594}
595
596/// Build "- key[N]:" list item directly without intermediate format! allocation
597fn indented_list_item_key_header(
598    depth: usize,
599    key: &str,
600    header: &str,
601    indent_size: usize,
602) -> String {
603    // Use saturating arithmetic to prevent overflow with deeply nested structures
604    let indent_chars = indent_size.saturating_mul(depth);
605    let prefix_len = LIST_ITEM_PREFIX.len();
606    // "- " + key + header
607    let capacity = indent_chars
608        .saturating_add(prefix_len)
609        .saturating_add(key.len())
610        .saturating_add(header.len());
611    let mut out = String::with_capacity(capacity);
612    for _ in 0..indent_chars {
613        out.push(' ');
614    }
615    out.push_str(LIST_ITEM_PREFIX);
616    out.push_str(key);
617    out.push_str(header);
618    out
619}
620
621/// Estimate the number of output lines for pre-allocation.
622/// This is a rough heuristic - over-estimation is fine, under-estimation causes reallocation.
623fn estimate_line_count(value: &JsonValue) -> usize {
624    match value {
625        JsonValue::Primitive(_) => 1,
626        JsonValue::Array(items) => {
627            // Header line + recursively estimate each item
628            1 + items.iter().map(estimate_line_count).sum::<usize>()
629        }
630        JsonValue::Object(entries) => {
631            // Each entry produces at least one line
632            entries
633                .iter()
634                .map(|(_, v)| estimate_line_count(v))
635                .sum::<usize>()
636                .max(1)
637        }
638    }
639}