1#[derive(Debug, Clone, PartialEq)]
9pub enum Value {
10 Null,
11 Bool(bool),
12 Integer(i64),
13 Float(f64),
14 String(String),
15 Array(Vec<Value>),
16 Object(Vec<(String, Value)>),
18}
19
20use crate::{KdlDocument, KdlEntry, KdlNode, KdlNumber, KdlValue};
21
22#[derive(Debug, Clone, PartialEq)]
23pub struct ValueError {
25 pub reason: ValueErrorReason,
27}
28
29impl ValueError {
30 fn new(reason: ValueErrorReason) -> Self {
31 Self { reason }
32 }
33
34 pub fn reason(&self) -> &ValueErrorReason {
36 &self.reason
37 }
38}
39
40impl core::fmt::Display for ValueError {
41 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
42 write!(f, "KDL value conversion failed: {:?}", self.reason)
43 }
44}
45
46impl std::error::Error for ValueError {}
47
48#[derive(Debug, Clone, PartialEq)]
49pub enum ValueErrorReason {
51 TopLevelMustBeObject,
52 NestedCollectionInArray,
53 EmptyArrayCannotBeConverted,
54 PropertyCannotBeConverted,
55 MixedArgumentsAndChildren,
56 UnsupportedTypeAnnotation(String),
57 NumberCannotBeConverted(String),
58}
59
60pub fn value_to_kdl_document(value: &Value) -> Result<KdlDocument, ValueError> {
75 match value {
76 Value::Object(fields) => {
77 let nodes = fields
78 .iter()
79 .map(|(key, val)| value_to_kdl_node(key, val))
80 .collect::<Result<Vec<_>, _>>()?;
81 Ok(KdlDocument { nodes })
82 }
83 _ => Err(ValueError::new(ValueErrorReason::TopLevelMustBeObject)),
84 }
85}
86
87pub fn kdl_document_to_value(doc: &KdlDocument) -> Result<Value, ValueError> {
103 let mut fields = Vec::with_capacity(doc.nodes.len());
104 for node in &doc.nodes {
105 let val = kdl_node_to_value(node)?;
106 fields.push((node.name.clone(), val));
107 }
108 Ok(Value::Object(fields))
109}
110
111const FLOAT_TYPE: &str = "f64";
121
122fn value_to_kdl_node(key: &str, value: &Value) -> Result<KdlNode, ValueError> {
124 match value {
125 Value::Object(fields) => {
127 let children = fields
128 .iter()
129 .map(|(k, v)| value_to_kdl_node(k, v))
130 .collect::<Result<Vec<_>, _>>()?;
131 Ok(KdlNode {
132 ty: None,
133 name: key.to_string(),
134 entries: Vec::new(),
135 children: Some(children),
136 })
137 }
138
139 Value::Array(items) => {
141 if items.is_empty() {
142 return Err(ValueError::new(
143 ValueErrorReason::EmptyArrayCannotBeConverted,
144 ));
145 }
146 let entries = items
147 .iter()
148 .map(primitive_to_argument)
149 .collect::<Result<Vec<_>, _>>()?;
150 Ok(KdlNode {
151 ty: None,
152 name: key.to_string(),
153 entries,
154 children: None,
155 })
156 }
157
158 _ => {
160 let entry = primitive_to_argument(value)?;
161 Ok(KdlNode {
162 ty: None,
163 name: key.to_string(),
164 entries: vec![entry],
165 children: None,
166 })
167 }
168 }
169}
170
171fn primitive_to_argument(value: &Value) -> Result<KdlEntry, ValueError> {
176 match value {
177 Value::Null => Ok(KdlEntry::Argument {
178 ty: None,
179 value: KdlValue::Null,
180 }),
181 Value::Bool(b) => Ok(KdlEntry::Argument {
182 ty: None,
183 value: KdlValue::Bool(*b),
184 }),
185 Value::Integer(i) => Ok(KdlEntry::Argument {
186 ty: None,
187 value: KdlValue::Number(i64_to_kdl_number(*i)?),
188 }),
189 Value::Float(f) => Ok(KdlEntry::Argument {
190 ty: Some(FLOAT_TYPE.to_string()),
192 value: KdlValue::Number(f64_to_kdl_number(*f)?),
193 }),
194 Value::String(s) => Ok(KdlEntry::Argument {
195 ty: None,
196 value: KdlValue::String(s.clone()),
197 }),
198 Value::Array(_) | Value::Object(_) => {
199 Err(ValueError::new(ValueErrorReason::NestedCollectionInArray))
200 }
201 }
202}
203
204fn kdl_node_to_value(node: &KdlNode) -> Result<Value, ValueError> {
206 if node
207 .entries
208 .iter()
209 .any(|entry| matches!(entry, KdlEntry::Property { .. }))
210 {
211 return Err(ValueError::new(ValueErrorReason::PropertyCannotBeConverted));
212 }
213 if let Some(ty) = &node.ty {
214 return Err(ValueError::new(
215 ValueErrorReason::UnsupportedTypeAnnotation(ty.clone()),
216 ));
217 }
218 for entry in &node.entries {
219 let KdlEntry::Argument {
220 ty: Some(ty),
221 value,
222 } = entry
223 else {
224 continue;
225 };
226 if ty != FLOAT_TYPE || !matches!(value, KdlValue::Number(_)) {
227 return Err(ValueError::new(
228 ValueErrorReason::UnsupportedTypeAnnotation(ty.clone()),
229 ));
230 }
231 }
232
233 let args: Vec<&KdlEntry> = node
234 .entries
235 .iter()
236 .filter(|e| matches!(e, KdlEntry::Argument { .. }))
237 .collect();
238
239 match (args.len(), node.children.as_ref()) {
240 (0, None) => Ok(Value::Null),
242
243 (0, Some(children)) => {
245 let mut fields = Vec::with_capacity(children.len());
246 for child in children {
247 let v = kdl_node_to_value(child)?;
248 fields.push((child.name.clone(), v));
249 }
250 Ok(Value::Object(fields))
251 }
252
253 (1, None) => kdl_entry_to_value(args[0]),
255
256 (_, None) => {
258 let items = args
259 .iter()
260 .map(|e| kdl_entry_to_value(e))
261 .collect::<Result<Vec<_>, _>>()?;
262 Ok(Value::Array(items))
263 }
264
265 (_, Some(_)) => Err(ValueError::new(ValueErrorReason::MixedArgumentsAndChildren)),
267 }
268}
269
270fn kdl_entry_to_value(entry: &KdlEntry) -> Result<Value, ValueError> {
272 let (ty, kdl_val) = match entry {
273 KdlEntry::Argument { ty, value } => (ty.as_deref(), value),
274 KdlEntry::Property { .. } => {
275 return Err(ValueError::new(ValueErrorReason::PropertyCannotBeConverted))
276 }
277 };
278
279 match kdl_val {
280 KdlValue::Null => Ok(Value::Null),
281 KdlValue::Bool(b) => Ok(Value::Bool(*b)),
282 KdlValue::String(s) => Ok(Value::String(s.clone())),
283 KdlValue::Number(n) => {
284 if ty == Some(FLOAT_TYPE) {
286 match n.as_f64() {
287 Some(f) => Ok(Value::Float(f)),
288 None => Err(ValueError::new(ValueErrorReason::NumberCannotBeConverted(
289 n.raw().to_string(),
290 ))),
291 }
292 } else {
293 match n.as_i64() {
295 Some(i) => Ok(Value::Integer(i)),
296 None => match n.as_f64() {
297 Some(f) => Ok(Value::Float(f)),
298 None => Err(ValueError::new(ValueErrorReason::NumberCannotBeConverted(
299 n.raw().to_string(),
300 ))),
301 },
302 }
303 }
304 }
305 }
306}
307
308fn i64_to_kdl_number(i: i64) -> Result<KdlNumber, ValueError> {
313 let raw = i.to_string();
314 KdlNumber::new(raw.clone(), Some(i), Some(i as f64))
315 .map_err(|_| ValueError::new(ValueErrorReason::NumberCannotBeConverted(raw)))
316}
317
318fn f64_to_kdl_number(f: f64) -> Result<KdlNumber, ValueError> {
319 let raw = format_f64(f);
323 KdlNumber::new(raw.clone(), None, Some(f))
324 .map_err(|_| ValueError::new(ValueErrorReason::NumberCannotBeConverted(raw)))
325}
326
327fn format_f64(f: f64) -> String {
329 if f.is_nan() {
330 return "#nan".to_string();
331 }
332 if f.is_infinite() {
333 return if f > 0.0 {
334 "#inf".to_string()
335 } else {
336 "#-inf".to_string()
337 };
338 }
339
340 let s = format!("{f}");
344 if s.contains('.') || s.contains('e') || s.contains('E') || s.starts_with('#') {
345 s
346 } else {
347 format!("{s}.0")
348 }
349}
350
351#[cfg(test)]
356mod tests {
357 use super::*;
358
359 fn roundtrip(v: &Value) -> Value {
361 let doc = value_to_kdl_document(v).expect("encode failed");
362 kdl_document_to_value(&doc).expect("decode failed")
363 }
364
365 #[test]
370 fn roundtrip_single_string_node() {
371 let input = Value::Object(vec![(
372 "text".to_string(),
373 Value::String("hello".to_string()),
374 )]);
375 let output = roundtrip(&input);
376 assert_eq!(input, output);
377 }
378
379 #[test]
384 fn roundtrip_array_node() {
385 let input = Value::Object(vec![(
386 "langs".to_string(),
387 Value::Array(vec![
388 Value::String("en".to_string()),
389 Value::String("ja".to_string()),
390 ]),
391 )]);
392 let output = roundtrip(&input);
393 assert_eq!(input, output);
394 }
395
396 #[test]
401 fn roundtrip_nested_object() {
402 let root_ref = Value::Object(vec![
403 ("cid".to_string(), Value::String("bafyCID".to_string())),
404 ("uri".to_string(), Value::String("at://x".to_string())),
405 ]);
406 let input = Value::Object(vec![(
407 "reply".to_string(),
408 Value::Object(vec![("root".to_string(), root_ref)]),
409 )]);
410 let output = roundtrip(&input);
411 assert_eq!(input, output);
412 }
413
414 #[test]
419 fn roundtrip_integer() {
420 let input = Value::Object(vec![("count".to_string(), Value::Integer(42))]);
421 let output = roundtrip(&input);
422 assert_eq!(input, output);
423 match &output {
425 Value::Object(fields) => {
426 assert_eq!(fields[0].1, Value::Integer(42));
427 }
428 _ => panic!("expected Object"),
429 }
430 }
431
432 #[test]
433 fn roundtrip_float() {
434 let input = Value::Object(vec![("ratio".to_string(), Value::Float(2.5))]);
435 let output = roundtrip(&input);
436 match &output {
437 Value::Object(fields) => {
438 if let Value::Float(f) = fields[0].1 {
439 assert!((f - 2.5_f64).abs() < 1e-10);
440 } else {
441 panic!("expected Float, got {:?}", fields[0].1);
442 }
443 }
444 _ => panic!("expected Object"),
445 }
446 }
447
448 #[test]
449 fn non_finite_floats_are_accepted() {
450 for (value, raw) in [
451 (f64::INFINITY, "#inf"),
452 (f64::NEG_INFINITY, "#-inf"),
453 (f64::NAN, "#nan"),
454 ] {
455 let parsed = crate::parse(&format!("number {raw}")).unwrap();
456 let decoded = kdl_document_to_value(&parsed).unwrap();
457 let Value::Object(fields) = decoded else {
458 panic!("expected object")
459 };
460 let Value::Float(decoded) = fields[0].1 else {
461 panic!("expected float")
462 };
463 if value.is_nan() {
464 assert!(decoded.is_nan());
465 } else {
466 assert_eq!(decoded, value);
467 }
468
469 let input = Value::Object(vec![("number".to_string(), Value::Float(value))]);
470 let document = value_to_kdl_document(&input).unwrap();
471 let KdlEntry::Argument {
472 value: KdlValue::Number(number),
473 ..
474 } = &document.nodes[0].entries[0]
475 else {
476 panic!("expected number")
477 };
478 assert_eq!(number.raw(), raw);
479
480 let decoded = kdl_document_to_value(&document).unwrap();
481 let Value::Object(fields) = decoded else {
482 panic!("expected object")
483 };
484 let Value::Float(decoded) = fields[0].1 else {
485 panic!("expected float")
486 };
487 if value.is_nan() {
488 assert!(decoded.is_nan());
489 } else {
490 assert_eq!(decoded, value);
491 }
492 }
493 }
494
495 #[test]
496 fn integer_and_float_are_distinct() {
497 let int_input = Value::Object(vec![("n".to_string(), Value::Integer(1))]);
498 let flt_input = Value::Object(vec![("n".to_string(), Value::Float(1.0))]);
499
500 let int_out = roundtrip(&int_input);
501 let flt_out = roundtrip(&flt_input);
502
503 assert_ne!(int_out, flt_out);
505
506 match &int_out {
507 Value::Object(f) => assert!(matches!(f[0].1, Value::Integer(_))),
508 _ => panic!(),
509 }
510 match &flt_out {
511 Value::Object(f) => assert!(matches!(f[0].1, Value::Float(_))),
512 _ => panic!(),
513 }
514 }
515
516 #[test]
521 fn roundtrip_null() {
522 let input = Value::Object(vec![("deleted".to_string(), Value::Null)]);
523 let output = roundtrip(&input);
524 assert_eq!(input, output);
525 }
526
527 #[test]
532 fn roundtrip_bool() {
533 let input = Value::Object(vec![
534 ("active".to_string(), Value::Bool(true)),
535 ("deleted".to_string(), Value::Bool(false)),
536 ]);
537 let output = roundtrip(&input);
538 assert_eq!(input, output);
539 }
540
541 #[test]
546 fn roundtrip_negative_integer() {
547 let input = Value::Object(vec![("offset".to_string(), Value::Integer(-7))]);
548 let output = roundtrip(&input);
549 assert_eq!(input, output);
550 }
551
552 #[test]
557 fn top_level_non_object_is_error() {
558 let result = value_to_kdl_document(&Value::String("oops".to_string()));
559 assert_eq!(
560 result.unwrap_err().reason,
561 ValueErrorReason::TopLevelMustBeObject
562 );
563 }
564
565 #[test]
570 fn roundtrip_via_text() {
571 use crate::{normalize, parse};
572
573 let input = Value::Object(vec![
574 ("name".to_string(), Value::String("Alice".to_string())),
575 ("age".to_string(), Value::Integer(30)),
576 ("score".to_string(), Value::Float(9.5)),
577 ("active".to_string(), Value::Bool(true)),
578 (
579 "meta".to_string(),
580 Value::Object(vec![(
581 "role".to_string(),
582 Value::String("admin".to_string()),
583 )]),
584 ),
585 ]);
586
587 let doc = value_to_kdl_document(&input).unwrap();
588 let text = normalize(&doc);
589 let doc2 = parse(&text).unwrap();
590 let output = kdl_document_to_value(&doc2).unwrap();
591
592 match (&input, &output) {
594 (Value::Object(a), Value::Object(b)) => {
595 assert_eq!(a.len(), b.len());
596 assert_eq!(a[0], b[0]); assert_eq!(a[1], b[1]); if let (Value::Float(fa), Value::Float(fb)) = (&a[2].1, &b[2].1) {
600 assert!((fa - fb).abs() < 1e-10);
601 } else {
602 panic!("expected Float for score");
603 }
604 assert_eq!(a[3], b[3]); assert_eq!(a[4], b[4]); }
607 _ => panic!("expected Object"),
608 }
609 }
610
611 #[test]
616 fn empty_node_decoded_as_null() {
617 let doc = KdlDocument {
619 nodes: vec![KdlNode {
620 ty: None,
621 name: "empty".to_string(),
622 entries: vec![],
623 children: None,
624 }],
625 };
626 let val = kdl_document_to_value(&doc).unwrap();
627 assert_eq!(val, Value::Object(vec![("empty".to_string(), Value::Null)]));
628 }
629
630 #[test]
631 fn empty_object_roundtrips_through_structure_and_text() {
632 use crate::{parse, serialize};
633
634 let input = Value::Object(vec![("empty".to_string(), Value::Object(vec![]))]);
635 let doc = value_to_kdl_document(&input).unwrap();
636 assert_eq!(doc.nodes[0].children, Some(vec![]));
637 assert_eq!(kdl_document_to_value(&doc).unwrap(), input);
638
639 let text = serialize(&doc);
640 assert_eq!(text, "empty {\n}\n");
641 let reparsed = parse(&text).unwrap();
642 assert_eq!(reparsed.nodes[0].children, Some(vec![]));
643 assert_eq!(kdl_document_to_value(&reparsed).unwrap(), input);
644 }
645
646 #[test]
647 fn empty_array_is_rejected() {
648 let input = Value::Object(vec![("empty".to_string(), Value::Array(vec![]))]);
649 assert_eq!(
650 value_to_kdl_document(&input).unwrap_err().reason,
651 ValueErrorReason::EmptyArrayCannotBeConverted
652 );
653 }
654
655 #[test]
656 fn nested_collections_in_array_are_rejected() {
657 for nested in [Value::Array(vec![]), Value::Object(vec![])] {
658 let input = Value::Object(vec![(
659 "items".to_string(),
660 Value::Array(vec![Value::Integer(1), nested]),
661 )]);
662 assert_eq!(
663 value_to_kdl_document(&input).unwrap_err().reason,
664 ValueErrorReason::NestedCollectionInArray
665 );
666 }
667 }
668
669 #[test]
670 fn properties_are_rejected_with_and_without_arguments() {
671 for input in ["node key=1", "node 1 key=2"] {
672 let doc = crate::parse(input).unwrap();
673 assert_eq!(
674 kdl_document_to_value(&doc).unwrap_err().reason,
675 ValueErrorReason::PropertyCannotBeConverted
676 );
677 }
678 }
679
680 #[test]
681 fn unsupported_type_annotations_are_rejected() {
682 for (input, expected) in [
683 (
684 "(node_ty)node 1",
685 ValueErrorReason::UnsupportedTypeAnnotation("node_ty".to_string()),
686 ),
687 (
688 "node (entry_ty)1",
689 ValueErrorReason::UnsupportedTypeAnnotation("entry_ty".to_string()),
690 ),
691 (
692 "node (f64)\"text\"",
693 ValueErrorReason::UnsupportedTypeAnnotation("f64".to_string()),
694 ),
695 ] {
696 let doc = crate::parse(input).unwrap();
697 assert_eq!(kdl_document_to_value(&doc).unwrap_err().reason, expected);
698 }
699 }
700
701 #[test]
702 fn mixed_arguments_and_children_are_rejected() {
703 let doc = crate::parse("node 1 { child }").unwrap();
704 assert_eq!(
705 kdl_document_to_value(&doc).unwrap_err().reason,
706 ValueErrorReason::MixedArgumentsAndChildren
707 );
708 }
709
710 #[test]
711 fn number_decode_preserves_supported_values_and_rejects_uninterpreted_raw() {
712 let hex = crate::parse("node 0xff").unwrap();
713 assert_eq!(
714 kdl_document_to_value(&hex).unwrap(),
715 Value::Object(vec![("node".to_string(), Value::Integer(255))])
716 );
717
718 let infinity = crate::parse("node #inf").unwrap();
719 let decoded = kdl_document_to_value(&infinity).unwrap();
720 let Value::Object(fields) = decoded else {
721 panic!("expected object")
722 };
723 let Value::Float(value) = fields[0].1 else {
724 panic!("expected float")
725 };
726 assert!(value.is_infinite() && value > 0.0);
727
728 let invalid = KdlDocument {
729 nodes: vec![KdlNode {
730 ty: None,
731 name: "node".to_string(),
732 entries: vec![KdlEntry::Argument {
733 ty: None,
734 value: KdlValue::Number(KdlNumber {
735 raw: "999999999999999999999999999999999999999999".to_string(),
736 as_i64: None,
737 as_f64: None,
738 }),
739 }],
740 children: None,
741 }],
742 };
743 assert_eq!(
744 kdl_document_to_value(&invalid).unwrap_err().reason,
745 ValueErrorReason::NumberCannotBeConverted(
746 "999999999999999999999999999999999999999999".to_string()
747 )
748 );
749 }
750
751 #[test]
752 fn rejection_priority_is_deterministic() {
753 let argument = KdlEntry::Argument {
754 ty: Some("entry_ty".to_string()),
755 value: KdlValue::Number(KdlNumber {
756 raw: "1".to_string(),
757 as_i64: Some(1),
758 as_f64: Some(1.0),
759 }),
760 };
761 let property = KdlEntry::Property {
762 key: "key".to_string(),
763 ty: None,
764 value: KdlValue::Bool(true),
765 };
766 let child = KdlNode {
767 ty: None,
768 name: "child".to_string(),
769 entries: vec![],
770 children: None,
771 };
772 let reason = |ty, entries| {
773 let doc = KdlDocument {
774 nodes: vec![KdlNode {
775 ty,
776 name: "node".to_string(),
777 entries,
778 children: Some(vec![child.clone()]),
779 }],
780 };
781 kdl_document_to_value(&doc).unwrap_err().reason
782 };
783
784 assert_eq!(
785 reason(
786 Some("node_ty".to_string()),
787 vec![argument.clone(), property]
788 ),
789 ValueErrorReason::PropertyCannotBeConverted
790 );
791 assert_eq!(
792 reason(Some("node_ty".to_string()), vec![argument.clone()]),
793 ValueErrorReason::UnsupportedTypeAnnotation("node_ty".to_string())
794 );
795 assert_eq!(
796 reason(None, vec![argument.clone()]),
797 ValueErrorReason::UnsupportedTypeAnnotation("entry_ty".to_string())
798 );
799 let untyped = KdlEntry::Argument {
800 ty: None,
801 value: argument.value().clone(),
802 };
803 assert_eq!(
804 reason(None, vec![untyped]),
805 ValueErrorReason::MixedArgumentsAndChildren
806 );
807 }
808}