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 write_array(writer, Some(key), arr, depth)?;
252 }
253 Value::Object(nested_obj) => {
254 if depth > 0 {
255 writer.write_indent(depth)?;
256 }
257 writer.write_key(key)?;
258 writer.write_char(':')?;
259 if !nested_obj.is_empty() {
260 writer.write_newline()?;
261 let nested_disable_folding = disable_folding || has_conflicting_sibling;
264 write_object_impl(writer, nested_obj, depth + 1, nested_disable_folding)?;
265 }
266 }
267 _ => {
268 if depth > 0 {
269 writer.write_indent(depth)?;
270 }
271 writer.write_key(key)?;
272 writer.write_char(':')?;
273 writer.write_char(' ')?;
274 write_primitive_value(writer, value, QuotingContext::ObjectValue)?;
275 }
276 }
277 }
278 }
279
280 Ok(())
281}
282
283fn write_array(
284 writer: &mut writer::Writer,
285 key: Option<&str>,
286 arr: &[Value],
287 depth: usize,
288) -> ToonResult<()> {
289 validate_depth(depth, MAX_DEPTH)?;
290
291 if arr.is_empty() {
292 writer.write_empty_array_with_key(key, depth)?;
293 return Ok(());
294 }
295
296 if let Some(keys) = is_tabular_array(arr) {
299 encode_tabular_array(writer, key, arr, &keys, depth)?;
300 } else if is_primitive_array(arr) {
301 encode_primitive_array(writer, key, arr, depth)?;
302 } else {
303 encode_nested_array(writer, key, arr, depth)?;
304 }
305
306 Ok(())
307}
308
309fn is_tabular_array(arr: &[Value]) -> Option<Vec<String>> {
312 if arr.is_empty() {
313 return None;
314 }
315
316 let first = arr.first()?;
317 if !first.is_object() {
318 return None;
319 }
320
321 let first_obj = first.as_object()?;
322 let keys: Vec<String> = first_obj.keys().cloned().collect();
323
324 for value in first_obj.values() {
326 if !is_primitive(value) {
327 return None;
328 }
329 }
330
331 for val in arr.iter().skip(1) {
333 if let Some(obj) = val.as_object() {
334 if obj.len() != keys.len() {
335 return None;
336 }
337 for key in &keys {
339 if !obj.contains_key(key) {
340 return None;
341 }
342 }
343 for value in obj.values() {
345 if !is_primitive(value) {
346 return None;
347 }
348 }
349 } else {
350 return None;
351 }
352 }
353
354 Some(keys)
355}
356
357fn is_primitive(value: &Value) -> bool {
359 matches!(
360 value,
361 Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_)
362 )
363}
364
365fn is_primitive_array(arr: &[Value]) -> bool {
367 arr.iter().all(is_primitive)
368}
369
370fn encode_primitive_array(
371 writer: &mut writer::Writer,
372 key: Option<&str>,
373 arr: &[Value],
374 depth: usize,
375) -> ToonResult<()> {
376 writer.write_array_header(key, arr.len(), None, depth)?;
377 writer.write_char(' ')?;
378 writer.push_active_delimiter(writer.options.delimiter);
380
381 for (i, val) in arr.iter().enumerate() {
382 if i > 0 {
383 writer.write_delimiter()?;
384 }
385 write_primitive_value(writer, val, QuotingContext::ArrayValue)?;
386 }
387 writer.pop_active_delimiter();
388
389 Ok(())
390}
391
392fn write_primitive_value(
393 writer: &mut writer::Writer,
394 value: &Value,
395 context: QuotingContext,
396) -> ToonResult<()> {
397 match value {
398 Value::Null => writer.write_str("null"),
399 Value::Bool(b) => writer.write_str(&b.to_string()),
400 Value::Number(n) => {
401 let num_str = format_canonical_number(n);
403 writer.write_str(&num_str)
404 }
405 Value::String(s) => {
406 if writer.needs_quoting(s, context) {
407 writer.write_quoted_string(s)
408 } else {
409 writer.write_str(s)
410 }
411 }
412 _ => Err(ToonError::InvalidInput(
413 "Expected primitive value".to_string(),
414 )),
415 }
416}
417
418fn encode_tabular_array(
419 writer: &mut writer::Writer,
420 key: Option<&str>,
421 arr: &[Value],
422 keys: &[String],
423 depth: usize,
424) -> ToonResult<()> {
425 writer.write_array_header(key, arr.len(), Some(keys), depth)?;
426 writer.write_newline()?;
427
428 writer.push_active_delimiter(writer.options.delimiter);
429
430 for (row_index, obj_val) in arr.iter().enumerate() {
432 if let Some(obj) = obj_val.as_object() {
433 writer.write_indent(depth + 1)?;
434
435 for (i, key) in keys.iter().enumerate() {
436 if i > 0 {
437 writer.write_delimiter()?;
438 }
439
440 if let Some(val) = obj.get(key) {
442 write_primitive_value(writer, val, QuotingContext::ArrayValue)?;
443 } else {
444 writer.write_str("null")?;
445 }
446 }
447
448 if row_index < arr.len() - 1 {
449 writer.write_newline()?;
450 }
451 }
452 }
453
454 Ok(())
455}
456
457fn encode_nested_array(
458 writer: &mut writer::Writer,
459 key: Option<&str>,
460 arr: &[Value],
461 depth: usize,
462) -> ToonResult<()> {
463 writer.write_array_header(key, arr.len(), None, depth)?;
464 writer.write_newline()?;
465 writer.push_active_delimiter(writer.options.delimiter);
466
467 for (i, val) in arr.iter().enumerate() {
468 writer.write_indent(depth + 1)?;
469 writer.write_char('-')?;
470 writer.write_char(' ')?;
471
472 match val {
473 Value::Array(inner_arr) => {
474 write_array(writer, None, inner_arr, depth + 1)?;
475 }
476 Value::Object(obj) => {
477 let keys: Vec<&String> = obj.keys().collect();
479 if let Some(first_key) = keys.first() {
480 let first_val = &obj[*first_key];
481
482 match first_val {
483 Value::Array(arr) => {
484 writer.write_key(first_key)?;
486 write_array(writer, None, arr, depth + 1)?;
487 }
488 Value::Object(nested_obj) => {
489 writer.write_key(first_key)?;
490 writer.write_char(':')?;
491 if !nested_obj.is_empty() {
492 writer.write_newline()?;
493 write_object(writer, nested_obj, depth + 3)?;
494 }
495 }
496 _ => {
497 writer.write_key(first_key)?;
498 writer.write_char(':')?;
499 writer.write_char(' ')?;
500 write_primitive_value(writer, first_val, QuotingContext::ObjectValue)?;
501 }
502 }
503
504 for key in keys.iter().skip(1) {
506 writer.write_newline()?;
507 writer.write_indent(depth + 2)?;
508
509 let value = &obj[*key];
510 match value {
511 Value::Array(arr) => {
512 writer.write_key(key)?;
513 write_array(writer, None, arr, depth + 1)?;
514 }
515 Value::Object(nested_obj) => {
516 writer.write_key(key)?;
517 writer.write_char(':')?;
518 if !nested_obj.is_empty() {
519 writer.write_newline()?;
520 write_object(writer, nested_obj, depth + 3)?;
521 }
522 }
523 _ => {
524 writer.write_key(key)?;
525 writer.write_char(':')?;
526 writer.write_char(' ')?;
527 write_primitive_value(writer, value, QuotingContext::ObjectValue)?;
528 }
529 }
530 }
531 }
532 }
533 _ => {
534 write_primitive_value(writer, val, QuotingContext::ArrayValue)?;
535 }
536 }
537
538 if i < arr.len() - 1 {
539 writer.write_newline()?;
540 }
541 }
542 writer.pop_active_delimiter();
543
544 Ok(())
545}
546
547#[cfg(test)]
548mod tests {
549 use core::f64;
550
551 use serde_json::json;
552
553 use super::*;
554
555 #[test]
556 fn test_encode_null() {
557 let value = json!(null);
558 assert_eq!(encode_default(&value).unwrap(), "null");
559 }
560
561 #[test]
562 fn test_encode_bool() {
563 assert_eq!(encode_default(json!(true)).unwrap(), "true");
564 assert_eq!(encode_default(json!(false)).unwrap(), "false");
565 }
566
567 #[test]
568 fn test_encode_number() {
569 assert_eq!(encode_default(json!(42)).unwrap(), "42");
570 assert_eq!(
571 encode_default(json!(f64::consts::PI)).unwrap(),
572 "3.141592653589793"
573 );
574 assert_eq!(encode_default(json!(-5)).unwrap(), "-5");
575 }
576
577 #[test]
578 fn test_encode_string() {
579 assert_eq!(encode_default(json!("hello")).unwrap(), "hello");
580 assert_eq!(encode_default(json!("hello world")).unwrap(), "hello world");
581 }
582
583 #[test]
584 fn test_encode_simple_object() {
585 let obj = json!({"name": "Alice", "age": 30});
586 let result = encode_default(&obj).unwrap();
587 assert!(result.contains("name: Alice"));
588 assert!(result.contains("age: 30"));
589 }
590
591 #[test]
592 fn test_encode_primitive_array() {
593 let obj = json!({"tags": ["reading", "gaming", "coding"]});
594 let result = encode_default(&obj).unwrap();
595 assert_eq!(result, "tags[3]: reading,gaming,coding");
596 }
597
598 #[test]
599 fn test_encode_tabular_array() {
600 let obj = json!({
601 "users": [
602 {"id": 1, "name": "Alice"},
603 {"id": 2, "name": "Bob"}
604 ]
605 });
606 let result = encode_default(&obj).unwrap();
607 assert!(result.contains("users[2]{id,name}:"));
608 assert!(result.contains("1,Alice"));
609 assert!(result.contains("2,Bob"));
610 }
611
612 #[test]
613 fn test_encode_empty_array() {
614 let obj = json!({"items": []});
615 let result = encode_default(&obj).unwrap();
616 assert_eq!(result, "items[0]:");
617 }
618
619 #[test]
620 fn test_encode_nested_object() {
621 let obj = json!({
622 "user": {
623 "name": "Alice",
624 "age": 30
625 }
626 });
627 let result = encode_default(&obj).unwrap();
628 assert!(result.contains("user:"));
629 assert!(result.contains("name: Alice"));
630 assert!(result.contains("age: 30"));
631 }
632}