os-identifier 0.4.0

Resolve product / release names of operating systems used by endoflife.date into canonical names.
Documentation
//
// https://learn.microsoft.com/lifecycle/products/windows-8
// https://learn.microsoft.com/lifecycle/products/windows-81
//
#[derive(Debug)]
pub(crate) struct Windows8 {
    vendor: String,
    product: String,
    editions: Editions,
}

impl Windows8 {
    pub(super) fn vendor(&self) -> &str {
        self.vendor.as_str()
    }

    pub(super) fn product(&self) -> &str {
        self.product.as_str()
    }

    pub(super) fn release(&self) -> String {
        "".to_string()
    }

    pub(super) fn is_enterprise(&self) -> bool {
        self.editions.contains(Edition::Enterprise) ||
            self.editions.contains(Edition::EnterpriseN)
    }

    pub(super) fn is_lts(&self) -> bool {
        self.is_enterprise()
    }
    
    pub(super) fn to_string(&self) -> Vec<String> {
        let out = self
            .editions
            .0
            .iter()
            .map(|edition| format!("{} {} {edition}", self.vendor, self.product))
            .collect();

        out
    }
}

impl TryFrom<&str> for Windows8 {
    type Error = String;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        match value {
            "windows-8" => Ok(Windows8 {
                vendor: "Microsoft".to_string(),
                product: "Windows 8".to_string(),
                editions: Editions::all(),
            }),
            "windows-8.1" => Ok(Windows8 {
                vendor: "Microsoft".to_string(),
                product: "Windows 8.1".to_string(),
                editions: Editions::all(),
            }),
            _ => Err(String::from("This is not a Windows 8.")),
        }
    }
}

#[derive(Debug)]
struct Editions(Vec<Edition>);

impl Editions {
    fn all() -> Self {
        Editions(vec![
            Edition::Enterprise,
            Edition::EnterpriseN,
            Edition::N,
            Edition::ProWithMediaCenter,
            Edition::Professional,
            Edition::ProfessionalN,
            Edition::SL,
        ])
    }

    #[allow(dead_code)]
    fn contains(&self, edition: Edition) -> bool {
        self.0.contains(&edition)
    }

    #[allow(dead_code)]
    fn len(&self) -> usize {
        self.0.len()
    }
}

#[derive(PartialEq, Debug)]
enum Edition {
    Enterprise,
    EnterpriseN,
    N,
    ProWithMediaCenter,
    Professional,
    ProfessionalN,
    SL,
}

impl std::fmt::Display for Edition {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let out = match self {
            Edition::Enterprise => "Enterprise",
            Edition::EnterpriseN => "Enterprise N",
            Edition::N => "N",
            Edition::ProWithMediaCenter => "Pro with Media Center",
            Edition::Professional => "Professional",
            Edition::ProfessionalN => "Professional N",
            Edition::SL => "SL",
        };

        write!(f, "{}", out.to_string())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_from_string_8() {
        let label = Windows8::try_from("windows-8").unwrap();

        assert_eq!(label.vendor, "Microsoft".to_string());
        assert_eq!(label.product, "Windows 8".to_string());

        assert_eq!(label.editions.len(), Editions::all().len());
    }

    #[test]
    fn test_from_string_81() {
        let label = Windows8::try_from("windows-8.1").unwrap();

        assert_eq!(label.vendor, "Microsoft".to_string());
        assert_eq!(label.product, "Windows 8.1".to_string());

        assert_eq!(label.editions.len(), Editions::all().len());
    }
}