Skip to main content

neco_kdl/
convert.rs

1/// Format-agnostic value type for KDL conversion.
2///
3/// This enum serves as an intermediate representation between KDL documents
4/// and other data formats (JSON, CBOR, etc.) without requiring external
5/// dependencies in the neco-kdl crate.
6///
7/// Integer(i64) and Float(f64) are kept distinct, unlike JSON's single Number(f64).
8#[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    /// Order-preserving map of key-value pairs.
17    Object(Vec<(String, Value)>),
18}
19
20use crate::{KdlDocument, KdlEntry, KdlNode, KdlNumber, KdlValue};
21
22#[derive(Debug, Clone, PartialEq)]
23/// Failure returned when a KDL document and `Value` cannot be converted losslessly.
24pub struct ValueError {
25    /// The conversion rule that rejected the input.
26    pub reason: ValueErrorReason,
27}
28
29impl ValueError {
30    fn new(reason: ValueErrorReason) -> Self {
31        Self { reason }
32    }
33
34    /// Returns the conversion rule that rejected the input.
35    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)]
49/// Explicit reasons that a KDL document and `Value` cannot be converted losslessly.
50pub enum ValueErrorReason {
51    TopLevelMustBeObject,
52    NestedCollectionInArray,
53    EmptyArrayCannotBeConverted,
54    PropertyCannotBeConverted,
55    MixedArgumentsAndChildren,
56    UnsupportedTypeAnnotation(String),
57    NumberCannotBeConverted(String),
58}
59
60/// Converts a `Value` into a `KdlDocument`.
61///
62/// Only `Value::Object` can be represented as a KDL document (since KDL is a
63/// collection of named nodes). Each key in the object becomes a node name.
64///
65/// Conversion rules:
66/// - Primitive values (Bool/Integer/Float/String/Null) → single positional argument
67/// - Array values → multiple positional arguments
68/// - Nested Object values → children block
69///
70/// # Errors
71///
72/// Returns an error for a non-Object top level, an empty Array, a nested
73/// collection inside an Array, or a number that cannot be constructed.
74pub 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
87/// Converts a `KdlDocument` back into a `Value`.
88///
89/// The document is always decoded as a `Value::Object` where each node name
90/// becomes a key.
91///
92/// Decoding rules for node arguments:
93/// - No positional arguments and no children → `Value::Null`
94/// - Exactly one positional argument, no children → scalar value
95/// - Two or more positional arguments, no children → `Value::Array`
96/// - Children block (no positional arguments) → nested `Value::Object`
97///
98/// # Errors
99///
100/// Returns an error if a node cannot be decoded (e.g., mixed arguments and
101/// children, or an unrecognised value type).
102pub 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
111// ---------------------------------------------------------------------------
112// Internal helpers
113// ---------------------------------------------------------------------------
114
115/// A sentinel type-annotation string used to distinguish `Value::Float` from
116/// `Value::Integer` when both are stored as KDL numbers.
117///
118/// KDL's `KdlNumber` stores a raw string; we use a `(f64)` type annotation on
119/// the argument to mark that the original value was a `Value::Float`.
120const FLOAT_TYPE: &str = "f64";
121
122/// Converts a single key-value pair into a `KdlNode`.
123fn value_to_kdl_node(key: &str, value: &Value) -> Result<KdlNode, ValueError> {
124    match value {
125        // Nested object → children block
126        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        // Array → multiple positional arguments
140        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        // Primitive → single positional argument
159        _ => {
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
171/// Converts a primitive `Value` (non-Array, non-Object) into a `KdlEntry::Argument`.
172///
173/// `Value::Float` gets a `(f64)` type annotation so that round-trip decoding
174/// can distinguish it from `Value::Integer`.
175fn 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            // Use a (f64) type annotation to distinguish from Integer on decode.
191            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
204/// Decodes a `KdlNode` into a `Value`.
205fn 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        // No arguments and no children → null
241        (0, None) => Ok(Value::Null),
242
243        // Children block only → nested Object
244        (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        // Exactly one argument → scalar
254        (1, None) => kdl_entry_to_value(args[0]),
255
256        // Multiple arguments → Array
257        (_, 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        // Both arguments and children → ambiguous, treat as error
266        (_, Some(_)) => Err(ValueError::new(ValueErrorReason::MixedArgumentsAndChildren)),
267    }
268}
269
270/// Decodes a single `KdlEntry::Argument` into a scalar `Value`.
271fn 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            // A (f64) type annotation marks an explicitly floating-point value.
285            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                // No annotation: prefer integer interpretation, fall back to float.
294                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
308// ---------------------------------------------------------------------------
309// Number construction helpers
310// ---------------------------------------------------------------------------
311
312fn 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    // Produce a raw string that round-trips through the KDL parser.
320    // We always emit at least one decimal digit so the parser recognises it as
321    // a float (e.g. "2.5", "1.0").
322    let raw = format_f64(f);
323    KdlNumber::new(raw.clone(), None, Some(f))
324        .map_err(|_| ValueError::new(ValueErrorReason::NumberCannotBeConverted(raw)))
325}
326
327/// Formats an f64 as a KDL-parseable decimal string with an explicit dot.
328fn 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    // Use Rust's default Display which includes the decimal point when
341    // needed.  For whole numbers like 1.0, Display produces "1" : we
342    // append ".0" explicitly.
343    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// ---------------------------------------------------------------------------
352// Tests
353// ---------------------------------------------------------------------------
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358
359    // Helper: round-trip a Value through KdlDocument and back.
360    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    // -----------------------------------------------------------------------
366    // Single scalar node: text "hello"
367    // -----------------------------------------------------------------------
368
369    #[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    // -----------------------------------------------------------------------
380    // Multiple-value node: langs "en" "ja"  →  Array
381    // -----------------------------------------------------------------------
382
383    #[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    // -----------------------------------------------------------------------
397    // Nested Object → children block
398    // -----------------------------------------------------------------------
399
400    #[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    // -----------------------------------------------------------------------
415    // Integer vs Float are preserved
416    // -----------------------------------------------------------------------
417
418    #[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        // Verify the decoded value is still Integer, not Float.
424        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        // After round-trip they must remain distinct.
504        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    // -----------------------------------------------------------------------
517    // Null
518    // -----------------------------------------------------------------------
519
520    #[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    // -----------------------------------------------------------------------
528    // Bool
529    // -----------------------------------------------------------------------
530
531    #[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    // -----------------------------------------------------------------------
542    // Negative integer
543    // -----------------------------------------------------------------------
544
545    #[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    // -----------------------------------------------------------------------
553    // Error: non-Object at top level
554    // -----------------------------------------------------------------------
555
556    #[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    // -----------------------------------------------------------------------
566    // Round-trip via normalize() + parse() (full text serialization)
567    // -----------------------------------------------------------------------
568
569    #[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        // Check field by field (Float comparison needs epsilon).
593        match (&input, &output) {
594            (Value::Object(a), Value::Object(b)) => {
595                assert_eq!(a.len(), b.len());
596                assert_eq!(a[0], b[0]); // name
597                assert_eq!(a[1], b[1]); // age
598                                        // score: float comparison
599                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]); // active
605                assert_eq!(a[4], b[4]); // meta
606            }
607            _ => panic!("expected Object"),
608        }
609    }
610
611    // -----------------------------------------------------------------------
612    // Node with no arguments decodes as Null
613    // -----------------------------------------------------------------------
614
615    #[test]
616    fn empty_node_decoded_as_null() {
617        // Build a document with a node that has no arguments.
618        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}