Skip to main content

neco_kdl/
value.rs

1/// KDL v2 ドキュメント。ゼロ個以上のノードで構成される。
2#[derive(Debug, Clone, PartialEq)]
3pub struct KdlDocument {
4    pub nodes: Vec<KdlNode>,
5}
6
7impl KdlDocument {
8    /// ドキュメント内のノード一覧を返す。
9    pub fn nodes(&self) -> &[KdlNode] {
10        &self.nodes
11    }
12}
13
14/// KDL v2 ノード。名前、エントリ群(argument + property)、子ノードを持つ。
15#[derive(Debug, Clone, PartialEq)]
16pub struct KdlNode {
17    /// type annotation `(type)name`
18    pub ty: Option<String>,
19    /// ノード名
20    pub name: String,
21    /// argument と property を出現順で保持
22    pub entries: Vec<KdlEntry>,
23    /// children block `{ ... }`
24    pub children: Option<Vec<KdlNode>>,
25}
26
27impl KdlNode {
28    /// type annotation を返す。
29    pub fn ty(&self) -> Option<&str> {
30        self.ty.as_deref()
31    }
32
33    /// ノード名を返す。
34    pub fn name(&self) -> &str {
35        &self.name
36    }
37
38    /// エントリ一覧を返す。
39    pub fn entries(&self) -> &[KdlEntry] {
40        &self.entries
41    }
42
43    /// 子ノードを返す。
44    pub fn children(&self) -> Option<&[KdlNode]> {
45        self.children.as_deref()
46    }
47}
48
49impl KdlNode {
50    /// named property を key で検索し、最初にマッチした値を返す。
51    pub fn get(&self, key: &str) -> Option<&KdlValue> {
52        self.entries().iter().find_map(|entry| match entry {
53            KdlEntry::Property { key: k, value, .. } if k == key => Some(value),
54            _ => None,
55        })
56    }
57}
58
59impl KdlNode {
60    /// 最初の Argument エントリの値を返す。 Property は skip する。
61    pub fn first_arg(&self) -> Option<&KdlValue> {
62        self.entries.iter().find_map(|e| match e {
63            KdlEntry::Argument { value, .. } => Some(value),
64            _ => None,
65        })
66    }
67
68    /// 最初の Argument エントリが文字列であればその参照を返す。
69    ///
70    /// ```
71    /// use neco_kdl::parse;
72    /// let doc = parse("name \"alice\"\n").unwrap();
73    /// assert_eq!(doc.nodes()[0].first_string_arg(), Some("alice"));
74    /// ```
75    pub fn first_string_arg(&self) -> Option<&str> {
76        self.first_arg().and_then(KdlValue::as_str)
77    }
78
79    /// Argument エントリのうち文字列値だけを出現順で返す。
80    pub fn string_args(&self) -> impl Iterator<Item = &str> {
81        self.entries.iter().filter_map(|entry| match entry {
82            KdlEntry::Argument { value, .. } => value.as_str(),
83            _ => None,
84        })
85    }
86
87    /// Argument エントリの値を出現順で返す。
88    pub fn arg_values(&self) -> impl Iterator<Item = &KdlValue> {
89        self.entries.iter().filter_map(|entry| match entry {
90            KdlEntry::Argument { value, .. } => Some(value),
91            _ => None,
92        })
93    }
94
95    /// 同名の最初の子ノードを返す。
96    ///
97    /// ```
98    /// use neco_kdl::parse;
99    /// let doc = parse("parent {\n    item \"a\"\n    item \"b\"\n}\n").unwrap();
100    /// let parent = &doc.nodes()[0];
101    /// assert_eq!(parent.find_child("item").and_then(|c| c.first_string_arg()), Some("a"));
102    /// ```
103    pub fn find_child(&self, name: &str) -> Option<&KdlNode> {
104        self.children.as_deref()?.iter().find(|c| c.name == name)
105    }
106
107    /// 同名の子ノードをすべて返す iterator。
108    pub fn find_children<'s>(&'s self, name: &'s str) -> impl Iterator<Item = &'s KdlNode> + 's {
109        self.children
110            .as_deref()
111            .into_iter()
112            .flat_map(|s| s.iter())
113            .filter(move |c| c.name == name)
114    }
115
116    /// 名前付き property を文字列として返す。
117    pub fn string_prop(&self, key: &str) -> Option<&str> {
118        self.get(key).and_then(KdlValue::as_str)
119    }
120
121    /// 名前付き property を bool として返す。
122    pub fn bool_prop(&self, key: &str) -> Option<bool> {
123        self.get(key).and_then(KdlValue::as_bool)
124    }
125
126    /// 名前付き property を i64 として返す。
127    pub fn int_prop(&self, key: &str) -> Option<i64> {
128        self.get(key).and_then(KdlValue::as_i64)
129    }
130
131    /// 同名子ノードのうち first_string_arg が取れるものを集める。
132    pub fn string_child_values(&self, child_name: &str) -> Vec<&str> {
133        self.children
134            .as_deref()
135            .into_iter()
136            .flat_map(|s| s.iter())
137            .filter(|c| c.name == child_name)
138            .filter_map(|c| c.first_string_arg())
139            .collect()
140    }
141}
142
143/// ノードのエントリ。argument(位置引数)または property(名前付き引数)。
144#[derive(Debug, Clone, PartialEq)]
145pub enum KdlEntry {
146    Argument {
147        ty: Option<String>,
148        value: KdlValue,
149    },
150    Property {
151        key: String,
152        ty: Option<String>,
153        value: KdlValue,
154    },
155}
156
157impl KdlEntry {
158    /// エントリの値を返す。
159    pub fn value(&self) -> &KdlValue {
160        match self {
161            KdlEntry::Argument { value, .. } => value,
162            KdlEntry::Property { value, .. } => value,
163        }
164    }
165
166    /// エントリの type annotation を返す。
167    pub fn ty(&self) -> Option<&str> {
168        match self {
169            KdlEntry::Argument { ty, .. } | KdlEntry::Property { ty, .. } => ty.as_deref(),
170        }
171    }
172}
173
174/// 入力 UTF-8 byte 列の半開区間。source 由来の位置だけを表す。
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub struct KdlSpan {
177    /// 開始 byte offset ( この位置を含む )
178    pub start: usize,
179    /// 終了 byte offset ( この位置を含まない )
180    pub end: usize,
181}
182
183/// source span を伴う KDL v2 ドキュメント。生成入口は parse_spanned だけである。
184#[derive(Debug, Clone, PartialEq)]
185pub struct SpannedKdlDocument {
186    nodes: Vec<SpannedKdlNode>,
187}
188
189impl SpannedKdlDocument {
190    pub(crate) fn new(nodes: Vec<SpannedKdlNode>) -> Self {
191        Self { nodes }
192    }
193
194    /// ドキュメント内の位置付きノード一覧を返す。
195    pub fn nodes(&self) -> &[SpannedKdlNode] {
196        &self.nodes
197    }
198
199    /// source 由来の span を除去し通常の `KdlDocument` へ変換する一方向の変換。
200    pub fn into_document(self) -> KdlDocument {
201        KdlDocument {
202            nodes: self
203                .nodes
204                .into_iter()
205                .map(SpannedKdlNode::into_node)
206                .collect(),
207        }
208    }
209}
210
211/// source span を伴う KDL v2 ノード。境界の規則は README に記す。
212#[derive(Debug, Clone, PartialEq)]
213pub struct SpannedKdlNode {
214    span: KdlSpan,
215    ty: Option<String>,
216    name: String,
217    entries: Vec<SpannedKdlEntry>,
218    children: Option<Vec<SpannedKdlNode>>,
219}
220
221impl SpannedKdlNode {
222    pub(crate) fn new(
223        span: KdlSpan,
224        ty: Option<String>,
225        name: String,
226        entries: Vec<SpannedKdlEntry>,
227        children: Option<Vec<SpannedKdlNode>>,
228    ) -> Self {
229        Self {
230            span,
231            ty,
232            name,
233            entries,
234            children,
235        }
236    }
237
238    /// ノードの source span を返す。
239    pub fn span(&self) -> KdlSpan {
240        self.span
241    }
242
243    /// type annotation を返す。
244    pub fn ty(&self) -> Option<&str> {
245        self.ty.as_deref()
246    }
247
248    /// ノード名を返す。
249    pub fn name(&self) -> &str {
250        &self.name
251    }
252
253    /// 位置付きエントリ一覧を返す。
254    pub fn entries(&self) -> &[SpannedKdlEntry] {
255        &self.entries
256    }
257
258    /// 位置付き子ノードを返す。
259    pub fn children(&self) -> Option<&[SpannedKdlNode]> {
260        self.children.as_deref()
261    }
262
263    /// source 由来の span を除去し通常の `KdlNode` へ変換する。
264    pub fn into_node(self) -> KdlNode {
265        KdlNode {
266            ty: self.ty,
267            name: self.name,
268            entries: self
269                .entries
270                .into_iter()
271                .map(SpannedKdlEntry::into_entry)
272                .collect(),
273            children: self
274                .children
275                .map(|nodes| nodes.into_iter().map(SpannedKdlNode::into_node).collect()),
276        }
277    }
278}
279
280/// source span を伴う KDL v2 エントリ。境界の規則は README に記す。
281#[derive(Debug, Clone, PartialEq)]
282pub struct SpannedKdlEntry {
283    span: KdlSpan,
284    entry: KdlEntry,
285}
286
287impl SpannedKdlEntry {
288    pub(crate) fn new(span: KdlSpan, entry: KdlEntry) -> Self {
289        Self { span, entry }
290    }
291
292    /// エントリの source span を返す。
293    pub fn span(&self) -> KdlSpan {
294        self.span
295    }
296
297    /// 通常のエントリ値を返す。
298    pub fn entry(&self) -> &KdlEntry {
299        &self.entry
300    }
301
302    fn into_entry(self) -> KdlEntry {
303        self.entry
304    }
305}
306
307/// KDL v2 の値。
308#[derive(Debug, Clone, PartialEq)]
309pub enum KdlValue {
310    String(String),
311    Number(KdlNumber),
312    Bool(bool),
313    Null,
314}
315
316impl KdlValue {
317    /// 文字列値を返す。
318    pub fn as_str(&self) -> Option<&str> {
319        match self {
320            KdlValue::String(s) => Some(s),
321            _ => None,
322        }
323    }
324
325    /// bool 値を返す。
326    pub fn as_bool(&self) -> Option<bool> {
327        match self {
328            KdlValue::Bool(b) => Some(*b),
329            _ => None,
330        }
331    }
332
333    /// f64 値を返す。
334    pub fn as_f64(&self) -> Option<f64> {
335        match self {
336            KdlValue::Number(n) => n.as_f64(),
337            _ => None,
338        }
339    }
340
341    /// i64 値を返す。
342    pub fn as_i64(&self) -> Option<i64> {
343        match self {
344            KdlValue::Number(n) => n.as_i64(),
345            _ => None,
346        }
347    }
348}
349
350/// 数値の raw 文字列を保持しつつ、可能な場合は解釈済み値も提供する。
351///
352/// KDL v2 は数値サイズに制限を置かないため、i64/f64 に収まらない値も受理する。
353#[derive(Debug, Clone)]
354pub struct KdlNumber {
355    /// 原文(アンダースコア・プレフィックス含む)
356    pub(crate) raw: String,
357    /// 整数として解釈可能な場合
358    pub(crate) as_i64: Option<i64>,
359    /// 浮動小数点として解釈可能な場合(#inf, #-inf, #nan 含む)
360    pub(crate) as_f64: Option<f64>,
361}
362
363impl KdlNumber {
364    /// 原文を返す。
365    pub fn raw(&self) -> &str {
366        &self.raw
367    }
368
369    /// 整数として解釈可能な場合の値を返す。
370    pub fn as_i64(&self) -> Option<i64> {
371        self.as_i64
372    }
373
374    /// 浮動小数点として解釈可能な場合の値を返す。
375    pub fn as_f64(&self) -> Option<f64> {
376        self.as_f64
377    }
378}
379
380/// `KdlNumber` の raw と解釈値が一致しない場合の構築エラー。
381#[derive(Debug, Clone, Copy, PartialEq, Eq)]
382pub struct NumberError;
383
384impl core::fmt::Display for NumberError {
385    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
386        write!(f, "KDL number raw and interpreted values do not match")
387    }
388}
389
390impl std::error::Error for NumberError {}
391
392impl PartialEq for KdlNumber {
393    fn eq(&self, other: &Self) -> bool {
394        self.raw == other.raw
395    }
396}
397
398/// パースエラー。
399#[derive(Debug, Clone, PartialEq)]
400pub struct KdlError {
401    /// 1-based 行番号
402    pub(crate) line: usize,
403    /// 1-based 列番号(Unicode scalar value 単位)
404    pub(crate) col: usize,
405    /// エラー種別
406    pub(crate) kind: KdlErrorKind,
407}
408
409impl KdlError {
410    /// 行番号(1-based)を返す。
411    pub fn line(&self) -> usize {
412        self.line
413    }
414
415    /// 列番号(1-based、Unicode scalar value 単位)を返す。
416    pub fn col(&self) -> usize {
417        self.col
418    }
419
420    /// エラー種別を返す。
421    pub fn kind(&self) -> &KdlErrorKind {
422        &self.kind
423    }
424}
425
426impl core::fmt::Display for KdlError {
427    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
428        write!(f, "{}:{}: {}", self.line, self.col, self.kind)
429    }
430}
431
432/// エラー種別。
433#[derive(Debug, Clone, PartialEq)]
434pub enum KdlErrorKind {
435    /// 予期しない文字
436    UnexpectedChar(char),
437    /// 予期しない EOF
438    UnexpectedEof,
439    /// 不正な文字列エスケープ
440    InvalidEscape,
441    /// 不正な Unicode エスケープ
442    InvalidUnicodeEscape,
443    /// 不正な数値リテラル
444    InvalidNumber,
445    /// 禁止コードポイント
446    DisallowedCodePoint(char),
447    /// 裸キーワード(true, false, null, inf, -inf, nan)
448    BareKeyword,
449    /// ネストされていないブロックコメント終端
450    UnmatchedBlockCommentEnd,
451    /// 閉じられていないブロックコメント
452    UnclosedBlockComment,
453    /// 閉じられていない文字列
454    UnclosedString,
455    /// multiline string のインデント不一致
456    InconsistentIndentation,
457    /// slashdash の不正な位置
458    InvalidSlashdash,
459}
460
461impl core::fmt::Display for KdlErrorKind {
462    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
463        match self {
464            Self::UnexpectedChar(c) => write!(f, "unexpected character: {:?}", c),
465            Self::UnexpectedEof => write!(f, "unexpected end of input"),
466            Self::InvalidEscape => write!(f, "invalid escape sequence"),
467            Self::InvalidUnicodeEscape => write!(f, "invalid unicode escape"),
468            Self::InvalidNumber => write!(f, "invalid number literal"),
469            Self::DisallowedCodePoint(c) => {
470                write!(f, "disallowed code point: U+{:04X}", *c as u32)
471            }
472            Self::BareKeyword => write!(f, "bare keyword (use #true, #false, #null, etc.)"),
473            Self::UnmatchedBlockCommentEnd => write!(f, "unmatched */"),
474            Self::UnclosedBlockComment => write!(f, "unclosed block comment"),
475            Self::UnclosedString => write!(f, "unclosed string"),
476            Self::InconsistentIndentation => write!(f, "inconsistent multiline string indentation"),
477            Self::InvalidSlashdash => write!(f, "slashdash in invalid position"),
478        }
479    }
480}
481
482#[cfg(test)]
483mod tests {
484    use super::*;
485
486    fn make_number(raw: &str, i: Option<i64>, f: Option<f64>) -> KdlNumber {
487        KdlNumber {
488            raw: raw.to_string(),
489            as_i64: i,
490            as_f64: f,
491        }
492    }
493
494    #[test]
495    fn number_constructor_accepts_matching_components() {
496        assert!(KdlNumber::new("5", Some(5), Some(5.0)).is_ok());
497        assert!(KdlNumber::new("1.0", None, Some(1.0)).is_ok());
498        assert!(KdlNumber::new("#nan", None, Some(f64::NAN)).is_ok());
499        assert!(KdlNumber::new("-0", Some(0), Some(-0.0)).is_ok());
500        assert!(KdlNumber::new("-0.0", None, Some(-0.0)).is_ok());
501    }
502
503    #[test]
504    fn number_constructor_rejects_invalid_raw() {
505        for raw in ["", "0xG", "0x", "0b2", "+"] {
506            assert_eq!(KdlNumber::new(raw, None, None), Err(NumberError));
507        }
508    }
509
510    #[test]
511    fn number_constructor_rejects_mismatched_components() {
512        assert_eq!(KdlNumber::new("5", Some(5), Some(999.0)), Err(NumberError));
513        assert_eq!(KdlNumber::new("5", None, None), Err(NumberError));
514        assert_eq!(KdlNumber::new("-0", Some(0), Some(0.0)), Err(NumberError));
515        assert_eq!(KdlNumber::new("-0.0", None, Some(0.0)), Err(NumberError));
516    }
517
518    // --- KdlValue::as_str ---
519
520    #[test]
521    fn as_str_returns_some_for_string() {
522        let v = KdlValue::String("hello".to_string());
523        assert_eq!(v.as_str(), Some("hello"));
524    }
525
526    #[test]
527    fn as_str_returns_none_for_number() {
528        let v = KdlValue::Number(make_number("42", Some(42), Some(42.0)));
529        assert_eq!(v.as_str(), None);
530    }
531
532    // --- KdlValue::as_bool ---
533
534    #[test]
535    fn as_bool_returns_some_for_bool() {
536        assert_eq!(KdlValue::Bool(true).as_bool(), Some(true));
537        assert_eq!(KdlValue::Bool(false).as_bool(), Some(false));
538    }
539
540    #[test]
541    fn as_bool_returns_none_for_string() {
542        let v = KdlValue::String("true".to_string());
543        assert_eq!(v.as_bool(), None);
544    }
545
546    // --- KdlValue::as_f64 ---
547
548    #[test]
549    fn as_f64_returns_some_for_number() {
550        let v = KdlValue::Number(make_number("2.5", None, Some(2.5)));
551        assert_eq!(v.as_f64(), Some(2.5));
552    }
553
554    #[test]
555    fn as_f64_returns_none_for_bool() {
556        assert_eq!(KdlValue::Bool(true).as_f64(), None);
557    }
558
559    // --- KdlValue::as_i64 ---
560
561    #[test]
562    fn as_i64_returns_some_for_integer() {
563        let v = KdlValue::Number(make_number("42", Some(42), Some(42.0)));
564        assert_eq!(v.as_i64(), Some(42));
565    }
566
567    #[test]
568    fn as_i64_returns_none_for_float_only() {
569        let v = KdlValue::Number(make_number("2.5", None, Some(2.5)));
570        assert_eq!(v.as_i64(), None);
571    }
572
573    // --- KdlEntry::value ---
574
575    #[test]
576    fn entry_value_for_argument() {
577        let entry = KdlEntry::Argument {
578            ty: None,
579            value: KdlValue::String("arg".to_string()),
580        };
581        assert_eq!(entry.value(), &KdlValue::String("arg".to_string()));
582    }
583
584    #[test]
585    fn entry_value_for_property() {
586        let entry = KdlEntry::Property {
587            key: "key".to_string(),
588            ty: None,
589            value: KdlValue::Bool(true),
590        };
591        assert_eq!(entry.value(), &KdlValue::Bool(true));
592    }
593
594    // --- KdlNode::get ---
595
596    #[test]
597    fn node_get_returns_some_for_existing_key() {
598        let node = KdlNode {
599            ty: None,
600            name: "test".to_string(),
601            entries: vec![
602                KdlEntry::Argument {
603                    ty: None,
604                    value: KdlValue::String("positional".to_string()),
605                },
606                KdlEntry::Property {
607                    key: "color".to_string(),
608                    ty: None,
609                    value: KdlValue::String("red".to_string()),
610                },
611            ],
612            children: None,
613        };
614        assert_eq!(
615            node.get("color"),
616            Some(&KdlValue::String("red".to_string()))
617        );
618    }
619
620    #[test]
621    fn node_get_returns_none_for_missing_key() {
622        let node = KdlNode {
623            ty: None,
624            name: "test".to_string(),
625            entries: vec![KdlEntry::Argument {
626                ty: None,
627                value: KdlValue::String("positional".to_string()),
628            }],
629            children: None,
630        };
631        assert_eq!(node.get("missing"), None);
632    }
633
634    // --- helpers for navigation tests ---
635
636    fn arg(value: KdlValue) -> KdlEntry {
637        KdlEntry::Argument { ty: None, value }
638    }
639
640    fn prop(key: &str, value: KdlValue) -> KdlEntry {
641        KdlEntry::Property {
642            key: key.to_string(),
643            ty: None,
644            value,
645        }
646    }
647
648    fn typed_arg(ty: &str, value: KdlValue) -> KdlEntry {
649        KdlEntry::Argument {
650            ty: Some(ty.to_string()),
651            value,
652        }
653    }
654
655    fn typed_prop(key: &str, ty: &str, value: KdlValue) -> KdlEntry {
656        KdlEntry::Property {
657            key: key.to_string(),
658            ty: Some(ty.to_string()),
659            value,
660        }
661    }
662
663    fn node(name: &str, entries: Vec<KdlEntry>, children: Option<Vec<KdlNode>>) -> KdlNode {
664        KdlNode {
665            ty: None,
666            name: name.to_string(),
667            entries,
668            children,
669        }
670    }
671
672    // --- KdlNode::first_arg / first_string_arg ---
673
674    #[test]
675    fn first_arg_skips_property() {
676        let n = node(
677            "n",
678            vec![
679                prop("k", KdlValue::Bool(true)),
680                arg(KdlValue::String("v".to_string())),
681            ],
682            None,
683        );
684        assert_eq!(n.first_arg(), Some(&KdlValue::String("v".to_string())));
685    }
686
687    #[test]
688    fn first_arg_returns_none_when_no_args() {
689        let n = node("n", vec![prop("k", KdlValue::Bool(true))], None);
690        assert_eq!(n.first_arg(), None);
691    }
692
693    #[test]
694    fn first_string_arg_for_string() {
695        let n = node("n", vec![arg(KdlValue::String("hi".to_string()))], None);
696        assert_eq!(n.first_string_arg(), Some("hi"));
697    }
698
699    #[test]
700    fn first_string_arg_for_non_string() {
701        let n = node(
702            "n",
703            vec![arg(KdlValue::Number(make_number("1", Some(1), Some(1.0))))],
704            None,
705        );
706        assert_eq!(n.first_string_arg(), None);
707    }
708
709    // --- KdlNode::find_child / find_children ---
710
711    #[test]
712    fn find_child_returns_first_match() {
713        let parent = node(
714            "p",
715            vec![],
716            Some(vec![
717                node("a", vec![arg(KdlValue::String("first".to_string()))], None),
718                node("a", vec![arg(KdlValue::String("second".to_string()))], None),
719            ]),
720        );
721        assert_eq!(
722            parent.find_child("a").map(|c| c.first_string_arg()),
723            Some(Some("first")),
724        );
725    }
726
727    #[test]
728    fn find_child_returns_none_when_no_children() {
729        let n = node("n", vec![], None);
730        assert!(n.find_child("a").is_none());
731    }
732
733    #[test]
734    fn find_children_iterates_all_matches() {
735        let parent = node(
736            "p",
737            vec![],
738            Some(vec![
739                node("a", vec![arg(KdlValue::String("1".to_string()))], None),
740                node("b", vec![], None),
741                node("a", vec![arg(KdlValue::String("2".to_string()))], None),
742            ]),
743        );
744        let collected: Vec<_> = parent.find_children("a").map(|c| c.name.clone()).collect();
745        assert_eq!(collected, vec!["a".to_string(), "a".to_string()]);
746    }
747
748    #[test]
749    fn find_children_empty_for_no_match() {
750        let parent = node("p", vec![], Some(vec![node("a", vec![], None)]));
751        assert_eq!(parent.find_children("z").count(), 0);
752    }
753
754    // --- KdlNode::string_prop / bool_prop / int_prop ---
755
756    #[test]
757    fn string_prop_for_string_value() {
758        let n = node(
759            "n",
760            vec![prop("k", KdlValue::String("hello".to_string()))],
761            None,
762        );
763        assert_eq!(n.string_prop("k"), Some("hello"));
764    }
765
766    #[test]
767    fn string_prop_for_wrong_type() {
768        let n = node(
769            "n",
770            vec![prop(
771                "k",
772                KdlValue::Number(make_number("1", Some(1), Some(1.0))),
773            )],
774            None,
775        );
776        assert_eq!(n.string_prop("k"), None);
777    }
778
779    #[test]
780    fn string_prop_missing_key() {
781        let n = node("n", vec![prop("k", KdlValue::Bool(true))], None);
782        assert_eq!(n.string_prop("absent"), None);
783    }
784
785    #[test]
786    fn bool_prop_for_bool_value() {
787        let n = node("n", vec![prop("flag", KdlValue::Bool(true))], None);
788        assert_eq!(n.bool_prop("flag"), Some(true));
789    }
790
791    #[test]
792    fn bool_prop_for_wrong_type() {
793        let n = node(
794            "n",
795            vec![prop("flag", KdlValue::String("true".to_string()))],
796            None,
797        );
798        assert_eq!(n.bool_prop("flag"), None);
799    }
800
801    #[test]
802    fn bool_prop_missing_key() {
803        let n = node("n", vec![], None);
804        assert_eq!(n.bool_prop("absent"), None);
805    }
806
807    #[test]
808    fn int_prop_for_int_value() {
809        let n = node(
810            "n",
811            vec![prop(
812                "count",
813                KdlValue::Number(make_number("42", Some(42), Some(42.0))),
814            )],
815            None,
816        );
817        assert_eq!(n.int_prop("count"), Some(42));
818    }
819
820    #[test]
821    fn int_prop_for_float_only() {
822        let n = node(
823            "n",
824            vec![prop(
825                "x",
826                KdlValue::Number(make_number("2.5", None, Some(2.5))),
827            )],
828            None,
829        );
830        assert_eq!(n.int_prop("x"), None);
831    }
832
833    #[test]
834    fn int_prop_missing_key() {
835        let n = node("n", vec![], None);
836        assert_eq!(n.int_prop("absent"), None);
837    }
838
839    // --- KdlNode::string_child_values ---
840
841    #[test]
842    fn string_child_values_collects_string_args() {
843        let parent = node(
844            "p",
845            vec![],
846            Some(vec![
847                node("item", vec![arg(KdlValue::String("a".to_string()))], None),
848                node("item", vec![arg(KdlValue::String("b".to_string()))], None),
849                node("other", vec![arg(KdlValue::String("z".to_string()))], None),
850            ]),
851        );
852        assert_eq!(parent.string_child_values("item"), vec!["a", "b"]);
853    }
854
855    #[test]
856    fn string_child_values_skips_non_string() {
857        let parent = node(
858            "p",
859            vec![],
860            Some(vec![
861                node("item", vec![arg(KdlValue::String("a".to_string()))], None),
862                node(
863                    "item",
864                    vec![arg(KdlValue::Number(make_number("1", Some(1), Some(1.0))))],
865                    None,
866                ),
867            ]),
868        );
869        assert_eq!(parent.string_child_values("item"), vec!["a"]);
870    }
871
872    // --- KdlEntry::ty ---
873
874    #[test]
875    fn entry_ty_for_typed_argument() {
876        let e = typed_arg(
877            "u32",
878            KdlValue::Number(make_number("1", Some(1), Some(1.0))),
879        );
880        assert_eq!(e.ty(), Some("u32"));
881    }
882
883    #[test]
884    fn entry_ty_for_untyped_argument() {
885        let e = arg(KdlValue::Bool(true));
886        assert_eq!(e.ty(), None);
887    }
888
889    #[test]
890    fn entry_ty_for_typed_property() {
891        let e = typed_prop(
892            "k",
893            "i64",
894            KdlValue::Number(make_number("7", Some(7), Some(7.0))),
895        );
896        assert_eq!(e.ty(), Some("i64"));
897    }
898
899    #[test]
900    fn entry_ty_for_untyped_property() {
901        let e = prop("k", KdlValue::Bool(false));
902        assert_eq!(e.ty(), None);
903    }
904}