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