facti_lib/
dependency.rs

1use std::{
2    fmt::{self, Display, Formatter},
3    str::FromStr,
4    sync::OnceLock,
5};
6
7use regex::Regex;
8
9use super::{error::ParseDependencyError, version::VersionReq};
10
11/// Describes the relationship of a compatible dependency.
12#[derive(Copy, Clone, Debug, PartialEq, Eq)]
13pub enum DependencyMode {
14    /// The dependency is required.
15    Required,
16
17    /// The dependency is optional, and may optionally be hidden from view
18    /// on the mod portal.
19    Optional { hidden: bool },
20
21    /// The dependency is independent, it will not affect the load order of
22    /// the mod that listed the dependency.
23    Independent,
24}
25
26impl Display for DependencyMode {
27    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
28        use DependencyMode::*;
29        match self {
30            Required => write!(f, ""),
31            Optional { hidden } => write!(f, "{}", if *hidden { "(?)" } else { "?" }),
32            Independent => write!(f, "~"),
33        }
34    }
35}
36
37/// Describes whether a dependency is compatible or not.
38#[derive(Clone, Debug, PartialEq, Eq)]
39pub enum Compatibility {
40    /// The dependency is compatible, with optional version requirements.
41    Compatible(DependencyMode, VersionReq),
42
43    /// The dependency is incompatible with the mod.
44    Incompatible,
45}
46
47/// Describes a dependency of a mod.
48#[derive(Clone, Debug, PartialEq, Eq)]
49pub struct Dependency {
50    /// The internal mod name of the dependency.
51    pub name: String,
52
53    /// The compatibility of the dependency.
54    pub compatibility: Compatibility,
55}
56
57impl Dependency {
58    pub fn new<T: Into<String>>(name: T, compatibility: Compatibility) -> Self {
59        Self {
60            name: name.into(),
61            compatibility,
62        }
63    }
64
65    /// Convenience method for creating a required dependency.
66    ///
67    /// Shortcut for:
68    ///
69    /// ```
70    /// # use facti_lib::dependency::{Compatibility, Dependency, DependencyMode};
71    /// # let name = "placeholder";
72    /// # let version_req = facti_lib::version::VersionReq::Latest;
73    /// let dependency = Dependency::new(name, Compatibility::Compatible(DependencyMode::Required, version_req));
74    /// ```
75    pub fn required<T: Into<String>>(name: T, version_req: VersionReq) -> Self {
76        Self::new(
77            name,
78            Compatibility::Compatible(DependencyMode::Required, version_req),
79        )
80    }
81
82    /// Convenience method for creating an optional dependency.
83    ///
84    /// Shortcut for:
85    ///
86    /// ```
87    /// # use facti_lib::dependency::{Compatibility, Dependency, DependencyMode};
88    /// # let name = "placeholder";
89    /// # let version_req = facti_lib::version::VersionReq::Latest;
90    /// # let hidden = false;
91    /// let dependency = Dependency::new(name, Compatibility::Compatible(DependencyMode::Optional { hidden }, version_req));
92    /// ```
93    pub fn optional<T: Into<String>>(name: T, version_req: VersionReq, hidden: bool) -> Self {
94        Self::new(
95            name,
96            Compatibility::Compatible(DependencyMode::Optional { hidden }, version_req),
97        )
98    }
99
100    /// Convenience method for creating an independent dependency.
101    ///
102    /// Shortcut for:
103    ///
104    /// ```
105    /// # use facti_lib::dependency::{Compatibility, Dependency, DependencyMode};
106    /// # let name = "placeholder";
107    /// # let version_req = facti_lib::version::VersionReq::Latest;
108    /// let dependency = Dependency::new(name, Compatibility::Compatible(DependencyMode::Independent, version_req));
109    /// ```
110    pub fn independent<T: Into<String>>(name: T, version_req: VersionReq) -> Self {
111        Self::new(
112            name,
113            Compatibility::Compatible(DependencyMode::Independent, version_req),
114        )
115    }
116
117    /// Convenience method for creating an incompatible dependency.
118    ///
119    /// Shortcut for:
120    ///
121    /// ```
122    /// # use facti_lib::dependency::{Compatibility, Dependency};
123    /// # let name = "placeholder";
124    /// let dependency = Dependency::new(name, Compatibility::Incompatible);
125    /// ```
126    pub fn incompatible<T: Into<String>>(name: T) -> Self {
127        Self::new(name, Compatibility::Incompatible)
128    }
129
130    /// Parses a [`Dependency`] from a string.
131    ///
132    /// # Examples
133    ///
134    /// ```
135    /// # use facti_lib::dependency::Dependency;
136    /// # use facti_lib::error::ParseDependencyError;
137    /// let dependency = Dependency::parse("my-mod >= 0.17.0")?;
138    /// # Ok::<(), ParseDependencyError>(())
139    pub fn parse(s: &str) -> Result<Self, ParseDependencyError> {
140        s.parse()
141    }
142}
143
144impl FromStr for Dependency {
145    type Err = ParseDependencyError;
146
147    fn from_str(s: &str) -> Result<Self, Self::Err> {
148        static RE: OnceLock<Regex> = OnceLock::new();
149        let re = RE.get_or_init(|| {
150            Regex::new(
151                r"(?sx)
152                \A\s*
153                (?<mode>!|\?|\(\?\)|~)?\s*
154                (?<name>[a-zA-Z0-9\-_\ ]+?)\s*
155                (?<version_spec>
156                    (?: < | <= | = | >= | > )\s*
157                    \d+\.\d+\.\d+
158                )?\s*\z",
159            )
160            .unwrap()
161        });
162
163        let captures = re
164            .captures(s)
165            .ok_or(ParseDependencyError::RegexMismatch(s.to_string()))?;
166
167        let name = captures.name("name").unwrap().as_str().to_string();
168        let version_req = captures
169            .name("version_spec")
170            .map_or(VersionReq::Latest, |m| {
171                // Given that the regex succeeded, we know it's safe to `unwrap` here
172                VersionReq::parse(m.as_str()).unwrap()
173            });
174
175        let compat = captures.name("mode").map_or(
176            Compatibility::Compatible(DependencyMode::Required, version_req),
177            |m| match m.as_str() {
178                "!" => Compatibility::Incompatible,
179                "?" => Compatibility::Compatible(
180                    DependencyMode::Optional { hidden: false },
181                    version_req,
182                ),
183                "(?)" => Compatibility::Compatible(
184                    DependencyMode::Optional { hidden: true },
185                    version_req,
186                ),
187                "~" => Compatibility::Compatible(DependencyMode::Independent, version_req),
188                _ => unreachable!(),
189            },
190        );
191
192        Ok(Self::new(name, compat))
193    }
194}
195
196impl Display for Dependency {
197    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
198        use Compatibility::*;
199        match &self.compatibility {
200            Compatible(m, r) => {
201                if *m != DependencyMode::Required {
202                    write!(f, "{} ", m)?;
203                }
204
205                match r {
206                    VersionReq::Latest => f.write_str(&self.name),
207                    VersionReq::Spec(spec) => write!(f, "{} {}", self.name, spec),
208                }
209            }
210            Incompatible => write!(f, "! {}", self.name),
211        }
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[test]
220    fn parse_required_versioned_dep() {
221        let s = "boblibrary >= 0.17.0";
222        let d: Dependency = s.parse().unwrap();
223        assert_eq!(
224            d,
225            Dependency::new(
226                "boblibrary".to_string(),
227                Compatibility::Compatible(
228                    DependencyMode::Required,
229                    VersionReq::parse(">= 0.17.0").unwrap()
230                )
231            )
232        );
233    }
234
235    #[test]
236    fn parse_required_unversioned_dep() {
237        let s = "boblibrary";
238        let d: Dependency = s.parse().unwrap();
239        assert_eq!(
240            d,
241            Dependency::new(
242                "boblibrary".to_string(),
243                Compatibility::Compatible(DependencyMode::Required, VersionReq::Latest)
244            )
245        );
246    }
247
248    #[test]
249    fn parse_optional_versioned_dep() {
250        let s = "? boblibrary >= 0.17.0";
251        let d: Dependency = s.parse().unwrap();
252        assert_eq!(
253            d,
254            Dependency::new(
255                "boblibrary".to_string(),
256                Compatibility::Compatible(
257                    DependencyMode::Optional { hidden: false },
258                    VersionReq::parse(">= 0.17.0").unwrap()
259                )
260            )
261        );
262    }
263
264    #[test]
265    fn parse_optional_unversioned_dep() {
266        let s = "? boblibrary";
267        let d: Dependency = s.parse().unwrap();
268        assert_eq!(
269            d,
270            Dependency::new(
271                "boblibrary".to_string(),
272                Compatibility::Compatible(
273                    DependencyMode::Optional { hidden: false },
274                    VersionReq::Latest
275                )
276            )
277        );
278    }
279
280    #[test]
281    fn parse_hidden_optional_versioned_dep() {
282        let s = "(?) boblibrary >= 0.17.0";
283        let d: Dependency = s.parse().unwrap();
284        assert_eq!(
285            d,
286            Dependency::new(
287                "boblibrary".to_string(),
288                Compatibility::Compatible(
289                    DependencyMode::Optional { hidden: true },
290                    VersionReq::parse(">= 0.17.0").unwrap()
291                )
292            )
293        );
294    }
295
296    #[test]
297    fn parse_hidden_optional_unversioned_dep() {
298        let s = "(?) boblibrary";
299        let d: Dependency = s.parse().unwrap();
300        assert_eq!(
301            d,
302            Dependency::new(
303                "boblibrary".to_string(),
304                Compatibility::Compatible(
305                    DependencyMode::Optional { hidden: true },
306                    VersionReq::Latest
307                )
308            )
309        );
310    }
311
312    #[test]
313    fn parse_incompatible_dep() {
314        let s = "! boblibrary";
315        let d: Dependency = s.parse().unwrap();
316        assert_eq!(
317            d,
318            Dependency::new("boblibrary".to_string(), Compatibility::Incompatible)
319        );
320    }
321
322    #[test]
323    fn parse_independent_versioned_dep() {
324        let s = "~ boblibrary >= 0.17.0";
325        let d: Dependency = s.parse().unwrap();
326        assert_eq!(
327            d,
328            Dependency::new(
329                "boblibrary".to_string(),
330                Compatibility::Compatible(
331                    DependencyMode::Independent,
332                    VersionReq::parse(">= 0.17.0").unwrap()
333                )
334            )
335        );
336    }
337
338    #[test]
339    fn parse_independent_unversioned_dep() {
340        let s = "~ boblibrary";
341        let d: Dependency = s.parse().unwrap();
342        assert_eq!(
343            d,
344            Dependency::new(
345                "boblibrary".to_string(),
346                Compatibility::Compatible(DependencyMode::Independent, VersionReq::Latest)
347            )
348        );
349    }
350}