1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
use std::{fmt::Display, str::FromStr};

use chrono::{DateTime, Utc};
use regex_macro::regex;
use semver::Version;
use serde::{de::Visitor, Deserialize, Deserializer};
use strum_macros::{Display, EnumString};
use url::Url;

use crate::{FactorioModApiError, Result};

/// A mod as returned from the `https://mods.factorio.com/api/mods` endpoint
/// (not yet implemented).
#[derive(Debug, Deserialize)]
pub struct ModListing {
    /// Metadata shared between the three different API invocations.
    #[serde(flatten)]
    pub metadata: ModMetadata,

    /// The latest version of the mod available for download.
    pub latest_release: ModRelease,
}

/// A mod as returned from the `https://mods.factorio.com/api/mods/{name}/full`
/// endpoint.
#[derive(Debug, Deserialize)]
pub struct FullModSpec {
    /// Spec data shared with short spec request.
    #[serde(flatten)]
    pub short_spec: ModSpec,

    /// A string describing the recent changes to a mod
    pub changelog: String,

    /// ISO 6501 for when the mod was created.
    pub created_at: DateTime<Utc>,

    /// Usually a URL to the mod's main project page, but can be any string.
    pub homepage: String,

    // Undocumented
    pub images: Vec<ModImage>,
    pub license: ModLicense,
    pub updated_at: DateTime<Utc>,
    pub source_url: Option<Url>,
    pub faq: Option<String>,
}

/// Mod metadata shared between the three different API invocations.
#[derive(Debug, Deserialize)]
pub struct ModMetadata {
    /// The mod's machine-readable ID string.
    pub name: String,

    /// The Factorio username of the mod's author.
    pub owner: String,

    /// A shorter mod description.
    pub summary: String,

    /// The mod's human-readable name.
    pub title: String,

    /// A single tag describing the mod. Warning: Seems to be absent sometimes.
    pub category: Option<String>,

    /// Number of downloads.
    pub downloads_count: u64,
}
/// A mod as returned from the `https://mods.factorio.com/api/mods/{name}`
/// endpoint (not yet implemented). Also returned as part of the full request.
#[derive(Debug, Deserialize)]
pub struct ModSpec {
    /// Metadata shared between the three different API invocations.
    #[serde(flatten)]
    pub metadata: ModMetadata,

    /// A list of different versions of the mod available for download.
    pub releases: Vec<ModRelease>,

    /// A longer description of the mod, in text only format.
    pub description: Option<String>,

    /// A link to the mod's github project page, just prepend "github.com/". Can
    /// be blank ("").
    pub github_path: Option<String>,

    /// A list of tag objects that categorize the mod.
    pub tag: Option<ModTag>,

    // Undocumented
    pub score: f64,
    pub thumbnail: Option<String>,
}

/// A tag object that categorizes a mod.
#[derive(Debug, Deserialize)]
pub struct ModTag {
    /// An all lower-case string used to identify this tag internally.
    pub name: String,
}

#[derive(Debug, Deserialize)]
pub struct ModImage {
    pub id: String,
    pub thumbnail: String,
    pub url: Url,
}

#[derive(Debug, Deserialize)]
pub struct ModLicense {
    pub id: String,
    pub name: String,
    pub title: String,
    pub description: String,
    pub url: Url,
}

#[derive(Clone, Debug, Deserialize)]
pub struct ModRelease {
    /// Path to download for a mod. starts with "/download" and does not include
    /// a full url.
    pub download_url: String,

    /// The file name of the release. Always seems to follow the pattern
    /// "{name}_{version}.zip"
    pub file_name: String,

    /// A copy of the mod's info.json file, only contains factorio_version in
    /// short version, also contains an array of dependencies in full version
    pub info_json: ModManifest,

    /// ISO 6501 for when the mod was released.
    pub released_at: DateTime<Utc>,

    /// The version string of this mod release. Used to determine dependencies.
    #[serde(deserialize_with = "parse_version")]
    pub version: Version,

    /// The sha1 key for the file
    pub sha1: String,
}

/// Deserializing visitor for `Version` fields.
struct VersionVisitor;

impl<'de> Visitor<'de> for VersionVisitor {
    type Value = Version;

    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(formatter, "a semver version string (potentially with leading zeros)")
    }

    fn visit_str<E: serde::de::Error>(self, v: &str) -> std::result::Result<Self::Value, E> {
        Version::parse(&regex!(r#"\.0+([1-9])"#).replace_all(v, ".$1"))
            .map_err(|e| E::custom(e.to_string()))
    }
}

fn parse_version<'de, D>(d: D) -> std::result::Result<Version, D::Error>
where
    D: Deserializer<'de>,
{
    d.deserialize_str(VersionVisitor)
}

/// Partial contents of the `info.json` file that describes a mod.
/// https://wiki.factorio.com/Tutorial:Mod_structure#info.json
#[derive(Clone, Debug, Deserialize)]
pub struct ModManifest {
    pub factorio_version: String, // Doesn't parse as semver::Version (no patch level).

    /// The mod's dependencies. Only available in "full" API calls.
    pub dependencies: Option<Vec<ModDependency>>,
}

/// A dependency specification between mods.
#[derive(Clone, Debug, Eq, PartialEq, Deserialize)]
#[serde(try_from = "&str")]
pub struct ModDependency {
    pub flavor: ModDependencyFlavor,
    pub name: String,
    pub comparator: Option<semver::Comparator>,
}

impl ModDependency {
    pub fn unversioned(name: String) -> ModDependency {
        ModDependency { flavor: ModDependencyFlavor::Normal, name, comparator: None }
    }
}

#[derive(Clone, Debug, Display, EnumString, Eq, PartialEq)]
pub enum ModDependencyFlavor {
    #[strum(serialize = "")]
    Normal,
    #[strum(serialize = "!")]
    Incompatibility,
    #[strum(serialize = "?")]
    Optional,
    #[strum(serialize = "(?)")]
    Hidden,
    #[strum(serialize = "~")]
    NoEffectOnLoadOrder,
}

impl TryFrom<&str> for ModDependency {
    type Error = FactorioModApiError;

    fn try_from(value: &str) -> Result<Self> {
        let re = regex!(
            r#"(?x)^
            (?: (?P<prefix> ! | \? | \(\?\) | ~ ) \s*)?
            (?P<name> [[[:alnum:]]-_][[[:alnum:]]-_\ ]{1, 48}[[[:alnum:]]-_])
            (?P<comparator>
                \s* (?: < | <= | = | >= | > )
                \s* \d{1,5}\.\d{1,5}(\.\d{1,5})?
            )?
            $"#,
        );

        let caps = re
            .captures(value)
            .ok_or_else(|| FactorioModApiError::InvalidModDependency { dep: value.into() })?;

        Ok(ModDependency {
            flavor: caps
                .name("prefix")
                .map(|prefix| ModDependencyFlavor::from_str(prefix.as_str()).unwrap())
                .unwrap_or(ModDependencyFlavor::Normal),
            name: caps["name"].into(),
            comparator: caps
                .name("comparator")
                .map(|comparator| semver::Comparator::parse(comparator.as_str()))
                .transpose()?,
        })
    }
}

impl Display for ModDependency {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.flavor)?;
        if self.flavor != ModDependencyFlavor::Normal {
            write!(f, " ")?;
        }

        write!(f, "{}", self.name)?;

        if let Some(comparator) = &self.comparator {
            use semver::Op::*;
            let op = match comparator.op {
                Exact => "=",
                Greater => ">",
                GreaterEq => ">=",
                Less => "<",
                LessEq => "<=",
                _ => unimplemented!(),
            };
            write!(
                f,
                " {op} {}.{}.{}",
                comparator.major,
                comparator.minor.unwrap(),
                comparator.patch.unwrap()
            )?;
        }

        Ok(())
    }
}

impl ModDependency {
    pub fn is_required(&self) -> bool {
        use ModDependencyFlavor::*;
        [Normal, NoEffectOnLoadOrder].contains(&self.flavor)
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::api::{ModDependency, ModDependencyFlavor};
    use semver::Comparator;

    #[test]
    fn basic() -> Result<()> {
        let d: ModDependency = serde_json::from_str(r#""? some-mod-everyone-loves >= 4.2.0""#)?;
        assert!(
            d == ModDependency {
                flavor: ModDependencyFlavor::Optional,
                name: "some-mod-everyone-loves".into(),
                comparator: Some(Comparator::parse(">= 4.2.0")?),
            }
        );
        Ok(())
    }
}