use once_cell::sync::OnceCell; use regex::Regex; use semver::VersionReq; use serde::{Deserialize, Serialize}; use std::fmt::Display; use std::str::FromStr; use thiserror::Error; #[derive(Debug, Deserialize, PartialEq, Serialize)] pub struct ModDependency { pub dep_type: ModDependencyType, pub name: String, pub version_req: Option, } impl FromStr for ModDependency { type Err = ModDependencyErr; fn from_str(s: &str) -> Result { // Avoid creating the regex object every time static DEP_STRING_REGEX: OnceCell = OnceCell::new(); let captures = DEP_STRING_REGEX .get_or_init(|| { Regex::new( r"^(?:(?P[!?~]|\(\?\)) *)?(?P(?: *[a-zA-Z0-9_-]+)+(?: *$)?)(?: *(?P[<>=]=?) *(?P(?:\d+\.){1,2}\d+))?$", ).unwrap() }) .captures(s) .ok_or_else(|| ModDependencyErr::InvalidDependencyString(s.to_string()))?; Ok(Self { dep_type: match captures.name("type").map(|mtch| mtch.as_str()) { None => ModDependencyType::Required, Some("!") => ModDependencyType::Incompatible, Some("~") => ModDependencyType::NoLoadOrder, Some("?") => ModDependencyType::Optional, Some("(?)") => ModDependencyType::OptionalHidden, Some(str) => return Err(ModDependencyErr::UnknownModifier(str.to_string())), }, name: match captures.name("name") { Some(mtch) => mtch.as_str().to_string(), None => return Err(ModDependencyErr::NameIsUnparsable(s.to_string())), }, version_req: match [captures.name("version_req"), captures.name("version")] { [Some(req_match), Some(version_match)] => { // Factorio does not sanitize leading zeros, so we must do it ourselves let version_str = version_match.as_str(); let sanitized = version_str .split('.') .map(|sub| { Ok(sub .parse::() .map_err(|_| { ModDependencyErr::InvalidDependencyString(s.to_string()) })? .to_string()) }) .intersperse(Ok(".".to_string())) .collect::>()?; // Assemble version req from parts let mut version_req = String::new(); version_req.push_str(req_match.as_str()); version_req.push(' '); version_req.push_str(&sanitized); match VersionReq::parse(&version_req) { Ok(version_req) => Some(version_req), Err(_) => { return Err(ModDependencyErr::InvalidVersionReq( req_match.as_str().to_string(), )) } } } _ => None, }, }) } } impl Display for ModDependency { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{}{}{}", match self.dep_type { ModDependencyType::Incompatible => "! ", ModDependencyType::NoLoadOrder => "~ ", ModDependencyType::Optional => "? ", ModDependencyType::OptionalHidden => "(?) ", ModDependencyType::Required => "", }, self.name, match &self.version_req { Some(version_req) => format!(" {}", version_req), None => String::new(), } ) } } #[derive(Debug, Deserialize, PartialEq, Serialize)] pub enum ModDependencyType { Incompatible, NoLoadOrder, Optional, OptionalHidden, Required, } #[derive(Clone, Debug, Error)] pub enum ModDependencyErr { #[error("Invalid dependency string: `{0}`")] InvalidDependencyString(String), #[error("Invalid dependency version requirement: `{0}`")] InvalidVersionReq(String), #[error("Dependency name could not be parsed: `{0}`")] NameIsUnparsable(String), #[error("Unknown dependency modifier: `{0}`")] UnknownModifier(String), }