sdl_parser/
story.rs

1use crate::helpers::Connection;
2use crate::Formalize;
3use crate::{constants::default_speed_value, script::Script};
4use anyhow::{anyhow, Result};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, Default)]
9pub struct Story {
10    #[serde(default, alias = "Name", alias = "NAME")]
11    pub name: Option<String>,
12    #[serde(default = "default_speed_value", alias = "Speed", alias = "SPEED")]
13    pub speed: f64,
14    #[serde(alias = "Scripts", alias = "SCRIPTS")]
15    pub scripts: Vec<String>,
16    #[serde(alias = "Description", alias = "DESCRIPTION")]
17    pub description: Option<String>,
18}
19
20impl Story {
21    pub fn new(potential_speed: Option<f64>) -> Self {
22        Self {
23            speed: match potential_speed {
24                Some(speed) => speed,
25                None => default_speed_value(),
26            },
27            ..Default::default()
28        }
29    }
30}
31
32pub type Stories = HashMap<String, Story>;
33
34impl Formalize for Story {
35    fn formalize(&mut self) -> Result<()> {
36        if self.scripts.is_empty() {
37            return Err(anyhow!("Story must have have at least one Script"));
38        }
39
40        if self.speed < 1.0 {
41            return Err(anyhow!("Story speed value must be at least 1.0"));
42        }
43
44        Ok(())
45    }
46}
47
48impl Connection<Script> for (&String, &Story) {
49    fn validate_connections(&self, potential_script_names: &Option<Vec<String>>) -> Result<()> {
50        if potential_script_names.is_none() {
51            return Err(anyhow!(
52                "Story \"{story_name}\" requires at least one Script but none found under Scenario",
53                story_name = self.0
54            ));
55        };
56
57        if let Some(script_names) = potential_script_names {
58            for script_name in &self.1.scripts {
59                if !script_names.contains(script_name) {
60                    return Err(anyhow!("Script \"{script_name}\" not found under Scenario"));
61                }
62            }
63        }
64
65        Ok(())
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72    use crate::parse_sdl;
73
74    #[test]
75    fn parses_sdl_with_stories() {
76        let sdl = r#"
77                name: test-scenario
78                description: some description
79                stories: 
80                    story-1: 
81                        speed: 1
82                        scripts: 
83                            - my-cool-script
84                conditions:
85                    condition-1:
86                        command: executable/path.sh
87                        interval: 30
88                scripts:
89                    my-cool-script:
90                        start-time: 10min 2 sec
91                        end-time: 1 week 1day 1h 10 ms
92                        speed: 1.5
93                        events:
94                            my-cool-event: 20 min
95                injects:
96                    my-cool-inject:
97                        source: inject-package
98                events:
99                    my-cool-event:
100                        conditions:
101                            - condition-1
102                        injects:
103                            - my-cool-inject
104            "#;
105        let schema = parse_sdl(sdl).unwrap();
106
107        insta::with_settings!({sort_maps => true}, {
108                insta::assert_yaml_snapshot!(schema);
109        });
110    }
111
112    #[test]
113    fn parses_single_story() {
114        let story = r#" 
115            speed: 1
116            scripts: 
117                - script-1
118                - script-2
119     
120      "#;
121        serde_yaml::from_str::<Story>(story).unwrap();
122    }
123
124    #[test]
125    #[should_panic(expected = "Story speed value must be at least 1.0")]
126    fn fails_speed_is_zero() {
127        let story = r#"
128            speed: 0
129            scripts: 
130                - script-1
131                - script-2
132      "#;
133        serde_yaml::from_str::<Story>(story)
134            .unwrap()
135            .formalize()
136            .unwrap();
137    }
138
139    #[test]
140    fn adds_default_speed() {
141        let story = r#"
142            scripts: 
143                - script-1
144                - script-2
145      "#;
146
147        let story = serde_yaml::from_str::<Story>(story).unwrap();
148
149        assert_eq!(story.speed, 1.0);
150    }
151
152    #[test]
153    #[should_panic(expected = "Story must have have at least one Script")]
154    fn fails_when_scripts_is_empty() {
155        let story = r#"
156            speed: 1
157            scripts:
158      "#;
159        serde_yaml::from_str::<Story>(story)
160            .unwrap()
161            .formalize()
162            .unwrap();
163    }
164
165    #[test]
166    #[should_panic(expected = "Error(\"missing field `scripts`\", line: 2, column: 13)")]
167    fn fails_when_no_scripts_field() {
168        let story = r#"
169            speed: 15
170      "#;
171        serde_yaml::from_str::<Story>(story)
172            .unwrap()
173            .formalize()
174            .unwrap();
175    }
176
177    #[test]
178    #[should_panic(
179        expected = "Story \"story-1\" requires at least one Script but none found under Scenario"
180    )]
181    fn fails_on_script_not_defined_for_story() {
182        let sdl = r#"
183                name: test-scenario
184                description: some description
185                stories: 
186                    story-1: 
187                        speed: 1
188                        scripts: 
189                            - script-1
190            "#;
191        parse_sdl(sdl).unwrap();
192    }
193
194    #[test]
195    #[should_panic(expected = "Script \"script-1\" not found under Scenario")]
196    fn fails_on_missing_script_for_story() {
197        let sdl = r#"
198                name: test-scenario
199                description: some description
200                stories: 
201                    story-1: 
202                        speed: 1
203                        scripts: 
204                            - script-1
205                conditions:
206                    condition-1:
207                        command: executable/path.sh
208                        interval: 30
209                scripts:
210                    my-cool-script:
211                        start-time: 10min 2 sec
212                        end-time: 1 week 1day 1h 10 ms
213                        speed: 1.5
214                        events:
215                            my-cool-event: 20 min
216                injects:
217                    my-cool-inject:
218                        source: inject-package
219                events:
220                    my-cool-event:
221                        conditions:
222                            - condition-1
223                        injects:
224                            - my-cool-inject
225            "#;
226        parse_sdl(sdl).unwrap();
227    }
228}