arma_preset_parser 0.3.0

A simple parser for ArmA 3 HTML launcher presets
Documentation
extern crate custom_error;
use crate::ParserError::*;
use custom_error::custom_error;
use std::{fs::File, io::Read};

/// Mod struct. Stores the id, name, link and whether the mod is from the workshop or not
#[derive(Clone, Eq, PartialEq, Debug, Ord, PartialOrd)]
pub struct Mod {
    pub id: i64,
    pub name: String,
    pub from_steam: bool,
    pub link: String,
}

/// Preset struct. Stores the preset name and a vector of all the mods
#[derive(Clone, PartialEq, Debug)]
pub struct Preset {
    pub name: String,
    pub mods: Vec<Mod>,
}

custom_error! {pub TagErrType
    HtmlRoot = "Unable to find root html tag. Is this even a html file?",
    PresetName = "Unable to find PresetName metatag",
    Modlist = "Unable to find mod-list div",
    ModTable = "Unable to find table tag",
    DataTypeText = "Unable to find text in data-type attribute children",
    ModLinkAnchor = "Unable to find remote mod link",
    ModLinkSpan = "Unable to find local mod link",
}

custom_error! {pub ParserError
    StringConvFail = "Failed to read file to string",
    DocParseErr = "Failed to parse document with error",
    DocReadErr = "Failed to read file from path",
    TagFindErr{tag_type: TagErrType} = "Unable to find tag with error: {tag_type}"
}

impl Preset {
    /// This function returns a preset from a file object
    ///
    /// # Examples
    ///
    /// ```rust
    /// match std::fs::File::open(some_path) {
    ///     Ok(file) => {
    ///         match arma_preset_parser::Preset::from_file(file) {
    ///             Ok(preset) => println!("{:?}", preset),
    ///             Err(e) => println!("{}", e)
    ///         };
    ///     },
    ///     Err(e) => println!("{}", e)
    /// };
    /// ```
    pub fn from_file(file: File) -> Result<Self, ParserError> {
        parse(file)
    }

    /// This function returns a preset from a String filepath
    ///
    /// # Examples
    ///
    /// ```rust
    /// match arma_preset_parser::Preset::from_fs("some_path".parse().unwrap()) {
    ///     Ok(preset) => println!("{:?}", preset),
    ///     Err(e) => println!("{}", e)
    /// };
    /// ```
    pub fn from_fs(path: String) -> Result<Self, ParserError> {
        let file: File = match File::open(path) {
            Ok(f) => f,
            Err(_) => return Err(DocReadErr),
        };
        parse(file)
    }

    /// This function returns a vector of mods from a preset
    ///
    /// # Examples
    ///
    /// ```rust
    /// match arma_preset_parser::Preset::from_fs("some_path".parse().unwrap()) {
    ///     Ok(preset) => println!("{:?}", preset.as_mods()),
    ///     Err(e) => println!("{}", e)
    /// };
    /// ```
    pub fn as_mods(&self) -> Vec<Mod> {
        let mut vec = vec![];
        for _mod in &self.mods {
            vec.push(_mod.clone())
        }
        vec
    }

    /// This function returns a vector of mod ids from a preset
    ///
    /// # Examples
    ///
    /// ```rust
    /// match arma_preset_parser::Preset::from_fs("some_path".parse().unwrap()) {
    ///     Ok(preset) => println!("{:?}", preset.as_ids()),
    ///     Err(e) => println!("{}", e)
    /// };
    /// ```
    pub fn as_ids(&self) -> Vec<i64> {
        let mut vec = vec![];
        for _mod in &self.mods {
            vec.push(_mod.clone().id)
        }
        vec
    }

    /// This function returns a vector of mod names from a preset
    ///
    /// # Examples
    ///
    /// ```rust
    /// match arma_preset_parser::Preset::from_fs("some_path".parse().unwrap()) {
    ///     Ok(preset) => println!("{:?}", preset.as_names()),
    ///     Err(e) => println!("{}", e)
    /// };
    /// ```
    pub fn as_names(&self) -> Vec<String> {
        let mut vec = vec![];
        for _mod in &self.mods {
            vec.push(_mod.clone().name)
        }
        vec
    }

    /// This function returns a vector of mod names from a preset
    ///
    /// # Examples
    ///
    /// ```rust
    /// match arma_preset_parser::Preset::from_fs("some_path".parse().unwrap()) {
    ///     Ok(preset) => println!("{:?}", preset.as_links()),
    ///     Err(e) => println!("{}", e)
    /// };
    /// ```
    pub fn as_links(&self) -> Vec<String> {
        let mut vec = vec![];
        for _mod in &self.mods {
            vec.push(_mod.clone().link)
        }
        vec
    }
}

/// Actual parser for the preset. Constructed around the lovely roxmltree package
/// 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)
/// This is an entirely internal function and as such does not contain any external-facing components
fn parse(mut file: File) -> Result<Preset, ParserError> {
    let mut contents: String = "".to_string();

    match file.read_to_string(&mut contents) {
        Ok(_) => {}
        Err(_) => return Err(StringConvFail),
    };

    match roxmltree::Document::parse(&contents) {
        Ok(doc) => {
            let mut preset: Preset = Preset {
                name: "".to_string(),
                mods: vec![],
            };
            let html_node = match doc.root().children().find(|n| n.has_tag_name("html")) {
                Some(n) => n,
                None => return Err(TagFindErr{ tag_type: TagErrType::HtmlRoot }),
            };
            for node in html_node.children().filter(|n| n.is_element()) {
                if node.has_tag_name("head") {
                    let tag = match node.children().find(|n| {
                        n.has_tag_name("meta") && n.attribute("name") == Some("arma:PresetName")
                    }) {
                        Some(n) => n,
                        None => return Err(TagFindErr{ tag_type: TagErrType::PresetName}),
                    };
                    preset.name = tag.attribute("content").unwrap().parse().unwrap();
                } else if node.has_tag_name("body") {
                    let im1 = match node
                        .children()
                        .find(|n| n.has_tag_name("div") && n.attribute("class") == Some("mod-list"))
                    {
                        Some(n) => n,
                        None => return Err(TagFindErr{ tag_type: TagErrType::Modlist }),
                    };
                    let im2 = match im1.children().find(|n| n.has_tag_name("table")) {
                        Some(n) => n,
                        None => return Err(TagFindErr{ tag_type: TagErrType::ModTable }),
                    };
                    let im3 = im2.children().filter(|n| {
                        n.has_tag_name("tr") && n.attribute("data-type") == Some("ModContainer")
                    });
                    for mod_cont in im3 {
                        let mut temp_mod = Mod {
                            id: 0,
                            name: "".to_string(),
                            from_steam: false,
                            link: "".to_string(),
                        };
                        for item in mod_cont.children().filter(|n| n.is_element()) {
                            if item.has_attribute("data-type") {
                                temp_mod.name = match item.children().find(|n| n.is_text()) {
                                    Some(n) => n,
                                    None => return Err(TagFindErr{ tag_type: TagErrType::DataTypeText }),
                                }
                                .text()
                                .unwrap()
                                .parse()
                                .unwrap();
                            } else {
                                if item
                                    .children()
                                    .find(|n| n.attribute("class") == Some("from-steam"))
                                    .is_some()
                                {
                                    temp_mod.from_steam = true;
                                } else if item
                                    .children()
                                    .find(|n| n.attribute("class") == Some("from-local"))
                                    .is_some()
                                {
                                    temp_mod.from_steam = false;
                                } else {
                                    if item.children().find(|n| n.has_tag_name("a")).is_some() {
                                        temp_mod.link =
                                            match item.children().find(|n| n.has_tag_name("a")) {
                                                Some(n) => n,
                                                None => return Err(TagFindErr{ tag_type: TagErrType::ModLinkAnchor }),
                                            }
                                            .attribute("href")
                                            .unwrap()
                                            .parse()
                                            .unwrap();
                                        temp_mod.id = temp_mod.link.replace("http://steamcommunity.com/sharedfiles/filedetails/?id=", "").parse().unwrap()
                                    } else {
                                        temp_mod.link = match item
                                            .children()
                                            .find(|n| n.has_tag_name("span"))
                                        {
                                            Some(n) => n,
                                            None => return Err(TagFindErr{ tag_type: TagErrType::ModLinkSpan }),
                                        }
                                        .attribute("data-meta")
                                        .unwrap()
                                        .parse()
                                        .unwrap();
                                    }
                                }
                            }
                        }
                        preset.mods.push(temp_mod);
                    }
                }
            }
            Ok(preset)
        }
        Err(_) => return Err(DocParseErr),
    }
}

#[cfg(test)]
mod tests {
    use crate::{parse, Mod, Preset};
    use std::fs::File;

    #[test]
    fn parse_file() {
        let preset = Preset {
            name: "Parser Test".parse().unwrap(),
            mods: vec![
                Mod {
                    id: 0,
                    name: "Ryan\'s ACE Canteen".parse().unwrap(),
                    from_steam: false,
                    link: "local:Ryan\'s ACE Canteen|@Ryan\'s ACE Canteen|"
                        .parse()
                        .unwrap(),
                },
                Mod {
                    id: 450814997,
                    name: "CBA_A3".parse().unwrap(),
                    from_steam: true,
                    link: "http://steamcommunity.com/sharedfiles/filedetails/?id=450814997"
                        .parse()
                        .unwrap(),
                },
                Mod {
                    id: 463939057,
                    name: "ace".parse().unwrap(),
                    from_steam: true,
                    link: "http://steamcommunity.com/sharedfiles/filedetails/?id=463939057"
                        .parse()
                        .unwrap(),
                },
            ],
        };
        assert_eq!(
            parse(File::open("tests/samples/Arma 3 Preset Parser Test.html").unwrap()).unwrap(),
            preset
        );
    }

    #[test]
    fn parse_from_fs() {
        let preset: Preset = Preset {
            name: "Parser Test".parse().unwrap(),
            mods: vec![
                Mod {
                    id: 0,
                    name: "Ryan\'s ACE Canteen".parse().unwrap(),
                    from_steam: false,
                    link: "local:Ryan\'s ACE Canteen|@Ryan\'s ACE Canteen|"
                        .parse()
                        .unwrap(),
                },
                Mod {
                    id: 450814997,
                    name: "CBA_A3".parse().unwrap(),
                    from_steam: true,
                    link: "http://steamcommunity.com/sharedfiles/filedetails/?id=450814997"
                        .parse()
                        .unwrap(),
                },
                Mod {
                    id: 463939057,
                    name: "ace".parse().unwrap(),
                    from_steam: true,
                    link: "http://steamcommunity.com/sharedfiles/filedetails/?id=463939057"
                        .parse()
                        .unwrap(),
                },
            ],
        };
        assert_eq!(
            Preset::from_fs(
                "tests/samples/Arma 3 Preset Parser Test.html"
                    .parse()
                    .unwrap()
            )
            .unwrap(),
            preset
        );
    }
    #[test]
    fn parse_from_file() {
        let preset: Preset = Preset {
            name: "Parser Test".parse().unwrap(),
            mods: vec![
                Mod {
                    id: 0,
                    name: "Ryan\'s ACE Canteen".parse().unwrap(),
                    from_steam: false,
                    link: "local:Ryan\'s ACE Canteen|@Ryan\'s ACE Canteen|"
                        .parse()
                        .unwrap(),
                },
                Mod {
                    id: 450814997,
                    name: "CBA_A3".parse().unwrap(),
                    from_steam: true,
                    link: "http://steamcommunity.com/sharedfiles/filedetails/?id=450814997"
                        .parse()
                        .unwrap(),
                },
                Mod {
                    id: 463939057,
                    name: "ace".parse().unwrap(),
                    from_steam: true,
                    link: "http://steamcommunity.com/sharedfiles/filedetails/?id=463939057"
                        .parse()
                        .unwrap(),
                },
            ],
        };
        assert_eq!(
            Preset::from_file(
                File::open("tests/samples/Arma 3 Preset Parser Test.html").unwrap()
            )
            .unwrap(),
            preset
        );
    }
}