1use std::{fmt::Display, str::FromStr};
4
5use chrono::{DateTime, Utc};
6use regex_macro::regex;
7use semver::Version;
8use serde::{de::Visitor, Deserialize, Deserializer, Serialize};
9use strum_macros::{Display, EnumString};
10use url::Url;
11
12use crate::{FactorioModApiError, Result};
13
14#[derive(Debug, Deserialize)]
17pub struct ModListing {
18 #[serde(flatten)]
20 pub metadata: ModMetadata,
21
22 pub latest_release: ModRelease,
24}
25
26#[derive(Debug, Deserialize)]
29pub struct FullModSpec {
30 #[serde(flatten)]
32 pub short_spec: ModSpec,
33
34 pub changelog: String,
36
37 pub created_at: DateTime<Utc>,
39
40 pub homepage: String,
42
43 pub images: Vec<ModImage>,
45 pub license: ModLicense,
46 pub updated_at: DateTime<Utc>,
47 pub source_url: Option<Url>,
48 pub faq: Option<String>,
49}
50
51#[derive(Debug, Deserialize)]
53pub struct ModMetadata {
54 pub name: String,
56
57 pub owner: String,
59
60 pub summary: String,
62
63 pub title: String,
65
66 pub category: Option<String>,
68
69 pub downloads_count: u64,
71}
72#[derive(Debug, Deserialize)]
75pub struct ModSpec {
76 #[serde(flatten)]
78 pub metadata: ModMetadata,
79
80 pub releases: Vec<ModRelease>,
82
83 pub description: Option<String>,
85
86 pub github_path: Option<String>,
89
90 pub tag: Option<ModTag>,
92
93 pub score: f64,
95 pub thumbnail: Option<String>,
96}
97
98#[derive(Debug, Deserialize)]
100pub struct ModTag {
101 pub name: String,
103}
104
105#[derive(Debug, Deserialize)]
106pub struct ModImage {
107 pub id: String,
108 pub thumbnail: String,
109 pub url: Url,
110}
111
112#[derive(Debug, Deserialize)]
113pub struct ModLicense {
114 pub id: String,
115 pub name: String,
116 pub title: String,
117 pub description: String,
118 pub url: Url,
119}
120
121#[derive(Clone, Debug, Deserialize)]
122pub struct ModRelease {
123 pub download_url: String,
126
127 pub file_name: String,
130
131 pub info_json: ModManifest,
134
135 pub released_at: DateTime<Utc>,
137
138 #[serde(deserialize_with = "parse_version")]
140 pub version: Version,
141
142 pub sha1: String,
144}
145
146struct VersionVisitor;
148
149impl<'de> Visitor<'de> for VersionVisitor {
150 type Value = Version;
151
152 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
153 write!(formatter, "a semver version string (potentially with leading zeros)")
154 }
155
156 fn visit_str<E: serde::de::Error>(self, v: &str) -> std::result::Result<Self::Value, E> {
157 Version::parse(®ex!(r#"\.0+([1-9])"#).replace_all(v, ".$1"))
158 .map_err(|e| E::custom(e.to_string()))
159 }
160}
161
162fn parse_version<'de, D>(d: D) -> std::result::Result<Version, D::Error>
163where
164 D: Deserializer<'de>,
165{
166 d.deserialize_str(VersionVisitor)
167}
168
169#[derive(Clone, Debug, Deserialize)]
172pub struct ModManifest {
173 pub factorio_version: String, pub dependencies: Option<Vec<ModDependency>>,
177}
178
179#[derive(Clone, Debug, Eq, PartialEq, Deserialize)]
181#[serde(try_from = "&str")]
182pub struct ModDependency {
183 pub flavor: ModDependencyFlavor,
184 pub name: String,
185 pub comparator: Option<semver::Comparator>,
186}
187
188impl ModDependency {
189 pub fn unversioned(name: String) -> ModDependency {
190 ModDependency { flavor: ModDependencyFlavor::Normal, name, comparator: None }
191 }
192}
193
194#[derive(Clone, Debug, Display, EnumString, Eq, PartialEq)]
195pub enum ModDependencyFlavor {
196 #[strum(serialize = "")]
197 Normal,
198 #[strum(serialize = "!")]
199 Incompatibility,
200 #[strum(serialize = "?")]
201 Optional,
202 #[strum(serialize = "(?)")]
203 Hidden,
204 #[strum(serialize = "~")]
205 NoEffectOnLoadOrder,
206}
207
208impl TryFrom<&str> for ModDependency {
209 type Error = FactorioModApiError;
210
211 fn try_from(value: &str) -> Result<Self> {
212 let re = regex!(
213 r#"(?x)^
214 (?: (?P<prefix> ! | \? | \(\?\) | ~ ) \s*)?
215 (?P<name> [[[:alnum:]]-_][[[:alnum:]]-_\ ]{1, 48}[[[:alnum:]]-_])
216 (?P<comparator>
217 \s* (?: < | <= | = | >= | > )
218 \s* \d{1,5}\.\d{1,5}(\.\d{1,5})?
219 )?
220 $"#,
221 );
222
223 let caps = re
224 .captures(value)
225 .ok_or_else(|| FactorioModApiError::InvalidModDependency { dep: value.into() })?;
226
227 Ok(ModDependency {
228 flavor: caps
229 .name("prefix")
230 .map(|prefix| ModDependencyFlavor::from_str(prefix.as_str()).unwrap())
231 .unwrap_or(ModDependencyFlavor::Normal),
232 name: caps["name"].into(),
233 comparator: caps
234 .name("comparator")
235 .map(|comparator| semver::Comparator::parse(comparator.as_str()))
236 .transpose()?,
237 })
238 }
239}
240
241impl Display for ModDependency {
242 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243 write!(f, "{}", self.flavor)?;
244 if self.flavor != ModDependencyFlavor::Normal {
245 write!(f, " ")?;
246 }
247
248 write!(f, "{}", self.name)?;
249
250 if let Some(comparator) = &self.comparator {
251 use semver::Op::*;
252 let op = match comparator.op {
253 Exact => "=",
254 Greater => ">",
255 GreaterEq => ">=",
256 Less => "<",
257 LessEq => "<=",
258 _ => unimplemented!(),
259 };
260 write!(
261 f,
262 " {op} {}.{}.{}",
263 comparator.major,
264 comparator.minor.unwrap(),
265 comparator.patch.unwrap()
266 )?;
267 }
268
269 Ok(())
270 }
271}
272
273impl ModDependency {
274 pub fn is_required(&self) -> bool {
275 use ModDependencyFlavor::*;
276 [Normal, NoEffectOnLoadOrder].contains(&self.flavor)
277 }
278}
279
280#[derive(Debug, Deserialize, Serialize)]
285pub struct ApiToken {
286 pub token: String,
287 pub username: String,
288}
289
290#[derive(Debug, Deserialize)]
292#[serde(untagged)]
293pub enum LoginResponse {
294 Success {
295 #[serde(flatten)]
296 token: ApiToken,
297 },
298 Error {
299 error: String,
300 message: String,
301 },
302}
303
304#[cfg(test)]
305mod test {
306 use super::*;
307 use crate::api::{ModDependency, ModDependencyFlavor};
308 use semver::Comparator;
309
310 #[test]
311 fn basic() -> Result<()> {
312 let d: ModDependency = serde_json::from_str(r#""? some-mod-everyone-loves >= 4.2.0""#)?;
313 assert!(
314 d == ModDependency {
315 flavor: ModDependencyFlavor::Optional,
316 name: "some-mod-everyone-loves".into(),
317 comparator: Some(Comparator::parse(">= 4.2.0")?),
318 }
319 );
320 Ok(())
321 }
322}