config/
parser_config.rs

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