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_nested_array(
489 writer: &mut writer::Writer,
490 key: Option<&str>,
491 arr: &[Value],
492 depth: usize,
493) -> ToonResult<()> {
494 writer.write_array_header(key, arr.len(), None, depth)?;
495 writer.write_newline()?;
496 writer.push_active_delimiter(writer.options.delimiter);
497
498 for (i, val) in arr.iter().enumerate() {
499 writer.write_indent(depth + 1)?;
500 writer.write_char('-')?;
501 writer.write_char(' ')?;
502
503 match val {
504 Value::Array(inner_arr) => {
505 write_array(writer, None, inner_arr, depth + 1)?;
506 }
507 Value::Object(obj) => {
508 let keys: Vec<&String> = obj.keys().collect();
510 if let Some(first_key) = keys.first() {
511 let first_val = &obj[*first_key];
512
513 match first_val {
514 Value::Array(arr) => {
515 writer.write_key(first_key)?;
517 write_array(writer, None, arr, depth + 1)?;
518 }
519 Value::Object(nested_obj) => {
520 writer.write_key(first_key)?;
521 writer.write_char(':')?;
522 if !nested_obj.is_empty() {
523 writer.write_newline()?;
524 write_object(writer, nested_obj, depth + 3)?;
525 }
526 }
527 _ => {
528 writer.write_key(first_key)?;
529 writer.write_char(':')?;
530 writer.write_char(' ')?;
531 write_primitive_value(writer, first_val, QuotingContext::ObjectValue)?;
532 }
533 }
534
535 for key in keys.iter().skip(1) {
537 writer.write_newline()?;
538 writer.write_indent(depth + 2)?;
539
540 let value = &obj[*key];
541 match value {
542 Value::Array(arr) => {
543 writer.write_key(key)?;
544 write_array(writer, None, arr, depth + 1)?;
545 }
546 Value::Object(nested_obj) => {
547 writer.write_key(key)?;
548 writer.write_char(':')?;
549 if !nested_obj.is_empty() {
550 writer.write_newline()?;
551 write_object(writer, nested_obj, depth + 3)?;
552 }
553 }
554 _ => {
555 writer.write_key(key)?;
556 writer.write_char(':')?;
557 writer.write_char(' ')?;
558 write_primitive_value(writer, value, QuotingContext::ObjectValue)?;
559 }
560 }
561 }
562 }
563 }
564 _ => {
565 write_primitive_value(writer, val, QuotingContext::ArrayValue)?;
566 }
567 }
568
569 if i < arr.len() - 1 {
570 writer.write_newline()?;
571 }
572 }
573 writer.pop_active_delimiter();
574
575 Ok(())
576}
577
578#[cfg(test)]
579mod tests {
580 use core::f64;
581
582 use serde_json::json;
583
584 use super::*;
585
586 #[test]
587 fn test_encode_null() {
588 let value = json!(null);
589 assert_eq!(encode_default(&value).unwrap(), "null");
590 }
591
592 #[test]
593 fn test_encode_bool() {
594 assert_eq!(encode_default(&json!(true)).unwrap(), "true");
595 assert_eq!(encode_default(&json!(false)).unwrap(), "false");
596 }
597
598 #[test]
599 fn test_encode_number() {
600 assert_eq!(encode_default(&json!(42)).unwrap(), "42");
601 assert_eq!(
602 encode_default(&json!(f64::consts::PI)).unwrap(),
603 "3.141592653589793"
604 );
605 assert_eq!(encode_default(&json!(-5)).unwrap(), "-5");
606 }
607
608 #[test]
609 fn test_encode_string() {
610 assert_eq!(encode_default(&json!("hello")).unwrap(), "hello");
611 assert_eq!(
612 encode_default(&json!("hello world")).unwrap(),
613 "hello world"
614 );
615 }
616
617 #[test]
618 fn test_encode_simple_object() {
619 let obj = json!({"name": "Alice", "age": 30});
620 let result = encode_default(&obj).unwrap();
621 assert!(result.contains("name: Alice"));
622 assert!(result.contains("age: 30"));
623 }
624
625 #[test]
626 fn test_encode_primitive_array() {
627 let obj = json!({"tags": ["reading", "gaming", "coding"]});
628 let result = encode_default(&obj).unwrap();
629 assert_eq!(result, "tags[3]: reading,gaming,coding");
630 }
631
632 #[test]
633 fn test_encode_tabular_array() {
634 let obj = json!({
635 "users": [
636 {"id": 1, "name": "Alice"},
637 {"id": 2, "name": "Bob"}
638 ]
639 });
640 let result = encode_default(&obj).unwrap();
641 assert!(result.contains("users[2]{id,name}:"));
642 assert!(result.contains("1,Alice"));
643 assert!(result.contains("2,Bob"));
644 }
645
646 #[test]
647 fn test_encode_empty_array() {
648 let obj = json!({"items": []});
649 let result = encode_default(&obj).unwrap();
650 assert_eq!(result, "items[0]:");
651 }
652
653 #[test]
654 fn test_encode_nested_object() {
655 let obj = json!({
656 "user": {
657 "name": "Alice",
658 "age": 30
659 }
660 });
661 let result = encode_default(&obj).unwrap();
662 assert!(result.contains("user:"));
663 assert!(result.contains("name: Alice"));
664 assert!(result.contains("age: 30"));
665 }
666}