arma_preset_parser/
lib.rs

1extern crate custom_error;
2use crate::ParserError::*;
3use custom_error::custom_error;
4use std::{fs::File, io::Read};
5
6/// Mod struct. Stores the id, name, link and whether the mod is from the workshop or not
7#[derive(Clone, Eq, PartialEq, Debug, Ord, PartialOrd)]
8pub struct Mod {
9    pub id: i64,
10    pub name: String,
11    pub from_steam: bool,
12    pub link: String,
13}
14
15/// Preset struct. Stores the preset name and a vector of all the mods
16#[derive(Clone, PartialEq, Debug)]
17pub struct Preset {
18    pub name: String,
19    pub mods: Vec<Mod>,
20}
21
22custom_error! {pub TagErrType
23    HtmlRoot = "Unable to find root html tag. Is this even a html file?",
24    PresetName = "Unable to find PresetName metatag",
25    Modlist = "Unable to find mod-list div",
26    ModTable = "Unable to find table tag",
27    DataTypeText = "Unable to find text in data-type attribute children",
28    ModLinkAnchor = "Unable to find remote mod link",
29    ModLinkSpan = "Unable to find local mod link",
30}
31
32custom_error! {pub ParserError
33    StringConvFail = "Failed to read file to string",
34    DocParseErr = "Failed to parse document with error",
35    DocReadErr = "Failed to read file from path",
36    TagFindErr{tag_type: TagErrType} = "Unable to find tag with error: {tag_type}"
37}
38
39impl Preset {
40    /// This function returns a preset from a file object
41    ///
42    /// # Examples
43    ///
44    /// ```rust
45    /// match std::fs::File::open(some_path) {
46    ///     Ok(file) => {
47    ///         match arma_preset_parser::Preset::from_file(file) {
48    ///             Ok(preset) => println!("{:?}", preset),
49    ///             Err(e) => println!("{}", e)
50    ///         };
51    ///     },
52    ///     Err(e) => println!("{}", e)
53    /// };
54    /// ```
55    pub fn from_file(file: File) -> Result<Self, ParserError> {
56        parse(file)
57    }
58
59    /// This function returns a preset from a String filepath
60    ///
61    /// # Examples
62    ///
63    /// ```rust
64    /// match arma_preset_parser::Preset::from_fs("some_path".parse().unwrap()) {
65    ///     Ok(preset) => println!("{:?}", preset),
66    ///     Err(e) => println!("{}", e)
67    /// };
68    /// ```
69    pub fn from_fs(path: String) -> Result<Self, ParserError> {
70        let file: File = match File::open(path) {
71            Ok(f) => f,
72            Err(_) => return Err(DocReadErr),
73        };
74        parse(file)
75    }
76
77    /// This function returns a vector of mods from a preset
78    ///
79    /// # Examples
80    ///
81    /// ```rust
82    /// match arma_preset_parser::Preset::from_fs("some_path".parse().unwrap()) {
83    ///     Ok(preset) => println!("{:?}", preset.as_mods()),
84    ///     Err(e) => println!("{}", e)
85    /// };
86    /// ```
87    pub fn as_mods(&self) -> Vec<Mod> {
88        let mut vec = vec![];
89        for _mod in &self.mods {
90            vec.push(_mod.clone())
91        }
92        vec
93    }
94
95    /// This function returns a vector of mod ids from a preset
96    ///
97    /// # Examples
98    ///
99    /// ```rust
100    /// match arma_preset_parser::Preset::from_fs("some_path".parse().unwrap()) {
101    ///     Ok(preset) => println!("{:?}", preset.as_ids()),
102    ///     Err(e) => println!("{}", e)
103    /// };
104    /// ```
105    pub fn as_ids(&self) -> Vec<i64> {
106        let mut vec = vec![];
107        for _mod in &self.mods {
108            vec.push(_mod.clone().id)
109        }
110        vec
111    }
112
113    /// This function returns a vector of mod names from a preset
114    ///
115    /// # Examples
116    ///
117    /// ```rust
118    /// match arma_preset_parser::Preset::from_fs("some_path".parse().unwrap()) {
119    ///     Ok(preset) => println!("{:?}", preset.as_names()),
120    ///     Err(e) => println!("{}", e)
121    /// };
122    /// ```
123    pub fn as_names(&self) -> Vec<String> {
124        let mut vec = vec![];
125        for _mod in &self.mods {
126            vec.push(_mod.clone().name)
127        }
128        vec
129    }
130
131    /// This function returns a vector of mod names from a preset
132    ///
133    /// # Examples
134    ///
135    /// ```rust
136    /// match arma_preset_parser::Preset::from_fs("some_path".parse().unwrap()) {
137    ///     Ok(preset) => println!("{:?}", preset.as_links()),
138    ///     Err(e) => println!("{}", e)
139    /// };
140    /// ```
141    pub fn as_links(&self) -> Vec<String> {
142        let mut vec = vec![];
143        for _mod in &self.mods {
144            vec.push(_mod.clone().link)
145        }
146        vec
147    }
148}
149
150/// Actual parser for the preset. Constructed around the lovely roxmltree package
151/// This function takes a file object as the input and presents the user with a Result containing both the preset and some custom error responses (see above)
152/// This is an entirely internal function and as such does not contain any external-facing components
153fn parse(mut file: File) -> Result<Preset, ParserError> {
154    let mut contents: String = "".to_string();
155
156    match file.read_to_string(&mut contents) {
157        Ok(_) => {}
158        Err(_) => return Err(StringConvFail),
159    };
160
161    match roxmltree::Document::parse(&contents) {
162        Ok(doc) => {
163            let mut preset: Preset = Preset {
164                name: "".to_string(),
165                mods: vec![],
166            };
167            let html_node = match doc.root().children().find(|n| n.has_tag_name("html")) {
168                Some(n) => n,
169                None => return Err(TagFindErr{ tag_type: TagErrType::HtmlRoot }),
170            };
171            for node in html_node.children().filter(|n| n.is_element()) {
172                if node.has_tag_name("head") {
173                    let tag = match node.children().find(|n| {
174                        n.has_tag_name("meta") && n.attribute("name") == Some("arma:PresetName")
175                    }) {
176                        Some(n) => n,
177                        None => return Err(TagFindErr{ tag_type: TagErrType::PresetName}),
178                    };
179                    preset.name = tag.attribute("content").unwrap().parse().unwrap();
180                } else if node.has_tag_name("body") {
181                    let im1 = match node
182                        .children()
183                        .find(|n| n.has_tag_name("div") && n.attribute("class") == Some("mod-list"))
184                    {
185                        Some(n) => n,
186                        None => return Err(TagFindErr{ tag_type: TagErrType::Modlist }),
187                    };
188                    let im2 = match im1.children().find(|n| n.has_tag_name("table")) {
189                        Some(n) => n,
190                        None => return Err(TagFindErr{ tag_type: TagErrType::ModTable }),
191                    };
192                    let im3 = im2.children().filter(|n| {
193                        n.has_tag_name("tr") && n.attribute("data-type") == Some("ModContainer")
194                    });
195                    for mod_cont in im3 {
196                        let mut temp_mod = Mod {
197                            id: 0,
198                            name: "".to_string(),
199                            from_steam: false,
200                            link: "".to_string(),
201                        };
202                        for item in mod_cont.children().filter(|n| n.is_element()) {
203                            if item.has_attribute("data-type") {
204                                temp_mod.name = match item.children().find(|n| n.is_text()) {
205                                    Some(n) => n,
206                                    None => return Err(TagFindErr{ tag_type: TagErrType::DataTypeText }),
207                                }
208                                .text()
209                                .unwrap()
210                                .parse()
211                                .unwrap();
212                            } else {
213                                if item
214                                    .children()
215                                    .find(|n| n.attribute("class") == Some("from-steam"))
216                                    .is_some()
217                                {
218                                    temp_mod.from_steam = true;
219                                } else if item
220                                    .children()
221                                    .find(|n| n.attribute("class") == Some("from-local"))
222                                    .is_some()
223                                {
224                                    temp_mod.from_steam = false;
225                                } else {
226                                    if item.children().find(|n| n.has_tag_name("a")).is_some() {
227                                        temp_mod.link =
228                                            match item.children().find(|n| n.has_tag_name("a")) {
229                                                Some(n) => n,
230                                                None => return Err(TagFindErr{ tag_type: TagErrType::ModLinkAnchor }),
231                                            }
232                                            .attribute("href")
233                                            .unwrap()
234                                            .parse()
235                                            .unwrap();
236                                        temp_mod.id = temp_mod.link.replace("http://steamcommunity.com/sharedfiles/filedetails/?id=", "").parse().unwrap()
237                                    } else {
238                                        temp_mod.link = match item
239                                            .children()
240                                            .find(|n| n.has_tag_name("span"))
241                                        {
242                                            Some(n) => n,
243                                            None => return Err(TagFindErr{ tag_type: TagErrType::ModLinkSpan }),
244                                        }
245                                        .attribute("data-meta")
246                                        .unwrap()
247                                        .parse()
248                                        .unwrap();
249                                    }
250                                }
251                            }
252                        }
253                        preset.mods.push(temp_mod);
254                    }
255                }
256            }
257            Ok(preset)
258        }
259        Err(_) => return Err(DocParseErr),
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use crate::{parse, Mod, Preset};
266    use std::fs::File;
267
268    #[test]
269    fn parse_file() {
270        let preset = Preset {
271            name: "Parser Test".parse().unwrap(),
272            mods: vec![
273                Mod {
274                    id: 0,
275                    name: "Ryan\'s ACE Canteen".parse().unwrap(),
276                    from_steam: false,
277                    link: "local:Ryan\'s ACE Canteen|@Ryan\'s ACE Canteen|"
278                        .parse()
279                        .unwrap(),
280                },
281                Mod {
282                    id: 450814997,
283                    name: "CBA_A3".parse().unwrap(),
284                    from_steam: true,
285                    link: "http://steamcommunity.com/sharedfiles/filedetails/?id=450814997"
286                        .parse()
287                        .unwrap(),
288                },
289                Mod {
290                    id: 463939057,
291                    name: "ace".parse().unwrap(),
292                    from_steam: true,
293                    link: "http://steamcommunity.com/sharedfiles/filedetails/?id=463939057"
294                        .parse()
295                        .unwrap(),
296                },
297            ],
298        };
299        assert_eq!(
300            parse(File::open("tests/samples/Arma 3 Preset Parser Test.html").unwrap()).unwrap(),
301            preset
302        );
303    }
304
305    #[test]
306    fn parse_from_fs() {
307        let preset: Preset = Preset {
308            name: "Parser Test".parse().unwrap(),
309            mods: vec![
310                Mod {
311                    id: 0,
312                    name: "Ryan\'s ACE Canteen".parse().unwrap(),
313                    from_steam: false,
314                    link: "local:Ryan\'s ACE Canteen|@Ryan\'s ACE Canteen|"
315                        .parse()
316                        .unwrap(),
317                },
318                Mod {
319                    id: 450814997,
320                    name: "CBA_A3".parse().unwrap(),
321                    from_steam: true,
322                    link: "http://steamcommunity.com/sharedfiles/filedetails/?id=450814997"
323                        .parse()
324                        .unwrap(),
325                },
326                Mod {
327                    id: 463939057,
328                    name: "ace".parse().unwrap(),
329                    from_steam: true,
330                    link: "http://steamcommunity.com/sharedfiles/filedetails/?id=463939057"
331                        .parse()
332                        .unwrap(),
333                },
334            ],
335        };
336        assert_eq!(
337            Preset::from_fs(
338                "tests/samples/Arma 3 Preset Parser Test.html"
339                    .parse()
340                    .unwrap()
341            )
342            .unwrap(),
343            preset
344        );
345    }
346    #[test]
347    fn parse_from_file() {
348        let preset: Preset = Preset {
349            name: "Parser Test".parse().unwrap(),
350            mods: vec![
351                Mod {
352                    id: 0,
353                    name: "Ryan\'s ACE Canteen".parse().unwrap(),
354                    from_steam: false,
355                    link: "local:Ryan\'s ACE Canteen|@Ryan\'s ACE Canteen|"
356                        .parse()
357                        .unwrap(),
358                },
359                Mod {
360                    id: 450814997,
361                    name: "CBA_A3".parse().unwrap(),
362                    from_steam: true,
363                    link: "http://steamcommunity.com/sharedfiles/filedetails/?id=450814997"
364                        .parse()
365                        .unwrap(),
366                },
367                Mod {
368                    id: 463939057,
369                    name: "ace".parse().unwrap(),
370                    from_steam: true,
371                    link: "http://steamcommunity.com/sharedfiles/filedetails/?id=463939057"
372                        .parse()
373                        .unwrap(),
374                },
375            ],
376        };
377        assert_eq!(
378            Preset::from_file(
379                File::open("tests/samples/Arma 3 Preset Parser Test.html").unwrap()
380            )
381            .unwrap(),
382            preset
383        );
384    }
385}