use std::{fmt::Display, str::FromStr};
use chrono::{DateTime, Utc};
use regex_macro::regex;
use semver::Version;
use serde::{de::Visitor, Deserialize, Deserializer, Serialize};
use strum_macros::{Display, EnumString};
use url::Url;
use crate::{FactorioModApiError, Result};
#[derive(Debug, Deserialize)]
pub struct ModListing {
#[serde(flatten)]
pub metadata: ModMetadata,
pub latest_release: ModRelease,
}
#[derive(Debug, Deserialize)]
pub struct FullModSpec {
#[serde(flatten)]
pub short_spec: ModSpec,
pub changelog: String,
pub created_at: DateTime<Utc>,
pub homepage: String,
pub images: Vec<ModImage>,
pub license: ModLicense,
pub updated_at: DateTime<Utc>,
pub source_url: Option<Url>,
pub faq: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct ModMetadata {
pub name: String,
pub owner: String,
pub summary: String,
pub title: String,
pub category: Option<String>,
pub downloads_count: u64,
}
#[derive(Debug, Deserialize)]
pub struct ModSpec {
#[serde(flatten)]
pub metadata: ModMetadata,
pub releases: Vec<ModRelease>,
pub description: Option<String>,
pub github_path: Option<String>,
pub tag: Option<ModTag>,
pub score: f64,
pub thumbnail: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct ModTag {
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 {
pub download_url: String,
pub file_name: String,
pub info_json: ModManifest,
pub released_at: DateTime<Utc>,
#[serde(deserialize_with = "parse_version")]
pub version: Version,
pub sha1: String,
}
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(®ex!(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)
}
#[derive(Clone, Debug, Deserialize)]
pub struct ModManifest {
pub factorio_version: String,
pub dependencies: Option<Vec<ModDependency>>,
}
#[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)
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ApiToken {
pub token: String,
pub username: String,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum LoginResponse {
Success {
#[serde(flatten)]
token: ApiToken,
},
Error {
error: String,
message: String,
},
}
#[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(())
}
}