Skip to main content

vihaco_parser_core/
lib.rs

1// SPDX-FileCopyrightText: 2026 The vihaco Authors
2// SPDX-License-Identifier: MIT
3
4pub mod impls;
5
6pub use impls::ident;
7
8use chumsky::error::Simple;
9use chumsky::extra;
10
11/// A parser whose input is `&'src str` (char stream) and whose error type is `Simple<char>`.
12///
13/// The lifetime `'src` is the input lifetime. Output type `Self` is owned and does not borrow
14/// from the input.
15pub trait Parse<'src>: Sized {
16    fn parser() -> impl chumsky::Parser<'src, &'src str, Self, extra::Err<Simple<'src, char>>>;
17}
18
19#[cfg(test)]
20mod tests {
21    use super::*;
22    use chumsky::Parser;
23
24    fn parses<'src, T: Parse<'src>>(input: &'src str) -> T {
25        T::parser().parse(input).into_result().unwrap()
26    }
27
28    #[test]
29    fn i64_basic() {
30        assert_eq!(parses::<i64>("42"), 42);
31    }
32    #[test]
33    fn i32_basic() {
34        assert_eq!(parses::<i32>("7"), 7);
35    }
36    #[test]
37    fn u64_basic() {
38        assert_eq!(parses::<u64>("100"), 100);
39    }
40    #[test]
41    fn u32_basic() {
42        assert_eq!(parses::<u32>("0"), 0);
43    }
44    #[test]
45    fn usize_basic() {
46        assert_eq!(parses::<usize>("9"), 9);
47    }
48    #[test]
49    fn f64_int() {
50        assert_eq!(parses::<f64>("3"), 3.0);
51    }
52    #[test]
53    #[allow(clippy::approx_constant)]
54    fn f64_float() {
55        assert_eq!(parses::<f64>("3.14"), 3.14);
56    }
57    #[test]
58    fn f32_float() {
59        assert!((parses::<f32>("1.5") - 1.5f32).abs() < 1e-6);
60    }
61    #[test]
62    fn i64_negative() {
63        assert_eq!(parses::<i64>("-42"), -42);
64    }
65    #[test]
66    fn i32_negative() {
67        assert_eq!(parses::<i32>("-7"), -7);
68    }
69    #[test]
70    fn f64_negative() {
71        assert_eq!(parses::<f64>("-0.5"), -0.5);
72    }
73    #[test]
74    fn f64_negative_scientific() {
75        assert_eq!(parses::<f64>("-1.0e-3"), -1.0e-3);
76    }
77    #[test]
78    fn u64_rejects_negative() {
79        assert!(u64::parser().parse("-1").into_result().is_err());
80    }
81    #[test]
82    fn bool_true() {
83        assert!(parses::<bool>("true"));
84    }
85    #[test]
86    fn bool_false() {
87        assert!(!parses::<bool>("false"));
88    }
89    #[test]
90    fn string_word() {
91        assert_eq!(parses::<String>("hello"), "hello");
92    }
93
94    #[test]
95    fn string_stops_at_ws() {
96        // Without a trailing end(), Parser::parse() requires consuming all input — so a
97        // String parser given "hello world" fails because " world" is left unconsumed.
98        // Use lazy() / nested combinators for composition; that's not this test's job.
99        let result = String::parser().parse("hello world").into_result();
100        assert!(result.is_err());
101    }
102
103    #[test]
104    fn ident_operand_with_colons() {
105        assert_eq!(
106            ident().parse("AOD0:T1:A").into_result().unwrap(),
107            "AOD0:T1:A"
108        );
109    }
110
111    #[test]
112    fn ident_stops_at_comma() {
113        let result = ident()
114            .then_ignore(chumsky::primitive::just(','))
115            .parse("foo,")
116            .into_result();
117        assert_eq!(result.unwrap(), "foo");
118    }
119
120    #[test]
121    fn ident_allows_dots() {
122        assert_eq!(ident().parse("a.b.c").into_result().unwrap(), "a.b.c");
123    }
124
125    #[test]
126    fn ident_digi_target() {
127        assert_eq!(ident().parse("DIGI:0").into_result().unwrap(), "DIGI:0");
128    }
129
130    #[test]
131    fn ident_rejects_empty() {
132        assert!(ident().parse("").into_result().is_err());
133    }
134
135    #[test]
136    fn ident_rejects_leading_ws() {
137        assert!(ident().parse("  hello").into_result().is_err());
138    }
139
140    #[test]
141    fn ident_stops_at_open_paren() {
142        let result = ident()
143            .then_ignore(chumsky::primitive::just('('))
144            .parse("foo(")
145            .into_result();
146        assert_eq!(result.unwrap(), "foo");
147    }
148
149    #[test]
150    fn ident_stops_at_brace() {
151        let result = ident()
152            .then_ignore(chumsky::primitive::just('{'))
153            .parse("device{")
154            .into_result();
155        assert_eq!(result.unwrap(), "device");
156    }
157}