z_mirror/fmm

git clone https://git.raiguard.me/z_mirror/fmm

raiguardActually deserialize (and serialize) dependenciesee3d597

refactor
4.5 KiB122 linesraw
1use once_cell::sync::OnceCell;
2use regex::Regex;
3use semver::VersionReq;
4use serde::{Deserialize, Serialize};
5use std::fmt::Display;
6use std::str::FromStr;
7use thiserror::Error;
8
9#[derive(Debug, Deserialize, PartialEq, Serialize)]
10pub struct ModDependency {
11    pub dep_type: ModDependencyType,
12    pub name: String,
13    pub version_req: Option<VersionReq>,
14}
15
16impl FromStr for ModDependency {
17    type Err = ModDependencyErr;
18
19    fn from_str(s: &str) -> Result<Self, Self::Err> {
20        // Avoid creating the regex object every time
21        static DEP_STRING_REGEX: OnceCell<Regex> = OnceCell::new();
22        let captures = DEP_STRING_REGEX
23            .get_or_init(|| {
24                Regex::new(
25                    r"^(?:(?P<type>[!?~]|\(\?\)) *)?(?P<name>(?: *[a-zA-Z0-9_-]+)+(?: *$)?)(?: *(?P<version_req>[<>=]=?) *(?P<version>(?:\d+\.){1,2}\d+))?$",
26                ).unwrap()
27            })
28            .captures(s)
29            .ok_or_else(|| ModDependencyErr::InvalidDependencyString(s.to_string()))?;
30
31        Ok(Self {
32            dep_type: match captures.name("type").map(|mtch| mtch.as_str()) {
33                None => ModDependencyType::Required,
34                Some("!") => ModDependencyType::Incompatible,
35                Some("~") => ModDependencyType::NoLoadOrder,
36                Some("?") => ModDependencyType::Optional,
37                Some("(?)") => ModDependencyType::OptionalHidden,
38                Some(str) => return Err(ModDependencyErr::UnknownModifier(str.to_string())),
39            },
40            name: match captures.name("name") {
41                Some(mtch) => mtch.as_str().to_string(),
42                None => return Err(ModDependencyErr::NameIsUnparsable(s.to_string())),
43            },
44            version_req: match [captures.name("version_req"), captures.name("version")] {
45                [Some(req_match), Some(version_match)] => {
46                    // Factorio does not sanitize leading zeros, so we must do it ourselves
47                    let version_str = version_match.as_str();
48                    let sanitized = version_str
49                        .split('.')
50                        .map(|sub| {
51                            Ok(sub
52                                .parse::<usize>()
53                                .map_err(|_| {
54                                    ModDependencyErr::InvalidDependencyString(s.to_string())
55                                })?
56                                .to_string())
57                        })
58                        .intersperse(Ok(".".to_string()))
59                        .collect::<Result<String, ModDependencyErr>>()?;
60
61                    // Assemble version req from parts
62                    let mut version_req = String::new();
63                    version_req.push_str(req_match.as_str());
64                    version_req.push(' ');
65                    version_req.push_str(&sanitized);
66
67                    match VersionReq::parse(&version_req) {
68                        Ok(version_req) => Some(version_req),
69                        Err(_) => {
70                            return Err(ModDependencyErr::InvalidVersionReq(
71                                req_match.as_str().to_string(),
72                            ))
73                        }
74                    }
75                }
76                _ => None,
77            },
78        })
79    }
80}
81
82impl Display for ModDependency {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        write!(
85            f,
86            "{}{}{}",
87            match self.dep_type {
88                ModDependencyType::Incompatible => "! ",
89                ModDependencyType::NoLoadOrder => "~ ",
90                ModDependencyType::Optional => "? ",
91                ModDependencyType::OptionalHidden => "(?) ",
92                ModDependencyType::Required => "",
93            },
94            self.name,
95            match &self.version_req {
96                Some(version_req) => format!(" {}", version_req),
97                None => String::new(),
98            }
99        )
100    }
101}
102
103#[derive(Debug, Deserialize, PartialEq, Serialize)]
104pub enum ModDependencyType {
105    Incompatible,
106    NoLoadOrder,
107    Optional,
108    OptionalHidden,
109    Required,
110}
111
112#[derive(Clone, Debug, Error)]
113pub enum ModDependencyErr {
114    #[error("Invalid dependency string: `{0}`")]
115    InvalidDependencyString(String),
116    #[error("Invalid dependency version requirement: `{0}`")]
117    InvalidVersionReq(String),
118    #[error("Dependency name could not be parsed: `{0}`")]
119    NameIsUnparsable(String),
120    #[error("Unknown dependency modifier: `{0}`")]
121    UnknownModifier(String),
122}