1use crate::{config::meta::Metadata, state::State};
2
3use serde_derive::{Deserialize, Serialize};
4
5pub(crate) const PARSER_CONFIG_LOCATION: &str = "parser";
6
7#[derive(Debug, PartialEq, Serialize, Deserialize)]
8pub(crate) struct ParserConfig {
9 pub(crate) in_progress_words: Vec<String>,
10 pub(crate) useful_status_words: Vec<String>,
11 pub(crate) choice_words: Vec<String>,
12 pub(crate) choice_phrase: Vec<String>,
13 pub(crate) completed_with_warnings: Vec<String>,
14 pub(crate) failed_with_error: Vec<String>,
15 pub(crate) finished: Vec<String>,
16 pub(crate) metadata: Metadata,
17}
18
19impl Default for ParserConfig {
20 fn default() -> Self {
21 Self {
22 in_progress_words: vec!["installing".to_string(), "creating".to_string()],
23 useful_status_words: vec![
24 "copied".to_string(),
25 "copying".to_string(),
26 "creating".to_string(),
27 "installed".to_string(),
28 "installing".to_string(),
29 "patched".to_string(),
30 "patching".to_string(),
31 "processed".to_string(),
32 "processing".to_string(),
33 ],
34 choice_words: vec![
35 "choice".to_string(),
36 "choose".to_string(),
37 "select".to_string(),
38 "enter".to_string(),
39 ],
40 choice_phrase: vec![
41 "do you want".to_string(),
42 "would you like".to_string(),
43 "answer [y]es or [n]o.".to_string(),
44 "is this correct? [y]es or [n]o".to_string(),
45 ],
46 completed_with_warnings: vec!["installed with warnings".to_string()],
47 failed_with_error: vec![
48 "not installed due to errors".to_string(),
49 "installation aborted".to_string(),
50 ],
51 finished: vec![
52 "successfully installed".to_string(),
53 "process ended".to_string(),
54 ],
55 metadata: Metadata::default(),
56 }
57 }
58}
59
60impl ParserConfig {
61 pub fn string_looks_like_question(&self, weidu_output: &str) -> bool {
62 let comparable_output = weidu_output.trim().to_ascii_lowercase();
63 for progress_word in self.in_progress_words.iter() {
65 if comparable_output.contains(progress_word) {
66 return false;
67 }
68 }
69
70 for question in self.choice_phrase.iter() {
71 if comparable_output.contains(question) {
72 return true;
73 }
74 }
75
76 for question in self.choice_words.iter() {
77 for word in comparable_output.split_whitespace() {
78 if word
79 .chars()
80 .filter(|c| c.is_alphabetic())
81 .collect::<String>()
82 == *question
83 {
84 return true;
85 }
86 }
87 }
88
89 false
90 }
91
92 pub fn detect_weidu_finished_state(&self, weidu_output: &str) -> State {
93 let comparable_output = weidu_output.trim().to_lowercase();
94 let failure = self.failed_with_error.iter().fold(false, |acc, fail_case| {
95 comparable_output.contains(fail_case) || acc
96 });
97 if failure {
98 return State::CompletedWithErrors {
99 error_details: comparable_output,
100 };
101 }
102 let warning = self
103 .completed_with_warnings
104 .iter()
105 .fold(false, |acc, warn_case| {
106 comparable_output.contains(warn_case) || acc
107 });
108 if warning {
109 return State::CompletedWithWarnings;
110 }
111 let finished = self.finished.iter().fold(false, |acc, success_case| {
112 comparable_output.contains(success_case) || acc
113 });
114 if finished {
115 return State::Completed;
116 }
117 State::InProgress
118 }
119}
120
121#[cfg(test)]
122mod tests {
123
124 use super::*;
125 use pretty_assertions::assert_eq;
126 use std::{error::Error, path::Path, result::Result};
127
128 #[test]
129 fn test_exit_warnings() -> Result<(), Box<dyn Error>> {
130 let config = ParserConfig::default();
131 let test = "INSTALLED WITH WARNINGS Additional equipment for Thieves and Bards";
132 assert_eq!(config.string_looks_like_question(test), false);
133 assert_eq!(
134 config.detect_weidu_finished_state(test),
135 State::CompletedWithWarnings
136 );
137 Ok(())
138 }
139
140 #[test]
141 fn test_exit_success() -> Result<(), Box<dyn Error>> {
142 let config = ParserConfig::default();
143 let test = "SUCCESSFULLY INSTALLED Jan's Extended Quest";
144 assert_eq!(config.string_looks_like_question(test), false);
145 assert_eq!(config.detect_weidu_finished_state(test), State::Completed);
146 Ok(())
147 }
148
149 #[test]
150 fn is_a_question() -> Result<(), Box<dyn Error>> {
151 let config = ParserConfig::default();
152 let tests = vec![
153 "Enter the full path to your Baldur's Gate installation then press Enter.",
154 "Enter the full path to your BG:EE+SoD installation then press Enter.\
155Example: C:\\Program Files (x86)\\BeamDog\\Games\\00806",
156 "[N]o, [Q]uit or choose one:",
157 "Please enter the chance for items to randomly not be randomised as a integet number (e.g. 10 for 10%)",
158 "Is this correct? [Y]es or [N]o",
159 ];
160 for test in tests {
161 assert_eq!(
162 config.string_looks_like_question(test),
163 true,
164 "String {} doesn't look like a question",
165 test
166 );
167 assert_eq!(config.detect_weidu_finished_state(test), State::InProgress);
168 assert_eq!(
169 config.useful_status_words.contains(&test.to_string()),
170 false,
171 "String {} looks like useful status words, it should only look like a question",
172 test
173 )
174 }
175 Ok(())
176 }
177
178 #[test]
179 fn is_not_a_question() -> Result<(), Box<dyn Error>> {
180 let config = ParserConfig::default();
181 let tests = vec![
182 "FAILURE:",
183 "NOT INSTALLED DUE TO ERRORS The BG1 NPC Project: Required Modifications",
184 "Creating epilogues. Too many epilogues... Why are there so many options here?",
185 "Including file(s) spellchoices_defensive/vanilla/ENCHANTER.TPH",
186 ];
187 for test in tests {
188 assert_eq!(
189 config.string_looks_like_question(test),
190 false,
191 "String {} does look like a question",
192 test
193 );
194 }
195 Ok(())
196 }
197
198 #[test]
199 fn load_config() -> Result<(), Box<dyn Error>> {
200 let root = std::env::current_dir()?;
201 let config_path = Path::join(&root, Path::new("example_config.toml"));
202 let config: ParserConfig = confy::load_path(config_path)?;
203 let mut expected = ParserConfig::default();
204 expected.metadata = config.metadata.clone();
205 assert_eq!(expected, config);
206 Ok(())
207 }
208
209 #[test]
210 fn failure() -> Result<(), Box<dyn Error>> {
211 let config = ParserConfig::default();
212 let tests = vec![
213 "not installed due to errors the bg1 npc project: required modifications",
214 "installation aborted merge dlc into game -> merge all available dlcs",
215 ];
216 for input in tests {
217 assert_eq!(
218 config.detect_weidu_finished_state(input),
219 State::CompletedWithErrors {
220 error_details: input.to_string(),
221 },
222 "Input {} did not fail",
223 input
224 );
225 }
226 Ok(())
227 }
228}