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<V: IntoJsonValue>(value: V, options: &EncodeOptions) -> ToonResult<String> {
43 let json_value = value.into_json_value();
44 encode_impl(&json_value, options)
45}
46
47fn encode_impl(value: &Value, options: &EncodeOptions) -> ToonResult<String> {
48 let normalized: Value = normalize(value.clone());
49 let mut writer = writer::Writer::new(options.clone());
50
51 match &normalized {
52 Value::Array(arr) => {
53 write_array(&mut writer, None, arr, 0)?;
54 }
55 Value::Object(obj) => {
56 write_object(&mut writer, obj, 0)?;
57 }
58 _ => {
59 write_primitive_value(&mut writer, &normalized, QuotingContext::ObjectValue)?;
60 }
61 }
62
63 Ok(writer.finish())
64}
65
66pub fn encode_default<V: IntoJsonValue>(value: V) -> ToonResult<String> {
100 encode(value, &EncodeOptions::default())
101}
102
103pub fn encode_object<V: IntoJsonValue>(value: V, options: &EncodeOptions) -> ToonResult<String> {
123 let json_value = value.into_json_value();
124 if !json_value.is_object() {
125 return Err(ToonError::TypeMismatch {
126 expected: "object".to_string(),
127 found: value_type_name(&json_value).to_string(),
128 });
129 }
130 encode_impl(&json_value, options)
131}
132
133pub fn encode_array<V: IntoJsonValue>(value: V, options: &EncodeOptions) -> ToonResult<String> {
153 let json_value = value.into_json_value();
154 if !json_value.is_array() {
155 return Err(ToonError::TypeMismatch {
156 expected: "array".to_string(),
157 found: value_type_name(&json_value).to_string(),
158 });
159 }
160 encode_impl(&json_value, options)
161}
162
163fn value_type_name(value: &Value) -> &'static str {
164 match value {
165 Value::Null => "null",
166 Value::Bool(_) => "boolean",
167 Value::Number(_) => "number",
168 Value::String(_) => "string",
169 Value::Array(_) => "array",
170 Value::Object(_) => "object",
171 }
172}
173
174fn write_object(
175 writer: &mut writer::Writer,
176 obj: &IndexMap<String, Value>,
177 depth: usize,
178) -> ToonResult<()> {
179 write_object_impl(writer, obj, depth, false)
180}
181
182fn write_object_impl(
183 writer: &mut writer::Writer,
184 obj: &IndexMap<String, Value>,
185 depth: usize,
186 disable_folding: bool,
187) -> ToonResult<()> {
188 validate_depth(depth, MAX_DEPTH)?;
189
190 let keys: Vec<&String> = obj.keys().collect();
191
192 for (i, key) in keys.iter().enumerate() {
193 if i > 0 {
194 writer.write_newline()?;
195 }
196
197 let value = &obj[*key];
198
199 let has_conflicting_sibling = keys
203 .iter()
204 .any(|k| k.starts_with(&format!("{key}.")) || (k.contains('.') && k == key));
205
206 let folded = if !disable_folding
207 && writer.options.key_folding == KeyFoldingMode::Safe
208 && !has_conflicting_sibling
209 {
210 folding::analyze_foldable_chain(key, value, writer.options.flatten_depth, &keys)
211 } else {
212 None
213 };
214
215 if let Some(chain) = folded {
216 if depth > 0 {
218 writer.write_indent(depth)?;
219 }
220
221 match &chain.leaf_value {
223 Value::Array(arr) => {
224 write_array(writer, Some(&chain.folded_key), arr, 0)?;
227 }
228 Value::Object(nested_obj) => {
229 writer.write_key(&chain.folded_key)?;
231 writer.write_char(':')?;
232 if !nested_obj.is_empty() {
233 writer.write_newline()?;
234 write_object_impl(writer, nested_obj, depth + 1, true)?;
237 }
238 }
239 _ => {
240 writer.write_key(&chain.folded_key)?;
242 writer.write_char(':')?;
243 writer.write_char(' ')?;
244 write_primitive_value(writer, &chain.leaf_value, QuotingContext::ObjectValue)?;
245 }
246 }
247 } else {
248 match value {
250 Value::Array(arr) => {
251 if depth > 0 {
252 writer.write_indent(depth)?;
253 }
254 write_array(writer, Some(key), arr, 0)?;
255 }
256 Value::Object(nested_obj) => {
257 if depth > 0 {
258 writer.write_indent(depth)?;
259 }
260 writer.write_key(key)?;
261 writer.write_char(':')?;
262 if !nested_obj.is_empty() {
263 writer.write_newline()?;
264 let nested_disable_folding = disable_folding || has_conflicting_sibling;
267 write_object_impl(writer, nested_obj, depth + 1, nested_disable_folding)?;
268 }
269 }
270 _ => {
271 if depth > 0 {
272 writer.write_indent(depth)?;
273 }
274 writer.write_key(key)?;
275 writer.write_char(':')?;
276 writer.write_char(' ')?;
277 write_primitive_value(writer, value, QuotingContext::ObjectValue)?;
278 }
279 }
280 }
281 }
282
283 Ok(())
284}
285
286fn write_array(
287 writer: &mut writer::Writer,
288 key: Option<&str>,
289 arr: &[Value],
290 depth: usize,
291) -> ToonResult<()> {
292 validate_depth(depth, MAX_DEPTH)?;
293
294 if arr.is_empty() {
295 writer.write_empty_array_with_key(key)?;
296 return Ok(());
297 }
298
299 if let Some(keys) = is_tabular_array(arr) {
302 encode_tabular_array(writer, key, arr, &keys, depth)?;
303 } else if is_primitive_array(arr) {
304 encode_primitive_array(writer, key, arr, depth)?;
305 } else {
306 encode_nested_array(writer, key, arr, depth)?;
307 }
308
309 Ok(())
310}
311
312fn is_tabular_array(arr: &[Value]) -> Option<Vec<String>> {
315 if arr.is_empty() {
316 return None;
317 }
318
319 let first = arr.first()?;
320 if !first.is_object() {
321 return None;
322 }
323
324 let first_obj = first.as_object()?;
325 let keys: Vec<String> = first_obj.keys().cloned().collect();
326
327 for value in first_obj.values() {
329 if !is_primitive(value) {
330 return None;
331 }
332 }
333
334 for val in arr.iter().skip(1) {
336 if let Some(obj) = val.as_object() {
337 if obj.len() != keys.len() {
338 return None;
339 }
340 for key in &keys {
342 if !obj.contains_key(key) {
343 return None;
344 }
345 }
346 for value in obj.values() {
348 if !is_primitive(value) {
349 return None;
350 }
351 }
352 } else {
353 return None;
354 }
355 }
356
357 Some(keys)
358}
359
360fn is_primitive(value: &Value) -> bool {
362 matches!(
363 value,
364 Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_)
365 )
366}
367
368fn is_primitive_array(arr: &[Value]) -> bool {
370 arr.iter().all(is_primitive)
371}
372
373fn encode_primitive_array(
374 writer: &mut writer::Writer,
375 key: Option<&str>,
376 arr: &[Value],
377 depth: usize,
378) -> ToonResult<()> {
379 writer.write_array_header(key, arr.len(), None, depth)?;
380 writer.write_char(' ')?;
381 writer.push_active_delimiter(writer.options.delimiter);
383
384 for (i, val) in arr.iter().enumerate() {
385 if i > 0 {
386 writer.write_delimiter()?;
387 }
388 write_primitive_value(writer, val, QuotingContext::ArrayValue)?;
389 }
390 writer.pop_active_delimiter();
391
392 Ok(())
393}
394
395fn write_primitive_value(
396 writer: &mut writer::Writer,
397 value: &Value,
398 context: QuotingContext,
399) -> ToonResult<()> {
400 match value {
401 Value::Null => writer.write_str("null"),
402 Value::Bool(b) => writer.write_str(&b.to_string()),
403 Value::Number(n) => {
404 let num_str = format_canonical_number(n);
406 writer.write_str(&num_str)
407 }
408 Value::String(s) => {
409 if writer.needs_quoting(s, context) {
410 writer.write_quoted_string(s)
411 } else {
412 writer.write_str(s)
413 }
414 }
415 _ => Err(ToonError::InvalidInput(
416 "Expected primitive value".to_string(),
417 )),
418 }
419}
420
421fn encode_tabular_array(
422 writer: &mut writer::Writer,
423 key: Option<&str>,
424 arr: &[Value],
425 keys: &[String],
426 depth: usize,
427) -> ToonResult<()> {
428 writer.write_array_header(key, arr.len(), Some(keys), depth)?;
429 writer.write_newline()?;
430
431 writer.push_active_delimiter(writer.options.delimiter);
432
433 for (row_index, obj_val) in arr.iter().enumerate() {
435 if let Some(obj) = obj_val.as_object() {
436 writer.write_indent(depth + 1)?;
437
438 for (i, key) in keys.iter().enumerate() {
439 if i > 0 {
440 writer.write_delimiter()?;
441 }
442
443 if let Some(val) = obj.get(key) {
445 write_primitive_value(writer, val, QuotingContext::ArrayValue)?;
446 } else {
447 writer.write_str("null")?;
448 }
449 }
450
451 if row_index < arr.len() - 1 {
452 writer.write_newline()?;
453 }
454 }
455 }
456
457 Ok(())
458}
459
460fn encode_nested_array(
461 writer: &mut writer::Writer,
462 key: Option<&str>,
463 arr: &[Value],
464 depth: usize,
465) -> ToonResult<()> {
466 writer.write_array_header(key, arr.len(), None, depth)?;
467 writer.write_newline()?;
468 writer.push_active_delimiter(writer.options.delimiter);
469
470 for (i, val) in arr.iter().enumerate() {
471 writer.write_indent(depth + 1)?;
472 writer.write_char('-')?;
473 writer.write_char(' ')?;
474
475 match val {
476 Value::Array(inner_arr) => {
477 write_array(writer, None, inner_arr, depth + 1)?;
478 }
479 Value::Object(obj) => {
480 let keys: Vec<&String> = obj.keys().collect();
482 if let Some(first_key) = keys.first() {
483 let first_val = &obj[*first_key];
484
485 match first_val {
486 Value::Array(arr) => {
487 writer.write_key(first_key)?;
489 write_array(writer, None, arr, depth + 1)?;
490 }
491 Value::Object(nested_obj) => {
492 writer.write_key(first_key)?;
493 writer.write_char(':')?;
494 if !nested_obj.is_empty() {
495 writer.write_newline()?;
496 write_object(writer, nested_obj, depth + 2)?;
497 }
498 }
499 _ => {
500 writer.write_key(first_key)?;
501 writer.write_char(':')?;
502 writer.write_char(' ')?;
503 write_primitive_value(writer, first_val, QuotingContext::ObjectValue)?;
504 }
505 }
506
507 for key in keys.iter().skip(1) {
509 writer.write_newline()?;
510 writer.write_indent(depth + 2)?;
511
512 let value = &obj[*key];
513 match value {
514 Value::Array(arr) => {
515 writer.write_key(key)?;
516 write_array(writer, None, arr, depth + 2)?;
517 }
518 Value::Object(nested_obj) => {
519 writer.write_key(key)?;
520 writer.write_char(':')?;
521 if !nested_obj.is_empty() {
522 writer.write_newline()?;
523 write_object(writer, nested_obj, depth + 3)?;
524 }
525 }
526 _ => {
527 writer.write_key(key)?;
528 writer.write_char(':')?;
529 writer.write_char(' ')?;
530 write_primitive_value(writer, value, QuotingContext::ObjectValue)?;
531 }
532 }
533 }
534 }
535 }
536 _ => {
537 write_primitive_value(writer, val, QuotingContext::ArrayValue)?;
538 }
539 }
540
541 if i < arr.len() - 1 {
542 writer.write_newline()?;
543 }
544 }
545 writer.pop_active_delimiter();
546
547 Ok(())
548}
549
550#[cfg(test)]
551mod tests {
552 use core::f64;
553
554 use serde_json::json;
555
556 use super::*;
557
558 #[test]
559 fn test_encode_null() {
560 let value = json!(null);
561 assert_eq!(encode_default(&value).unwrap(), "null");
562 }
563
564 #[test]
565 fn test_encode_bool() {
566 assert_eq!(encode_default(json!(true)).unwrap(), "true");
567 assert_eq!(encode_default(json!(false)).unwrap(), "false");
568 }
569
570 #[test]
571 fn test_encode_number() {
572 assert_eq!(encode_default(json!(42)).unwrap(), "42");
573 assert_eq!(
574 encode_default(json!(f64::consts::PI)).unwrap(),
575 "3.141592653589793"
576 );
577 assert_eq!(encode_default(json!(-5)).unwrap(), "-5");
578 }
579
580 #[test]
581 fn test_encode_string() {
582 assert_eq!(encode_default(json!("hello")).unwrap(), "hello");
583 assert_eq!(encode_default(json!("hello world")).unwrap(), "hello world");
584 }
585
586 #[test]
587 fn test_encode_simple_object() {
588 let obj = json!({"name": "Alice", "age": 30});
589 let result = encode_default(&obj).unwrap();
590 assert!(result.contains("name: Alice"));
591 assert!(result.contains("age: 30"));
592 }
593
594 #[test]
595 fn test_encode_primitive_array() {
596 let obj = json!({"tags": ["reading", "gaming", "coding"]});
597 let result = encode_default(&obj).unwrap();
598 assert_eq!(result, "tags[3]: reading,gaming,coding");
599 }
600
601 #[test]
602 fn test_encode_tabular_array() {
603 let obj = json!({
604 "users": [
605 {"id": 1, "name": "Alice"},
606 {"id": 2, "name": "Bob"}
607 ]
608 });
609 let result = encode_default(&obj).unwrap();
610 assert!(result.contains("users[2]{id,name}:"));
611 assert!(result.contains("1,Alice"));
612 assert!(result.contains("2,Bob"));
613 }
614
615 #[test]
616 fn test_encode_empty_array() {
617 let obj = json!({"items": []});
618 let result = encode_default(&obj).unwrap();
619 assert_eq!(result, "items[0]:");
620 }
621
622 #[test]
623 fn test_encode_nested_object() {
624 let obj = json!({
625 "user": {
626 "name": "Alice",
627 "age": 30
628 }
629 });
630 let result = encode_default(&obj).unwrap();
631 assert!(result.contains("user:"));
632 assert!(result.contains("name: Alice"));
633 assert!(result.contains("age: 30"));
634 }
635}