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 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(¤t_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 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 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
499fn indented_key_value_line(depth: usize, key: &str, value: &str, indent_size: usize) -> String {
501 let indent_chars = indent_size.saturating_mul(depth);
503 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
518fn indented_key_colon_line(depth: usize, key: &str, indent_size: usize) -> String {
520 let indent_chars = indent_size.saturating_mul(depth);
522 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 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
549fn indented_list_item_key_value(
551 depth: usize,
552 key: &str,
553 value: &str,
554 indent_size: usize,
555) -> String {
556 let indent_chars = indent_size.saturating_mul(depth);
558 let prefix_len = LIST_ITEM_PREFIX.len();
559 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
576fn indented_list_item_key_colon(depth: usize, key: &str, indent_size: usize) -> String {
578 let indent_chars = indent_size.saturating_mul(depth);
580 let prefix_len = LIST_ITEM_PREFIX.len();
581 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
596fn indented_list_item_key_header(
598 depth: usize,
599 key: &str,
600 header: &str,
601 indent_size: usize,
602) -> String {
603 let indent_chars = indent_size.saturating_mul(depth);
605 let prefix_len = LIST_ITEM_PREFIX.len();
606 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
621fn estimate_line_count(value: &JsonValue) -> usize {
624 match value {
625 JsonValue::Primitive(_) => 1,
626 JsonValue::Array(items) => {
627 1 + items.iter().map(estimate_line_count).sum::<usize>()
629 }
630 JsonValue::Object(entries) => {
631 entries
633 .iter()
634 .map(|(_, v)| estimate_line_count(v))
635 .sum::<usize>()
636 .max(1)
637 }
638 }
639}