Skip to main content

mnk_vmf/types/entity/
output.rs

1use chumsky::Parser as ChumskyParser;
2
3use crate::parser::{any_quoted_string, TokenError, TokenSource};
4
5/// Represents an output connection between entities
6#[derive(Debug, Default, Clone)]
7pub struct EntityOutput<'src> {
8    pub output_name: &'src str,
9    pub target: &'src str,
10    pub input: &'src str,
11    pub parameter: &'src str,
12    pub delay: f32,
13    pub times_to_fire: i32,
14}
15
16impl<'src> EntityOutput<'src> {
17    /// Parse an output string in the format: "target,input,parameter,delay,times_to_fire"
18    /// Example: "motor*,TurnOn,,0,-1"
19    pub fn parse_output_string(output_name: &'src str, value: &'src str) -> Result<Self, String> {
20        let mut parts = value.split(',').map(|split| split.trim());
21
22        let (target, input, parameter, delay, times_to_fire) = match (
23            parts.next(),
24            parts.next(),
25            parts.next(),
26            parts.next(),
27            parts.next(),
28        ) {
29            (Some(a), Some(b), Some(c), Some(d), Some(e)) => {
30                let delay = d
31                    .parse::<f32>()
32                    .map_err(|e| format!("invalid delay '{}': {}", d, e))?;
33                let times_to_fire = e
34                    .parse::<i32>()
35                    .map_err(|e| format!("invalid times_to_fire '{}': {}", e, e))?;
36                (a, b, c, delay, times_to_fire)
37            }
38            _ => return Err("expected at least 5 comma-separated values".into()),
39        };
40
41        Ok(EntityOutput::<'src> {
42            output_name,
43            target,
44            input,
45            parameter,
46            delay,
47            times_to_fire,
48        })
49    }
50}
51
52/// Parser for a single output key-value pair
53/// Format: "OutputName" "target,input,parameter,delay,times"
54pub(crate) fn parse_output_entry<'src, I>(
55) -> impl ChumskyParser<'src, I, EntityOutput<'src>, TokenError<'src>>
56where
57    I: TokenSource<'src>,
58{
59    use chumsky::error::Rich;
60
61    any_quoted_string()
62        .then(any_quoted_string())
63        .try_map(|(output_name, value_str), span| {
64            EntityOutput::parse_output_string(output_name, value_str)
65                .map_err(|err_msg| Rich::custom(span, format!("Invalid output: {}", err_msg)))
66        })
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72    use crate::util::lex;
73
74    #[test]
75    fn test_parse_output_string_complete() {
76        let result = EntityOutput::parse_output_string("OnIn", "motor*,TurnOn,,0,-1");
77        assert!(result.is_ok());
78
79        let output = result.unwrap();
80        assert_eq!(output.output_name, "OnIn");
81        assert_eq!(output.target, "motor*");
82        assert_eq!(output.input, "TurnOn");
83        assert_eq!(output.parameter, "");
84        assert_eq!(output.delay, 0.0);
85        assert_eq!(output.times_to_fire, -1);
86    }
87
88    #[test]
89    fn test_parse_output_string_with_parameter() {
90        let result = EntityOutput::parse_output_string("OnStartTouch", "door1,Open,fast,0.5,1");
91        assert!(result.is_ok());
92
93        let output = result.unwrap();
94        assert_eq!(output.output_name, "OnStartTouch");
95        assert_eq!(output.target, "door1");
96        assert_eq!(output.input, "Open");
97        assert_eq!(output.parameter, "fast");
98        assert_eq!(output.delay, 0.5);
99        assert_eq!(output.times_to_fire, 1);
100    }
101
102    #[test]
103    fn test_parse_output_string_invalid_format() {
104        let result = EntityOutput::parse_output_string("OnUse", "target,input,param");
105        assert!(result.is_err());
106    }
107
108    #[test]
109    fn test_parse_output_string_invalid_delay() {
110        let result = EntityOutput::parse_output_string("OnUse", "target,input,,not_a_number,-1");
111        assert!(result.is_err());
112    }
113
114    #[test]
115    fn test_parse_output_string_invalid_times() {
116        let result = EntityOutput::parse_output_string("OnUse", "target,input,,0,not_a_number");
117        assert!(result.is_err());
118    }
119
120    #[test]
121    fn test_parse_output_entry() {
122        let input = r#""OnIn" "motor*,TurnOn,,0,-1""#;
123        let stream = lex(input);
124
125        let parser = parse_output_entry();
126        let result = parser.parse(stream).into_result();
127
128        assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
129        let output = result.unwrap();
130        assert_eq!(output.output_name, "OnIn");
131        assert_eq!(output.target, "motor*");
132        assert_eq!(output.input, "TurnOn");
133        assert_eq!(output.delay, 0.0);
134        assert_eq!(output.times_to_fire, -1);
135    }
136
137    #[test]
138    fn test_parse_output_entry_with_spaces() {
139        let input = r#""OnOut" "motor*, TurnOff, , 0.5, 1""#;
140        let stream = lex(input);
141
142        let parser = parse_output_entry();
143        let result = parser.parse(stream).into_result();
144
145        assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
146        let output = result.unwrap();
147        assert_eq!(output.output_name, "OnOut");
148        assert_eq!(output.target, "motor*");
149        assert_eq!(output.input, "TurnOff");
150        assert_eq!(output.parameter, "");
151        assert_eq!(output.delay, 0.5);
152        assert_eq!(output.times_to_fire, 1);
153    }
154}