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 && is_array_of_objects(items)
378 && let Some(header) = extract_tabular_header(items)
379 {
380 let formatted = format_header(
381 items.len(),
382 Some(&first_key),
383 Some(&header),
384 options.delimiter,
385 );
386 out.push(indented_list_item(depth, &formatted, options.indent));
387 write_tabular_rows_lines(items, &header, depth + 2, options, out);
388 if !rest.is_empty() {
389 encode_object_lines(&rest, depth + 1, options, None, None, None, out);
390 }
391 return;
392 }
393
394 let encoded_key = encode_key(&first_key);
395
396 match first_value {
397 JsonValue::Primitive(primitive) => {
398 let encoded = encode_primitive(&primitive, options.delimiter);
399 out.push(indented_list_item_key_value(
400 depth,
401 &encoded_key,
402 &encoded,
403 options.indent,
404 ));
405 }
406 JsonValue::Array(items) => {
407 if items.is_empty() {
408 let header = format_header(0, None, None, options.delimiter);
409 out.push(indented_list_item_key_header(
410 depth,
411 &encoded_key,
412 &header,
413 options.indent,
414 ));
415 } else if is_array_of_primitives(&items) {
416 let line = encode_inline_array_line(&items, options.delimiter, None);
417 out.push(indented_list_item_key_header(
418 depth,
419 &encoded_key,
420 &line,
421 options.indent,
422 ));
423 } else {
424 let header = format_header(items.len(), None, None, options.delimiter);
425 out.push(indented_list_item_key_header(
426 depth,
427 &encoded_key,
428 &header,
429 options.indent,
430 ));
431 for item in &items {
432 encode_list_item_value_lines(item, depth + 2, options, out);
433 }
434 }
435 }
436 JsonValue::Object(entries) => {
437 out.push(indented_list_item_key_colon(
438 depth,
439 &encoded_key,
440 options.indent,
441 ));
442 if !is_empty_object(&entries) {
443 encode_object_lines(&entries, depth + 2, options, None, None, None, out);
444 }
445 }
446 }
447
448 if !rest.is_empty() {
449 encode_object_lines(&rest, depth + 1, options, None, None, None, out);
450 }
451}
452
453fn encode_list_item_value_lines(
454 value: &JsonValue,
455 depth: usize,
456 options: &ResolvedEncodeOptions,
457 out: &mut Vec<String>,
458) {
459 match value {
460 JsonValue::Primitive(primitive) => {
461 let encoded = encode_primitive(primitive, options.delimiter);
462 out.push(indented_list_item(depth, &encoded, options.indent));
463 }
464 JsonValue::Array(items) => {
465 if is_array_of_primitives(items) {
466 let line = encode_inline_array_line(items, options.delimiter, None);
467 out.push(indented_list_item(depth, &line, options.indent));
468 } else {
469 let header = format_header(items.len(), None, None, options.delimiter);
470 out.push(indented_list_item(depth, &header, options.indent));
471 for item in items {
472 encode_list_item_value_lines(item, depth + 1, options, out);
473 }
474 }
475 }
476 JsonValue::Object(entries) => {
477 encode_object_as_list_item_lines(entries, depth, options, out);
478 }
479 }
480}
481
482fn object_get<'a>(entries: &'a JsonObject, key: &str) -> Option<&'a JsonValue> {
483 entries.iter().find(|(k, _)| k == key).map(|(_, v)| v)
484}
485
486fn indented_line(depth: usize, content: &str, indent_size: usize) -> String {
487 let indent_chars = indent_size.saturating_mul(depth);
489 let capacity = indent_chars.saturating_add(content.len());
490 let mut out = String::with_capacity(capacity);
491 for _ in 0..indent_chars {
492 out.push(' ');
493 }
494 out.push_str(content);
495 out
496}
497
498fn indented_key_value_line(depth: usize, key: &str, value: &str, indent_size: usize) -> String {
500 let indent_chars = indent_size.saturating_mul(depth);
502 let capacity = indent_chars
504 .saturating_add(key.len())
505 .saturating_add(2)
506 .saturating_add(value.len());
507 let mut out = String::with_capacity(capacity);
508 for _ in 0..indent_chars {
509 out.push(' ');
510 }
511 out.push_str(key);
512 out.push_str(": ");
513 out.push_str(value);
514 out
515}
516
517fn indented_key_colon_line(depth: usize, key: &str, indent_size: usize) -> String {
519 let indent_chars = indent_size.saturating_mul(depth);
521 let capacity = indent_chars.saturating_add(key.len()).saturating_add(1);
523 let mut out = String::with_capacity(capacity);
524 for _ in 0..indent_chars {
525 out.push(' ');
526 }
527 out.push_str(key);
528 out.push(':');
529 out
530}
531
532fn indented_list_item(depth: usize, content: &str, indent_size: usize) -> String {
533 let indent_chars = indent_size.saturating_mul(depth);
535 let prefix_len = LIST_ITEM_PREFIX.len();
536 let capacity = indent_chars
537 .saturating_add(prefix_len)
538 .saturating_add(content.len());
539 let mut out = String::with_capacity(capacity);
540 for _ in 0..indent_chars {
541 out.push(' ');
542 }
543 out.push_str(LIST_ITEM_PREFIX);
544 out.push_str(content);
545 out
546}
547
548fn indented_list_item_key_value(
550 depth: usize,
551 key: &str,
552 value: &str,
553 indent_size: usize,
554) -> String {
555 let indent_chars = indent_size.saturating_mul(depth);
557 let prefix_len = LIST_ITEM_PREFIX.len();
558 let capacity = indent_chars
560 .saturating_add(prefix_len)
561 .saturating_add(key.len())
562 .saturating_add(2)
563 .saturating_add(value.len());
564 let mut out = String::with_capacity(capacity);
565 for _ in 0..indent_chars {
566 out.push(' ');
567 }
568 out.push_str(LIST_ITEM_PREFIX);
569 out.push_str(key);
570 out.push_str(": ");
571 out.push_str(value);
572 out
573}
574
575fn indented_list_item_key_colon(depth: usize, key: &str, indent_size: usize) -> String {
577 let indent_chars = indent_size.saturating_mul(depth);
579 let prefix_len = LIST_ITEM_PREFIX.len();
580 let capacity = indent_chars
582 .saturating_add(prefix_len)
583 .saturating_add(key.len())
584 .saturating_add(1);
585 let mut out = String::with_capacity(capacity);
586 for _ in 0..indent_chars {
587 out.push(' ');
588 }
589 out.push_str(LIST_ITEM_PREFIX);
590 out.push_str(key);
591 out.push(':');
592 out
593}
594
595fn indented_list_item_key_header(
597 depth: usize,
598 key: &str,
599 header: &str,
600 indent_size: usize,
601) -> String {
602 let indent_chars = indent_size.saturating_mul(depth);
604 let prefix_len = LIST_ITEM_PREFIX.len();
605 let capacity = indent_chars
607 .saturating_add(prefix_len)
608 .saturating_add(key.len())
609 .saturating_add(header.len());
610 let mut out = String::with_capacity(capacity);
611 for _ in 0..indent_chars {
612 out.push(' ');
613 }
614 out.push_str(LIST_ITEM_PREFIX);
615 out.push_str(key);
616 out.push_str(header);
617 out
618}
619
620fn estimate_line_count(value: &JsonValue) -> usize {
623 match value {
624 JsonValue::Primitive(_) => 1,
625 JsonValue::Array(items) => {
626 1 + items.iter().map(estimate_line_count).sum::<usize>()
628 }
629 JsonValue::Object(entries) => {
630 entries
632 .iter()
633 .map(|(_, v)| estimate_line_count(v))
634 .sum::<usize>()
635 .max(1)
636 }
637 }
638}