1pub mod folding;
3pub mod primitives;
4pub mod writer;
5use indexmap::IndexMap;
6
7use crate::{
8 constants::MAX_DEPTH,
9 types::{
10 EncodeOptions,
11 IntoJsonValue,
12 JsonValue as Value,
13 KeyFoldingMode,
14 ToonError,
15 ToonResult,
16 },
17 utils::{
18 format_canonical_number,
19 normalize,
20 validation::validate_depth,
21 QuotingContext,
22 },
23};
24
25pub fn encode<T: serde::Serialize>(value: &T, options: &EncodeOptions) -> ToonResult<String> {
69 let json_value =
70 serde_json::to_value(value).map_err(|e| ToonError::SerializationError(e.to_string()))?;
71 let json_value: Value = json_value.into();
72 encode_impl(&json_value, options)
73}
74
75fn encode_impl(value: &Value, options: &EncodeOptions) -> ToonResult<String> {
76 let normalized: Value = normalize(value.clone());
77 let mut writer = writer::Writer::new(options.clone());
78
79 match &normalized {
80 Value::Array(arr) => {
81 write_array(&mut writer, None, arr, 0)?;
82 }
83 Value::Object(obj) => {
84 write_object(&mut writer, obj, 0)?;
85 }
86 _ => {
87 write_primitive_value(&mut writer, &normalized, QuotingContext::ObjectValue)?;
88 }
89 }
90
91 Ok(writer.finish())
92}
93
94pub fn encode_default<T: serde::Serialize>(value: &T) -> ToonResult<String> {
131 encode(value, &EncodeOptions::default())
132}
133
134pub fn encode_object<V: IntoJsonValue>(value: V, options: &EncodeOptions) -> ToonResult<String> {
154 let json_value = value.into_json_value();
155 if !json_value.is_object() {
156 return Err(ToonError::TypeMismatch {
157 expected: "object".to_string(),
158 found: value_type_name(&json_value).to_string(),
159 });
160 }
161 encode_impl(&json_value, options)
162}
163
164pub fn encode_array<V: IntoJsonValue>(value: V, options: &EncodeOptions) -> ToonResult<String> {
184 let json_value = value.into_json_value();
185 if !json_value.is_array() {
186 return Err(ToonError::TypeMismatch {
187 expected: "array".to_string(),
188 found: value_type_name(&json_value).to_string(),
189 });
190 }
191 encode_impl(&json_value, options)
192}
193
194fn value_type_name(value: &Value) -> &'static str {
195 match value {
196 Value::Null => "null",
197 Value::Bool(_) => "boolean",
198 Value::Number(_) => "number",
199 Value::String(_) => "string",
200 Value::Array(_) => "array",
201 Value::Object(_) => "object",
202 }
203}
204
205fn write_object(
206 writer: &mut writer::Writer,
207 obj: &IndexMap<String, Value>,
208 depth: usize,
209) -> ToonResult<()> {
210 write_object_impl(writer, obj, depth, false)
211}
212
213fn write_object_impl(
214 writer: &mut writer::Writer,
215 obj: &IndexMap<String, Value>,
216 depth: usize,
217 disable_folding: bool,
218) -> ToonResult<()> {
219 validate_depth(depth, MAX_DEPTH)?;
220
221 let keys: Vec<&String> = obj.keys().collect();
222
223 for (i, key) in keys.iter().enumerate() {
224 if i > 0 {
225 writer.write_newline()?;
226 }
227
228 let value = &obj[*key];
229
230 let has_conflicting_sibling = keys
234 .iter()
235 .any(|k| k.starts_with(&format!("{key}.")) || (k.contains('.') && k == key));
236
237 let folded = if !disable_folding
238 && writer.options.key_folding == KeyFoldingMode::Safe
239 && !has_conflicting_sibling
240 {
241 folding::analyze_foldable_chain(key, value, writer.options.flatten_depth, &keys)
242 } else {
243 None
244 };
245
246 if let Some(chain) = folded {
247 if depth > 0 {
249 writer.write_indent(depth)?;
250 }
251
252 match &chain.leaf_value {
254 Value::Array(arr) => {
255 write_array(writer, Some(&chain.folded_key), arr, 0)?;
258 }
259 Value::Object(nested_obj) => {
260 writer.write_key(&chain.folded_key)?;
262 writer.write_char(':')?;
263 if !nested_obj.is_empty() {
264 writer.write_newline()?;
265 write_object_impl(writer, nested_obj, depth + 1, true)?;
268 }
269 }
270 _ => {
271 writer.write_key(&chain.folded_key)?;
273 writer.write_char(':')?;
274 writer.write_char(' ')?;
275 write_primitive_value(writer, &chain.leaf_value, QuotingContext::ObjectValue)?;
276 }
277 }
278 } else {
279 match value {
281 Value::Array(arr) => {
282 write_array(writer, Some(key), arr, depth)?;
283 }
284 Value::Object(nested_obj) => {
285 if depth > 0 {
286 writer.write_indent(depth)?;
287 }
288 writer.write_key(key)?;
289 writer.write_char(':')?;
290 if !nested_obj.is_empty() {
291 writer.write_newline()?;
292 let nested_disable_folding = disable_folding || has_conflicting_sibling;
295 write_object_impl(writer, nested_obj, depth + 1, nested_disable_folding)?;
296 }
297 }
298 _ => {
299 if depth > 0 {
300 writer.write_indent(depth)?;
301 }
302 writer.write_key(key)?;
303 writer.write_char(':')?;
304 writer.write_char(' ')?;
305 write_primitive_value(writer, value, QuotingContext::ObjectValue)?;
306 }
307 }
308 }
309 }
310
311 Ok(())
312}
313
314fn write_array(
315 writer: &mut writer::Writer,
316 key: Option<&str>,
317 arr: &[Value],
318 depth: usize,
319) -> ToonResult<()> {
320 validate_depth(depth, MAX_DEPTH)?;
321
322 if arr.is_empty() {
323 writer.write_empty_array_with_key(key, depth)?;
324 return Ok(());
325 }
326
327 if let Some(keys) = is_tabular_array(arr) {
330 encode_tabular_array(writer, key, arr, &keys, depth)?;
331 } else if is_primitive_array(arr) {
332 encode_primitive_array(writer, key, arr, depth)?;
333 } else {
334 encode_nested_array(writer, key, arr, depth)?;
335 }
336
337 Ok(())
338}
339
340fn is_tabular_array(arr: &[Value]) -> Option<Vec<String>> {
343 if arr.is_empty() {
344 return None;
345 }
346
347 let first = arr.first()?;
348 if !first.is_object() {
349 return None;
350 }
351
352 let first_obj = first.as_object()?;
353 let keys: Vec<String> = first_obj.keys().cloned().collect();
354
355 for value in first_obj.values() {
357 if !is_primitive(value) {
358 return None;
359 }
360 }
361
362 for val in arr.iter().skip(1) {
364 if let Some(obj) = val.as_object() {
365 if obj.len() != keys.len() {
366 return None;
367 }
368 for key in &keys {
370 if !obj.contains_key(key) {
371 return None;
372 }
373 }
374 for value in obj.values() {
376 if !is_primitive(value) {
377 return None;
378 }
379 }
380 } else {
381 return None;
382 }
383 }
384
385 Some(keys)
386}
387
388fn is_primitive(value: &Value) -> bool {
390 matches!(
391 value,
392 Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_)
393 )
394}
395
396fn is_primitive_array(arr: &[Value]) -> bool {
398 arr.iter().all(is_primitive)
399}
400
401fn encode_primitive_array(
402 writer: &mut writer::Writer,
403 key: Option<&str>,
404 arr: &[Value],
405 depth: usize,
406) -> ToonResult<()> {
407 writer.write_array_header(key, arr.len(), None, depth)?;
408 writer.write_char(' ')?;
409 writer.push_active_delimiter(writer.options.delimiter);
411
412 for (i, val) in arr.iter().enumerate() {
413 if i > 0 {
414 writer.write_delimiter()?;
415 }
416 write_primitive_value(writer, val, QuotingContext::ArrayValue)?;
417 }
418 writer.pop_active_delimiter();
419
420 Ok(())
421}
422
423fn write_primitive_value(
424 writer: &mut writer::Writer,
425 value: &Value,
426 context: QuotingContext,
427) -> ToonResult<()> {
428 match value {
429 Value::Null => writer.write_str("null"),
430 Value::Bool(b) => writer.write_str(&b.to_string()),
431 Value::Number(n) => {
432 let num_str = format_canonical_number(n);
434 writer.write_str(&num_str)
435 }
436 Value::String(s) => {
437 if writer.needs_quoting(s, context) {
438 writer.write_quoted_string(s)
439 } else {
440 writer.write_str(s)
441 }
442 }
443 _ => Err(ToonError::InvalidInput(
444 "Expected primitive value".to_string(),
445 )),
446 }
447}
448
449fn encode_tabular_array(
450 writer: &mut writer::Writer,
451 key: Option<&str>,
452 arr: &[Value],
453 keys: &[String],
454 depth: usize,
455) -> ToonResult<()> {
456 writer.write_array_header(key, arr.len(), Some(keys), depth)?;
457 writer.write_newline()?;
458
459 writer.push_active_delimiter(writer.options.delimiter);
460
461 for (row_index, obj_val) in arr.iter().enumerate() {
463 if let Some(obj) = obj_val.as_object() {
464 writer.write_indent(depth + 1)?;
465
466 for (i, key) in keys.iter().enumerate() {
467 if i > 0 {
468 writer.write_delimiter()?;
469 }
470
471 if let Some(val) = obj.get(key) {
473 write_primitive_value(writer, val, QuotingContext::ArrayValue)?;
474 } else {
475 writer.write_str("null")?;
476 }
477 }
478
479 if row_index < arr.len() - 1 {
480 writer.write_newline()?;
481 }
482 }
483 }
484
485 Ok(())
486}
487
488fn encode_list_item_tabular_array(
496 writer: &mut writer::Writer,
497 arr: &[Value],
498 keys: &[String],
499 depth: usize,
500) -> ToonResult<()> {
501 writer.write_char('[')?;
503 writer.write_str(&arr.len().to_string())?;
504
505 if writer.options.delimiter != crate::types::Delimiter::Comma {
506 writer.write_char(writer.options.delimiter.as_char())?;
507 }
508
509 writer.write_char(']')?;
510
511 writer.write_char('{')?;
513 for (i, field) in keys.iter().enumerate() {
514 if i > 0 {
515 writer.write_char(writer.options.delimiter.as_char())?;
516 }
517 writer.write_key(field)?;
518 }
519 writer.write_char('}')?;
520 writer.write_char(':')?;
521 writer.write_newline()?;
522
523 writer.push_active_delimiter(writer.options.delimiter);
524
525 for (row_index, obj_val) in arr.iter().enumerate() {
528 if let Some(obj) = obj_val.as_object() {
529 writer.write_indent(depth + 2)?;
530
531 for (i, key) in keys.iter().enumerate() {
532 if i > 0 {
533 writer.write_delimiter()?;
534 }
535
536 if let Some(val) = obj.get(key) {
538 write_primitive_value(writer, val, QuotingContext::ArrayValue)?;
539 } else {
540 writer.write_str("null")?;
541 }
542 }
543
544 if row_index < arr.len() - 1 {
545 writer.write_newline()?;
546 }
547 }
548 }
549
550 writer.pop_active_delimiter();
551
552 Ok(())
553}
554
555fn encode_nested_array(
556 writer: &mut writer::Writer,
557 key: Option<&str>,
558 arr: &[Value],
559 depth: usize,
560) -> ToonResult<()> {
561 writer.write_array_header(key, arr.len(), None, depth)?;
562 writer.write_newline()?;
563 writer.push_active_delimiter(writer.options.delimiter);
564
565 for (i, val) in arr.iter().enumerate() {
566 writer.write_indent(depth + 1)?;
567 writer.write_char('-')?;
568
569 match val {
570 Value::Array(inner_arr) => {
571 writer.write_char(' ')?;
572 write_array(writer, None, inner_arr, depth + 1)?;
573 }
574 Value::Object(obj) => {
575 let keys: Vec<&String> = obj.keys().collect();
578 if let Some(first_key) = keys.first() {
579 writer.write_char(' ')?;
580 let first_val = &obj[*first_key];
581
582 match first_val {
583 Value::Array(arr) => {
584 writer.write_key(first_key)?;
588
589 if let Some(keys) = is_tabular_array(arr) {
590 encode_list_item_tabular_array(writer, arr, &keys, depth + 1)?;
592 } else {
593 write_array(writer, None, arr, depth + 2)?;
596 }
597 }
598 Value::Object(nested_obj) => {
599 writer.write_key(first_key)?;
600 writer.write_char(':')?;
601 if !nested_obj.is_empty() {
602 writer.write_newline()?;
603 write_object(writer, nested_obj, depth + 3)?;
604 }
605 }
606 _ => {
607 writer.write_key(first_key)?;
608 writer.write_char(':')?;
609 writer.write_char(' ')?;
610 write_primitive_value(writer, first_val, QuotingContext::ObjectValue)?;
611 }
612 }
613
614 for key in keys.iter().skip(1) {
616 writer.write_newline()?;
617 writer.write_indent(depth + 2)?;
618
619 let value = &obj[*key];
620 match value {
621 Value::Array(arr) => {
622 writer.write_key(key)?;
623 write_array(writer, None, arr, depth + 1)?;
624 }
625 Value::Object(nested_obj) => {
626 writer.write_key(key)?;
627 writer.write_char(':')?;
628 if !nested_obj.is_empty() {
629 writer.write_newline()?;
630 write_object(writer, nested_obj, depth + 3)?;
631 }
632 }
633 _ => {
634 writer.write_key(key)?;
635 writer.write_char(':')?;
636 writer.write_char(' ')?;
637 write_primitive_value(writer, value, QuotingContext::ObjectValue)?;
638 }
639 }
640 }
641 }
642 }
643 _ => {
644 writer.write_char(' ')?;
645 write_primitive_value(writer, val, QuotingContext::ArrayValue)?;
646 }
647 }
648
649 if i < arr.len() - 1 {
650 writer.write_newline()?;
651 }
652 }
653 writer.pop_active_delimiter();
654
655 Ok(())
656}
657
658#[cfg(test)]
659mod tests {
660 use core::f64;
661
662 use serde_json::json;
663
664 use super::*;
665
666 #[test]
667 fn test_encode_null() {
668 let value = json!(null);
669 assert_eq!(encode_default(&value).unwrap(), "null");
670 }
671
672 #[test]
673 fn test_encode_bool() {
674 assert_eq!(encode_default(&json!(true)).unwrap(), "true");
675 assert_eq!(encode_default(&json!(false)).unwrap(), "false");
676 }
677
678 #[test]
679 fn test_encode_number() {
680 assert_eq!(encode_default(&json!(42)).unwrap(), "42");
681 assert_eq!(
682 encode_default(&json!(f64::consts::PI)).unwrap(),
683 "3.141592653589793"
684 );
685 assert_eq!(encode_default(&json!(-5)).unwrap(), "-5");
686 }
687
688 #[test]
689 fn test_encode_string() {
690 assert_eq!(encode_default(&json!("hello")).unwrap(), "hello");
691 assert_eq!(
692 encode_default(&json!("hello world")).unwrap(),
693 "hello world"
694 );
695 }
696
697 #[test]
698 fn test_encode_simple_object() {
699 let obj = json!({"name": "Alice", "age": 30});
700 let result = encode_default(&obj).unwrap();
701 assert!(result.contains("name: Alice"));
702 assert!(result.contains("age: 30"));
703 }
704
705 #[test]
706 fn test_encode_primitive_array() {
707 let obj = json!({"tags": ["reading", "gaming", "coding"]});
708 let result = encode_default(&obj).unwrap();
709 assert_eq!(result, "tags[3]: reading,gaming,coding");
710 }
711
712 #[test]
713 fn test_encode_tabular_array() {
714 let obj = json!({
715 "users": [
716 {"id": 1, "name": "Alice"},
717 {"id": 2, "name": "Bob"}
718 ]
719 });
720 let result = encode_default(&obj).unwrap();
721 assert!(result.contains("users[2]{id,name}:"));
722 assert!(result.contains("1,Alice"));
723 assert!(result.contains("2,Bob"));
724 }
725
726 #[test]
727 fn test_encode_empty_array() {
728 let obj = json!({"items": []});
729 let result = encode_default(&obj).unwrap();
730 assert_eq!(result, "items[0]:");
731 }
732
733 #[test]
734 fn test_encode_nested_object() {
735 let obj = json!({
736 "user": {
737 "name": "Alice",
738 "age": 30
739 }
740 });
741 let result = encode_default(&obj).unwrap();
742 assert!(result.contains("user:"));
743 assert!(result.contains("name: Alice"));
744 assert!(result.contains("age: 30"));
745 }
746
747 #[test]
748 fn test_encode_list_item_tabular_array_v3() {
749 let obj = json!({
750 "items": [
751 {
752 "users": [
753 {"id": 1, "name": "Ada"},
754 {"id": 2, "name": "Bob"}
755 ],
756 "status": "active"
757 }
758 ]
759 });
760
761 let result = encode_default(&obj).unwrap();
762
763 assert!(
764 result.contains(" - users[2]{id,name}:"),
765 "Header should be on hyphen line"
766 );
767
768 assert!(
769 result.contains(" 1,Ada"),
770 "First row should be at 6 spaces (depth +2 from hyphen). Got:\n{}",
771 result
772 );
773 assert!(
774 result.contains(" 2,Bob"),
775 "Second row should be at 6 spaces (depth +2 from hyphen). Got:\n{}",
776 result
777 );
778
779 assert!(
780 result.contains(" status: active"),
781 "Sibling field should be at 4 spaces (depth +1 from hyphen). Got:\n{}",
782 result
783 );
784 }
785
786 #[test]
787 fn test_encode_list_item_tabular_array_multiple_items() {
788 let obj = json!({
789 "data": [
790 {
791 "records": [
792 {"id": 1, "val": "x"}
793 ],
794 "count": 1
795 },
796 {
797 "records": [
798 {"id": 2, "val": "y"}
799 ],
800 "count": 1
801 }
802 ]
803 });
804
805 let result = encode_default(&obj).unwrap();
806
807 let lines: Vec<&str> = result.lines().collect();
808
809 let row_lines: Vec<&str> = lines
810 .iter()
811 .filter(|line| line.trim().starts_with(char::is_numeric))
812 .copied()
813 .collect();
814
815 for row in row_lines {
816 let spaces = row.len() - row.trim_start().len();
817 assert_eq!(
818 spaces, 6,
819 "Tabular rows should be at 6 spaces. Found {} spaces in: {}",
820 spaces, row
821 );
822 }
823 }
824
825 #[test]
826 fn test_encode_list_item_non_tabular_array_unchanged() {
827 let obj = json!({
828 "items": [
829 {
830 "tags": ["a", "b", "c"],
831 "name": "test"
832 }
833 ]
834 });
835
836 let result = encode_default(&obj).unwrap();
837
838 assert!(
839 result.contains(" - tags[3]: a,b,c"),
840 "Inline array should be on hyphen line. Got:\n{}",
841 result
842 );
843
844 assert!(
845 result.contains(" name: test"),
846 "Sibling field should be at 4 spaces. Got:\n{}",
847 result
848 );
849 }
850
851 #[test]
852 fn test_encode_list_item_tabular_array_with_nested_fields() {
853 let obj = json!({
854 "entries": [
855 {
856 "people": [
857 {"name": "Alice", "age": 30},
858 {"name": "Bob", "age": 25}
859 ],
860 "total": 2,
861 "category": "staff"
862 }
863 ]
864 });
865
866 let result = encode_default(&obj).unwrap();
867
868 assert!(result.contains(" - people[2]{name,age}:"));
869
870 assert!(result.contains(" Alice,30"));
871 assert!(result.contains(" Bob,25"));
872
873 assert!(result.contains(" total: 2"));
874 assert!(result.contains(" category: staff"));
875 }
876}