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}