diff --git a/.gitignore b/.gitignore index 5fd9c8f..5a1d923 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,9 @@ target/ **/samples/ **/.env -**/node_modules \ No newline at end of file +**/node_modules +**/app.config* +**/.output +**/.vinxi + +**/*.so \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index d903f48..e6337b7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,4 +3,14 @@ "lldb.showDisassembly": "never", "lldb.dereferencePointers": true, "lldb.consoleMode": "commands", + "rust-analyzer.cargo.features": "all", + "rust-analyzer.rustfmt.extraArgs": [ + "--config", + "tab_spaces=2" + ], + "[rust]": { + "editor.tabSize": 2, + "editor.detectIndentation": false, + "editor.defaultFormatter": "rust-lang.rust-analyzer", + }, } \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 385c9d8..3ff9691 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -209,6 +209,21 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "curseforge" +version = "0.1.0" +dependencies = [ + "anyhow", + "dotenv", + "env_logger", + "log", + "reqwest", + "serde", + "serde_json", + "thiserror", + "tokio", +] + [[package]] name = "denji" version = "0.1.0" @@ -225,6 +240,12 @@ dependencies = [ "tokio", ] +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "encoding_rs" version = "0.8.34" @@ -690,7 +711,7 @@ dependencies = [ [[package]] name = "modparser" -version = "1.0.0" +version = "1.1.0" dependencies = [ "log", "serde", diff --git a/Cargo.toml b/Cargo.toml index ec2c311..76b7616 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,14 @@ [workspace] resolver = "2" -members = ["denji", "hangar", "mar", "modparser", "modrinth", "mparse"] +members = [ + "curseforge", + "denji", + "hangar", + "mar", + "modparser", + "modrinth", + "mparse", +] [workspace.dependencies] log = "0.4.21" @@ -8,6 +16,8 @@ thiserror = "1.0.58" anyhow = "1.0.86" serde_json = "1.0.116" dotenv = "0.15.0" +env_logger = "0.11.5" + sha1_smol = { version = "1.0.0", features = ["serde"] } serde = { version = "1.0.197", features = ["derive", "rc"] } diff --git a/curseforge/Cargo.toml b/curseforge/Cargo.toml new file mode 100644 index 0000000..13dd365 --- /dev/null +++ b/curseforge/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "curseforge" +version = "0.1.0" +edition = "2021" + +[dependencies] +thiserror.workspace = true +anyhow.workspace = true +serde.workspace = true +serde_json.workspace = true +reqwest.workspace = true +log.workspace = true + +[dev-dependencies] +dotenv.workspace = true +anyhow.workspace = true +tokio.workspace = true +env_logger.workspace = true +# serde_qs = "0.13.0" # temporary + +[lints] +workspace = true diff --git a/curseforge/examples/dep-resolve.rs b/curseforge/examples/dep-resolve.rs new file mode 100644 index 0000000..1858b03 --- /dev/null +++ b/curseforge/examples/dep-resolve.rs @@ -0,0 +1,26 @@ +use std::vec; + +use anyhow::Context; +use dotenv::dotenv; +use log::info; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + dotenv().context("while loading dotenv")?; + env_logger::init(); + info!("Hello world!"); + + let curse = curseforge::CurseForge::new_from_env().context("while building client")?; + let query = curseforge::types::query::ModQueryBuilder::default() + .mod_name("Graves") + .categories(vec![420, 424, 421, 425]) + .game_versions(vec!["1.12.2".to_string()]); + + let curse_mods = curse.get_mod(&query).await?; + + for curse_mod in curse_mods.iter() { + dbg!(curse_mod); + } + + Ok(()) +} diff --git a/curseforge/src/lib.rs b/curseforge/src/lib.rs new file mode 100644 index 0000000..df48117 --- /dev/null +++ b/curseforge/src/lib.rs @@ -0,0 +1,74 @@ +use std::{env, time::Duration}; + +use anyhow::Context; +use log::debug; +use log::info; +use reqwest::header::HeaderMap; +use reqwest::header::HeaderValue; +use reqwest::Client; +use thiserror::Error; +use types::project::CurseMod; +use types::CurseResponse; + +pub mod types; + +pub(crate) const CURSE_API: &str = "https://api.curseforge.com/v1"; +pub(crate) const CURSE_MINECRAFT_ID: u16 = 432; +pub struct CurseForge { + client: Client, +} + +#[derive(Debug, Error)] +pub enum CurseClientError { + #[error("http error: {0}")] + Http(#[from] reqwest::Error), + + #[error("header error while building curseforge client: {0}")] + Header(#[from] reqwest::header::InvalidHeaderValue), + + #[error("no environment variable `CURSE_API_KEY` supplied: {0}")] + NoApiKey(#[from] std::env::VarError), +} + +impl CurseForge { + pub fn new(api_key: &str) -> Result { + info!("Building CurseForge Client"); + let mut headers = HeaderMap::new(); + + headers.insert("Accept", HeaderValue::from_static("application/json")); + headers.insert("X-Api-Key", HeaderValue::from_str(api_key)?); + + Ok(Self { + client: Client::builder() + .default_headers(headers) + .build() + .expect("expected client to be built"), + }) + } + + pub fn new_from_env() -> Result { + Self::new(&env::var("CURSE_API_KEY")?) + } + + pub async fn get_mod( + &self, + query: &types::query::ModQueryBuilder, + ) -> Result>, CurseClientError> { + info!("Fetching mod {:?} from curseforge", query.search_filter); + + let url = format!("{}/mods/search", CURSE_API); + let req = self.client.get(url).query(query); + + debug!("url:\n{:#?}", req); + + let resp = req + .send() + .await? + .json::>>() + .await?; + + info!("Got {} mod(s)", resp.pagination.result_count); + + Ok(resp) + } +} diff --git a/curseforge/src/types/mod.rs b/curseforge/src/types/mod.rs new file mode 100644 index 0000000..e415ea1 --- /dev/null +++ b/curseforge/src/types/mod.rs @@ -0,0 +1,114 @@ +use std::ops::Deref; + +use serde::{Deserialize, Serialize}; + +pub mod project; +pub mod query; + +#[derive(Debug, Default, Serialize, Deserialize, Clone, Copy)] +#[repr(u8)] +pub enum ModLoader { + #[default] + Any = 0, + Forge = 1, + Cauldron = 2, + LiteLoader = 3, + Fabric = 4, + Quilt = 5, + NeoForge = 6, +} + +impl ToString for ModLoader { + fn to_string(&self) -> String { + (*self as u8).to_string() + } +} + +#[derive(Debug)] +pub struct CurseResponse { + inner: T, + pub pagination: CursePagination, +} + +#[derive(Debug, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct CursePagination { + pub index: u8, + pub page_size: u8, + pub result_count: u8, + pub total_count: u8, +} + +impl Deref for CurseResponse { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl CurseResponse { + pub fn new(inner: T) -> Self { + Self { + inner, + pagination: CursePagination::default(), + } + } +} + +impl<'de, T> Deserialize<'de> for CurseResponse +where + T: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let mut data = serde_json::Map::deserialize(deserializer)?; + let inner = data + .remove("data") + .ok_or_else(|| serde::de::Error::missing_field("data")) + .and_then(T::deserialize) + .map_err(serde::de::Error::custom)?; + + let pagination = data + .remove("pagination") + .ok_or_else(|| serde::de::Error::missing_field("pagination")) + .and_then(serde_json::from_value) + .map_err(serde::de::Error::custom)?; + + Ok(Self { inner, pagination }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde::Deserialize; + + #[derive(Debug, Deserialize)] + struct TestStruct { + name: String, + } + + #[test] + fn test_cresp() { + let json_data = r#" + { + "data": { + "name": "Samuel L Jackson" + }, + "pagination": { + "index": 0, + "pageSize": 5, + "resultCount": 5, + "totalCount": 15 + } + } + "#; + + let data = serde_json::from_str::>(json_data); + + assert!(data.is_ok_and(|d| d.name == "Samuel L Jackson")) + } +} diff --git a/curseforge/src/types/project.rs b/curseforge/src/types/project.rs new file mode 100644 index 0000000..d35e324 --- /dev/null +++ b/curseforge/src/types/project.rs @@ -0,0 +1,114 @@ +use super::ModLoader; +use std::rc::Rc; + +use log::{error, warn}; +use serde::{Deserialize, Deserializer}; + +#[derive(Debug, Deserialize)] +#[serde(rename = "camelCase")] +pub struct CurseMod { + pub(crate) id: u32, + pub logo: CurseAsset, + pub name: Rc, + pub links: CurseModLinks, + pub summary: Rc, + pub latest_files_indexes: Option>, + pub latest_early_access_files_indexes: Option>, + pub categories: Rc<[CurseCategory]>, + pub authors: Rc<[CurseAuthor]>, + pub screenshots: Rc<[CurseAsset]>, + #[serde(rename = "allowModDistribution")] + pub(crate) allowed: Option, +} + +impl CurseMod { + pub fn is_allowed(&self) -> bool { + self.allowed.is_some_and(|v| v) + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CurseFileIndex { + // file_id: usize, + pub game_version: Rc, + #[serde(rename = "filename")] + pub file_name: Rc, + pub release_type: CurseRelease, + pub game_version_type_id: Option, + pub mod_loeader: ModLoader, +} + +#[derive(Debug, Deserialize)] +#[repr(u8)] +pub enum CurseRelease { + Release = 1, + Beta = 2, + Alpha = 3, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CurseModLinks { + #[serde(deserialize_with = "unwrap_str")] + pub website_url: Option>, + #[serde(deserialize_with = "unwrap_str")] + pub wiki_url: Option>, + #[serde(deserialize_with = "unwrap_str")] + pub issues_url: Option>, + #[serde(deserialize_with = "unwrap_str")] + pub source_url: Option>, +} + +#[derive(Debug, Deserialize)] +#[serde(rename = "camelCase")] +pub struct CurseCategory { + pub id: u32, + pub name: Rc, + pub url: Rc, + pub icon_url: Option>, +} + +#[derive(Debug, Deserialize)] +pub struct CurseAuthor { + pub id: u32, + pub name: Rc, + pub url: Rc, +} + +#[derive(Debug, Deserialize)] +#[serde(rename = "camelCase")] +pub struct CurseAsset { + pub id: u32, + + #[serde(deserialize_with = "unwrap_str")] + pub title: Option>, + + #[serde(deserialize_with = "unwrap_str")] + pub description: Option>, + + // #[serde(deserialize_with = "unwrap_str")] + pub thumbnail_url: Option>, + + #[serde(deserialize_with = "unwrap_str")] + pub url: Option>, +} + +fn unwrap_str<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + let raw = Option::deserialize(deserializer); + + if let Err(err) = &raw { + error!("String cannot be deserialized: {}", err); + warn!("This function will return `None` as default"); + + return Ok(None); + } + + let url: Option = raw.ok().flatten(); + let a = url.filter(|u| !u.is_empty()).map(|s| Rc::from(s.as_str())); + + Ok(a) +} diff --git a/curseforge/src/types/query.rs b/curseforge/src/types/query.rs new file mode 100644 index 0000000..8e990d9 --- /dev/null +++ b/curseforge/src/types/query.rs @@ -0,0 +1,140 @@ +use super::ModLoader; +use serde::{Serialize, Serializer}; + +#[derive(Debug, Default, Clone, Copy)] +#[repr(u8)] +pub enum SortBy { + Featured = 1, + #[default] + Popularity = 2, + LastUpdated = 3, + Name = 4, + Author = 5, + TotalDownloads = 6, + Category = 7, + GameVersion = 8, + EarlyAccess = 9, + FeaturedReleased = 10, + ReleasedDate = 11, + Rating = 12, +} + +impl Serialize for SortBy { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_u8(*self as u8) + } +} + +#[derive(Debug, Serialize, Default)] +pub enum OrderBy { + #[serde(rename = "asc")] + #[default] + Ascending, + #[serde(rename = "desc")] + Descending, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ModQueryBuilder { + pub(crate) game_id: u16, + #[serde(serialize_with = "serialize_vec")] + pub(crate) categories: Vec, + #[serde(serialize_with = "serialize_vec")] + pub(crate) game_versions: Vec, + pub(crate) search_filter: String, + pub(crate) sort_field: SortBy, + pub(crate) sort_order: OrderBy, + #[serde(serialize_with = "serialize_vec")] + pub(crate) mod_loader_types: Vec, + pub(crate) index: usize, +} + +impl Default for ModQueryBuilder { + fn default() -> Self { + Self { + game_id: crate::CURSE_MINECRAFT_ID, + categories: Default::default(), + game_versions: Default::default(), + search_filter: Default::default(), + sort_field: Default::default(), + sort_order: Default::default(), + mod_loader_types: Default::default(), + index: Default::default(), + } + } +} + +impl ModQueryBuilder { + pub fn mod_name(mut self, search: S) -> Self { + self.search_filter = search.to_string(); + + self + } + + pub fn categories(mut self, categories: Vec) -> Self { + self.categories = categories; + + self + } + + pub fn game_versions(mut self, game_versions: Vec) -> Self { + self.game_versions = game_versions; + + self + } + + pub fn sort_field(mut self, sort_field: SortBy) -> Self { + self.sort_field = sort_field; + + self + } + + pub fn sort_order(mut self, sort_order: OrderBy) -> Self { + self.sort_order = sort_order; + + self + } + + pub fn mod_loader_types(mut self, mod_loader_types: Vec) -> Self { + self.mod_loader_types = mod_loader_types; + + self + } + + pub fn index(mut self, index: usize) -> Self { + self.index = index; + + self + } +} + +fn serialize_vec(vec: &[T], serializer: S) -> Result +where + S: Serializer, + T: Serialize + ToString, +{ + // TODO: use serde_json here, + // we using that thing already anyway + let vec_str = format!( + "[{}]", + vec + .iter() + .map(|c| { + let c_str = c.to_string(); + + if c_str.parse::().is_ok() { + c_str + } else { + format!("{:?}", c_str) + } + }) + .collect::>() + .join(",") + ); + + serializer.serialize_str(&vec_str) +} diff --git a/denji/Cargo.toml b/denji/Cargo.toml index 9bae135..a13b62f 100644 --- a/denji/Cargo.toml +++ b/denji/Cargo.toml @@ -12,10 +12,10 @@ reqwest = { workspace = true, features = ["stream"] } mar = { path = "../mar" } [dev-dependencies] -env_logger = "0.11.5" +env_logger.workspace = true +tokio.workspace = true humantime = "2.1.0" tempdir = "0.3.7" -tokio.workspace = true [lints] workspace = true diff --git a/denji/examples/install-server.rs b/denji/examples/install-server.rs index 6c38bba..6703457 100644 --- a/denji/examples/install-server.rs +++ b/denji/examples/install-server.rs @@ -13,34 +13,34 @@ const CHANNEL_TIMEOUT: Duration = Duration::from_secs(90); #[tokio::main] async fn main() -> Result<()> { - env_logger::init(); - - let root_dir = TempDir::new("test.denji.serverInstall")?.into_path(); - let server_installer = - MinecraftServer::new(ServerSoftware::Forge, "1.20.4-49.1.4", "1.20.4", root_dir); - let (tx, rx) = channel(); - let server_build = spawn(async move { server_installer.build_server(tx).await }); - - info!( - "started installer (timeout: {})", - format_duration(CHANNEL_TIMEOUT) - ); - - loop { - match rx.recv_timeout(CHANNEL_TIMEOUT) { - Ok(line) => info!("{}", line), - Err(e) => { - warn!("{}. closing installer", e); - break; - } - } + env_logger::init(); + + let root_dir = TempDir::new("test.denji.serverInstall")?.into_path(); + let server_installer = + MinecraftServer::new(ServerSoftware::Forge, "1.20.4-49.1.4", "1.20.4", root_dir); + let (tx, rx) = channel(); + let server_build = spawn(async move { server_installer.build_server(tx).await }); + + info!( + "started installer (timeout: {})", + format_duration(CHANNEL_TIMEOUT) + ); + + loop { + match rx.recv_timeout(CHANNEL_TIMEOUT) { + Ok(line) => info!("{}", line), + Err(e) => { + warn!("{}. closing installer", e); + break; + } } + } - server_build - .await - .context("while trying to finish installer")? - .context("while trying to install server")?; + server_build + .await + .context("while trying to finish installer")? + .context("while trying to install server")?; - info!("you may test the channel and close this program when finished"); - Result::Ok(()) + info!("you may test the channel and close this program when finished"); + Result::Ok(()) } diff --git a/denji/src/shell.rs b/denji/src/shell.rs index 37ac51d..103ad69 100644 --- a/denji/src/shell.rs +++ b/denji/src/shell.rs @@ -25,240 +25,239 @@ macro_rules! args { #[derive(Clone, Copy)] pub enum ServerSoftware { - Forge, - Neoforge, - Fabric, - Quilt, - Glowstone, + Forge, + Neoforge, + Fabric, + Quilt, + Glowstone, } #[derive(Debug, Error)] pub enum ServerInstallError { - #[error("error while trying to fetch artifact data: {0}")] - Artifact(#[from] mar::RepositoryError), - #[error("version for artifact not found: {0}")] - Version(String), - #[error("net error: {0}")] - Net(#[from] reqwest::Error), - #[error("i/o error: {0}")] - Io(#[from] std::io::Error), - #[error("{0}")] - Contextual(#[from] anyhow::Error), + #[error("error while trying to fetch artifact data: {0}")] + Artifact(#[from] mar::RepositoryError), + #[error("version for artifact not found: {0}")] + Version(String), + #[error("net error: {0}")] + Net(#[from] reqwest::Error), + #[error("i/o error: {0}")] + Io(#[from] std::io::Error), + #[error("{0}")] + Contextual(#[from] anyhow::Error), } pub struct MinecraftServer { - server: S, - server_version: Arc, - game_version: Arc, - root_dir: I, + server: S, + server_version: Arc, + game_version: Arc, + root_dir: I, } impl, S: ServerSoftwareMeta> MinecraftServer { - pub fn new(server: S, server_version: V, game_version: V, root_dir: I) -> Self { - Self { - server, - server_version: server_version.to_string().into(), - game_version: game_version.to_string().into(), - root_dir, - } + pub fn new(server: S, server_version: V, game_version: V, root_dir: I) -> Self { + Self { + server, + server_version: server_version.to_string().into(), + game_version: game_version.to_string().into(), + root_dir, } + } - pub async fn build_server(&self, tx: Sender) -> Result<(), ServerInstallError> { - info!( - "installing {} v{} for minecraft {} to {}", - self.server, - self.server_version, - self.game_version, - self.root_dir.as_ref().display() - ); + pub async fn build_server(&self, tx: Sender) -> Result<(), ServerInstallError> { + info!( + "installing {} v{} for minecraft {} to {}", + self.server, + self.server_version, + self.game_version, + self.root_dir.as_ref().display() + ); - self.download_server().await?; - info!("installing server"); + self.download_server().await?; + info!("installing server"); - let mut installer = Command::new("java"); - let installer = installer - .args(vec![ - "-jar", - self.root_dir - .as_ref() - .join("installer.jar") - .to_str() - .unwrap(), - ]) - .args( - self.server - .installer_args(self.root_dir.as_ref(), &self.game_version), - ) - .stdout(Stdio::piped()); + let mut installer = Command::new("java"); + let installer = installer + .args(vec![ + "-jar", + self + .root_dir + .as_ref() + .join("installer.jar") + .to_str() + .unwrap(), + ]) + .args( + self + .server + .installer_args(self.root_dir.as_ref(), &self.game_version), + ) + .stdout(Stdio::piped()); - debug!("installing with args: {:?}", installer.get_args()); + debug!("installing with args: {:?}", installer.get_args()); - if !self.root_dir.as_ref().exists() { - error!( - "{} does not exist! creating dir", - self.root_dir.as_ref().display() - ); - create_dir(&self.root_dir)?; - } + if !self.root_dir.as_ref().exists() { + error!( + "{} does not exist! creating dir", + self.root_dir.as_ref().display() + ); + create_dir(&self.root_dir)?; + } - let mut installer = installer.spawn()?; - { - let stdout = BufReader::new(installer.stdout.as_mut().unwrap()); + let mut installer = installer.spawn()?; + { + let stdout = BufReader::new(installer.stdout.as_mut().unwrap()); - for line in stdout.lines() { - tx.send(line?) - .context("while installing server (tx -> rx)")?; - } - } + for line in stdout.lines() { + tx.send(line?) + .context("while installing server (tx -> rx)")?; + } + } - let stat = installer.wait()?; - if !stat.success() { - error!("installer exited with code {}", stat); - } + let stat = installer.wait()?; + if !stat.success() { + error!("installer exited with code {}", stat); + } - info!("installer exited with code {}", stat); - info!("running post-install utilities"); + info!("installer exited with code {}", stat); + info!("running post-install utilities"); - post::add_run_sh(&self.root_dir, self.server)?; - post::write_user_jvm_args(&self.root_dir, "-Xms2G -Xmx8G -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200 -XX:+UnlockExperimentalVMOptions -XX:+DisableExplicitGC -XX:+AlwaysPreTouch -XX:G1NewSizePercent=30 -XX:G1MaxNewSizePercent=40 -XX:G1HeapRegionSize=8M -XX:G1ReservePercent=20 -XX:G1HeapWastePercent=5 -XX:G1MixedGCCountTarget=4 -XX:InitiatingHeapOccupancyPercent=15 -XX:G1MixedGCLiveThresholdPercent=90 -XX:G1RSetUpdatingPauseTimePercent=5 -XX:SurvivorRatio=32 -XX:+PerfDisableSharedMem -XX:MaxTenuringThreshold=1 -Dusing.aikars.flags=https://mcflags.emc.gs -Daikars.new.flags=true") + post::add_run_sh(&self.root_dir, self.server)?; + post::write_user_jvm_args(&self.root_dir, "-Xms2G -Xmx8G -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200 -XX:+UnlockExperimentalVMOptions -XX:+DisableExplicitGC -XX:+AlwaysPreTouch -XX:G1NewSizePercent=30 -XX:G1MaxNewSizePercent=40 -XX:G1HeapRegionSize=8M -XX:G1ReservePercent=20 -XX:G1HeapWastePercent=5 -XX:G1MixedGCCountTarget=4 -XX:InitiatingHeapOccupancyPercent=15 -XX:G1MixedGCLiveThresholdPercent=90 -XX:G1RSetUpdatingPauseTimePercent=5 -XX:SurvivorRatio=32 -XX:+PerfDisableSharedMem -XX:MaxTenuringThreshold=1 -Dusing.aikars.flags=https://mcflags.emc.gs -Daikars.new.flags=true") .context("while writing user_jvm_args.txt")?; - Ok(()) - } - - async fn download_server(&self) -> Result<(), ServerInstallError> { - let mut artifact: MavenArtifact = self.server.into(); - let versions = get_versions(&artifact).await?; + Ok(()) + } - if !versions - .versioning - .versions() - .contains(&self.server_version.clone()) - { - error!("unable to find version {}", self.server_version); + async fn download_server(&self) -> Result<(), ServerInstallError> { + let mut artifact: MavenArtifact = self.server.into(); + let versions = get_versions(&artifact).await?; - return Err(ServerInstallError::Version( - self.server.artifact_name(self.server_version.clone()), - )); - } + if !versions + .versioning + .versions() + .contains(&self.server_version.clone()) + { + error!("unable to find version {}", self.server_version); - info!("version {} resolved!", self.server_version); - artifact.set_version(self.server_version.clone()); + return Err(ServerInstallError::Version( + self.server.artifact_name(self.server_version.clone()), + )); + } - let artifact_url = - get_artifact(&artifact, self.server.artifact_name(&self.server_version))?; + info!("version {} resolved!", self.server_version); + artifact.set_version(self.server_version.clone()); - info!("resolved artifact to {:?}", artifact_url); - info!("starting download..."); - let mut file = BufWriter::new( - File::create_new(self.root_dir.as_ref().join("installer.jar")) - .context("while creating file")?, - ); - let mut stream = get(artifact_url).await?.bytes_stream(); + let artifact_url = get_artifact(&artifact, self.server.artifact_name(&self.server_version))?; - let mut total = 0; - while let Some(chunk) = stream.next().await { - total += file.write(&chunk?).context("while downloading file")?; - } + info!("resolved artifact to {:?}", artifact_url); + info!("starting download..."); + let mut file = BufWriter::new( + File::create_new(self.root_dir.as_ref().join("installer.jar")) + .context("while creating file")?, + ); + let mut stream = get(artifact_url).await?.bytes_stream(); - info!("finished download (downloaded {} bytes)", total); - Ok(()) + let mut total = 0; + while let Some(chunk) = stream.next().await { + total += file.write(&chunk?).context("while downloading file")?; } + + info!("finished download (downloaded {} bytes)", total); + Ok(()) + } } pub trait ServerSoftwareMeta: Display + Into + Copy { - fn artifact_name(&self, version: V) -> String; - fn installer_args<'a, I>(&self, installer_dir: &'a I, game_version: &'a str) -> Vec<&'a OsStr> - where - I: AsRef + ?Sized + 'a; - fn run_sh_content(&self) -> Vec; + fn artifact_name(&self, version: V) -> String; + fn installer_args<'a, I>(&self, installer_dir: &'a I, game_version: &'a str) -> Vec<&'a OsStr> + where + I: AsRef + ?Sized + 'a; + fn run_sh_content(&self) -> Vec; } impl Display for ServerSoftware { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Self::Forge => "forge", - Self::Neoforge => "neoforge", - Self::Fabric => "fabric", - Self::Quilt => "quilt", - Self::Glowstone => "glowstone", - } - ) - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Forge => "forge", + Self::Neoforge => "neoforge", + Self::Fabric => "fabric", + Self::Quilt => "quilt", + Self::Glowstone => "glowstone", + } + ) + } } impl From for MavenArtifact { - fn from(value: ServerSoftware) -> Self { - match value { - ServerSoftware::Forge => "maven.minecraftforge.net:net.minecraftforge:forge:", - ServerSoftware::Neoforge => "maven.neoforged.net/releases:net.neoforged:neoforge:", - ServerSoftware::Fabric => "maven.fabricmc.net:net.fabricmc:fabric-installer:", - ServerSoftware::Quilt => { - "maven.quiltmc.org/repository/release:org.quiltm:quilt-installer:" - } - ServerSoftware::Glowstone => { - "repo.glowstone.net/content/repositories/snapshots:net.glowstone:glowstone:" - } - } - .parse() - .unwrap() + fn from(value: ServerSoftware) -> Self { + match value { + ServerSoftware::Forge => "maven.minecraftforge.net:net.minecraftforge:forge:", + ServerSoftware::Neoforge => "maven.neoforged.net/releases:net.neoforged:neoforge:", + ServerSoftware::Fabric => "maven.fabricmc.net:net.fabricmc:fabric-installer:", + ServerSoftware::Quilt => "maven.quiltmc.org/repository/release:org.quiltm:quilt-installer:", + ServerSoftware::Glowstone => { + "repo.glowstone.net/content/repositories/snapshots:net.glowstone:glowstone:" + } } + .parse() + .unwrap() + } } impl ServerSoftwareMeta for ServerSoftware { - fn artifact_name(&self, version: V) -> String { - match self { - Self::Forge => format!("forge-{}-installer.jar", version), - Self::Neoforge => format!("neoforge-{}-installer.jar", version), - Self::Quilt => format!("quilt-installer-{}.jar", version), - Self::Fabric => format!("fabric-installer-{}.jar", version), - Self::Glowstone => todo!(), // TODO: Fix this - } + fn artifact_name(&self, version: V) -> String { + match self { + Self::Forge => format!("forge-{}-installer.jar", version), + Self::Neoforge => format!("neoforge-{}-installer.jar", version), + Self::Quilt => format!("quilt-installer-{}.jar", version), + Self::Fabric => format!("fabric-installer-{}.jar", version), + Self::Glowstone => todo!(), // TODO: Fix this } + } - fn installer_args<'a, I>(&self, install_dir: &'a I, game_version: &'a str) -> Vec<&'a OsStr> - where - I: AsRef + ?Sized + 'a, - { - match self { - Self::Forge => args!["--installServer", install_dir], - Self::Neoforge => args!["--installServer", install_dir], - Self::Quilt => args![ - "install", - "server", - game_version, - "--install-dir", - install_dir, - "--create-scripts", - "--download-server" - ], - Self::Fabric => args![ - "server", - "-dir", - install_dir, - "-mcversion", - game_version, - "-downloadMinecraft", - ], - Self::Glowstone => todo!(), // TODO: Also this - } + fn installer_args<'a, I>(&self, install_dir: &'a I, game_version: &'a str) -> Vec<&'a OsStr> + where + I: AsRef + ?Sized + 'a, + { + match self { + Self::Forge => args!["--installServer", install_dir], + Self::Neoforge => args!["--installServer", install_dir], + Self::Quilt => args![ + "install", + "server", + game_version, + "--install-dir", + install_dir, + "--create-scripts", + "--download-server" + ], + Self::Fabric => args![ + "server", + "-dir", + install_dir, + "-mcversion", + game_version, + "-downloadMinecraft", + ], + Self::Glowstone => todo!(), // TODO: Also this } + } - fn run_sh_content(&self) -> Vec { - match self { - ServerSoftware::Fabric | ServerSoftware::Quilt => vec![ - "#!/usr/bin/env sh".to_string(), - format!( - "java -jar {}-server-launch.jar @user_jvm_args.txt \"$@\"", - self.to_string() - ), - ], - _ => vec![ - "#!/usr/bin/env sh".to_string(), - "java -jar server.jar @user_jvm_args.txt \"$@\"".to_string(), - ], - } + fn run_sh_content(&self) -> Vec { + match self { + ServerSoftware::Fabric | ServerSoftware::Quilt => vec![ + "#!/usr/bin/env sh".to_string(), + format!( + "java -jar {}-server-launch.jar @user_jvm_args.txt \"$@\"", + self.to_string() + ), + ], + _ => vec![ + "#!/usr/bin/env sh".to_string(), + "java -jar server.jar @user_jvm_args.txt \"$@\"".to_string(), + ], } + } } diff --git a/denji/src/shell/post.rs b/denji/src/shell/post.rs index 58cb793..e9aaaad 100644 --- a/denji/src/shell/post.rs +++ b/denji/src/shell/post.rs @@ -10,90 +10,92 @@ use std::io::{BufReader, BufWriter}; use std::os::unix::fs::OpenOptionsExt; pub(super) fn add_run_sh, S: super::ServerSoftwareMeta>( - root_dir: P, - server_type: S, + root_dir: P, + server_type: S, ) -> anyhow::Result<()> { - let filename = root_dir.as_ref().join("run.sh"); + let filename = root_dir.as_ref().join("run.sh"); - info!("writing initializer script at {:?}", filename.display()); - let mut lines = get_lines(&filename).unwrap_or_else(|e| { - let selected_default = server_type.run_sh_content(); + info!("writing initializer script at {:?}", filename.display()); + let mut lines = get_lines(&filename).unwrap_or_else(|e| { + let selected_default = server_type.run_sh_content(); - error!("failed to get lines: {}", e); - warn!("run.sh opts will be defaulted to: {:?}", selected_default); + error!("failed to get lines: {}", e); + warn!("run.sh opts will be defaulted to: {:?}", selected_default); - selected_default - }); + selected_default + }); - if let Some(line) = lines.last_mut() { - *line = get_modded_line(line); - } + if let Some(line) = lines.last_mut() { + *line = get_modded_line(line); + } - // dunno what to name this, plus it looks really - // dirty inside `BufWriter::new` 🤡 - let write_file_handle = File::options() - .mode(0o755) - .create(true) - .truncate(true) - .write(true) - .open(filename) - .context("while creating run.sh file")?; + // dunno what to name this, plus it looks really + // dirty inside `BufWriter::new` 🤡 + let write_file_handle = File::options() + .mode(0o755) + .create(true) + .truncate(true) + .write(true) + .open(filename) + .context("while creating run.sh file")?; - let mut write_file = BufWriter::new(write_file_handle); + let mut write_file = BufWriter::new(write_file_handle); - for line in lines { - writeln!(write_file, "{}", line).context("while writing run.sh file")?; - } + for line in lines { + writeln!(write_file, "{}", line).context("while writing run.sh file")?; + } - Ok(()) + Ok(()) } pub fn agree_eula>(base_dir: P) -> anyhow::Result { - let mut file = - File::create(base_dir.as_ref().join("eula.txt")).context("while eula.txt creating file")?; + let mut file = + File::create(base_dir.as_ref().join("eula.txt")).context("while eula.txt creating file")?; - file.write(b"eula=true") - .context("while writing eula.txt to file") + file + .write(b"eula=true") + .context("while writing eula.txt to file") } pub(super) fn write_user_jvm_args>( - base_dir: P, - args: T, + base_dir: P, + args: T, ) -> anyhow::Result<()> { - let filename = base_dir.as_ref().join("user_jvm_args.txt"); - info!("writing JVM args at {:?}", filename.display()); + let filename = base_dir.as_ref().join("user_jvm_args.txt"); + info!("writing JVM args at {:?}", filename.display()); - let mut file = File::create(filename).context("while creating jvm args file")?; + let mut file = File::create(filename).context("while creating jvm args file")?; - file.write_all(args.to_string().as_bytes()) - .context("while writing jvm args file") + file + .write_all(args.to_string().as_bytes()) + .context("while writing jvm args file") } fn get_lines(filename: &PathBuf) -> anyhow::Result> { - let read_file = BufReader::new(File::open(filename).context("while reading run.sh lines")?); - let lines: Vec = read_file - .lines() - .collect::>() - .context("while reading run.sh lines")?; + let read_file = BufReader::new(File::open(filename).context("while reading run.sh lines")?); + let lines: Vec = read_file + .lines() + .collect::>() + .context("while reading run.sh lines")?; - Ok(lines) + Ok(lines) } fn get_modded_line(line: &String) -> String { - debug!("recieved line: {}", line); + debug!("recieved line: {}", line); - let mut args = line.split(' ').collect::>(); - // index of the second to the last argument, typically a `"$@"` - let sttl_arg_index = args.len() - 1; + let mut args = line.split(' ').collect::>(); + // index of the second to the last argument, typically a `"$@"` + let sttl_arg_index = args.len() - 1; - // - if !line.contains("user_jvm_args.txt") { - args.insert(2, "@user_jvm_args.txt"); - } else if !line.contains("\"$@\"") { - args.push("\"$@\""); - } + // + if !line.contains("user_jvm_args.txt") { + args.insert(2, "@user_jvm_args.txt"); + } else if !line.contains("\"$@\"") { + args.push("\"$@\""); + } - args.insert(sttl_arg_index, "--nogui"); + args.insert(sttl_arg_index, "--nogui"); - args.join(" ").to_string() + args.join(" ").to_string() } diff --git a/hangar/src/api/project.rs b/hangar/src/api/project.rs index af3f939..90c12ad 100644 --- a/hangar/src/api/project.rs +++ b/hangar/src/api/project.rs @@ -4,37 +4,39 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum ProjectError { - #[error("http error: {0}")] - Http(#[from] reqwest::Error), + #[error("http error: {0}")] + Http(#[from] reqwest::Error), } pub async fn search_project( - client: &Client, - params: &SearchQuery, + client: &Client, + params: &SearchQuery, ) -> Result { - Ok(client - .get(format!("{}/api/v1/projects", super::HANGAR_ENDPOINT)) - .query(params) - .send() - .await? - .json() - .await?) + Ok( + client + .get(format!("{}/api/v1/projects", super::HANGAR_ENDPOINT)) + .query(params) + .send() + .await? + .json() + .await?, + ) } #[cfg(test)] mod tests { - use super::*; - use crate::SearchQueryBuilder; + use super::*; + use crate::SearchQueryBuilder; - #[tokio::test] - async fn test_search_project() { - let client = Client::new(); - let query = SearchQueryBuilder::default() - .query("ViaVersion") - .version("1.20.1") - .build(); + #[tokio::test] + async fn test_search_project() { + let client = Client::new(); + let query = SearchQueryBuilder::default() + .query("ViaVersion") + .version("1.20.1") + .build(); - let projects = search_project(&client, &query).await; - assert!(projects.is_ok()); - } + let projects = search_project(&client, &query).await; + assert!(projects.is_ok()); + } } diff --git a/hangar/src/api/version.rs b/hangar/src/api/version.rs index 3a76b03..8d0465e 100644 --- a/hangar/src/api/version.rs +++ b/hangar/src/api/version.rs @@ -1,87 +1,91 @@ use std::fmt::Display; use crate::types::{ - project::HangarProject, query::version::VersionQuery, version::HangarVersion, HangarPlatform, - HangarVersions, + project::HangarProject, query::version::VersionQuery, version::HangarVersion, HangarPlatform, + HangarVersions, }; use reqwest::Client; use thiserror::Error; #[derive(Debug, Error)] pub enum VersionError { - #[error("http error: {0}")] - Http(#[from] reqwest::Error), + #[error("http error: {0}")] + Http(#[from] reqwest::Error), } pub async fn get_versions( - client: &Client, - project: &HangarProject, - params: &VersionQuery, + client: &Client, + project: &HangarProject, + params: &VersionQuery, ) -> Result { - Ok(client - .get(format!( - "{}/api/v1/projects/{}/versions", - super::HANGAR_ENDPOINT, - project.namespace.slug - )) - .query(params) - .send() - .await? - .json() - .await?) + Ok( + client + .get(format!( + "{}/api/v1/projects/{}/versions", + super::HANGAR_ENDPOINT, + project.namespace.slug + )) + .query(params) + .send() + .await? + .json() + .await?, + ) } pub async fn get_version( - client: &Client, - project: &HangarProject, - version: String, - params: &VersionQuery, + client: &Client, + project: &HangarProject, + version: String, + params: &VersionQuery, ) -> Result { - Ok(client - .get(format!( - "{}/api/v1/projects/{}/versions/{}", - super::HANGAR_ENDPOINT, - project.namespace.slug, - version - )) - .query(params) - .send() - .await? - .json() - .await?) + Ok( + client + .get(format!( + "{}/api/v1/projects/{}/versions/{}", + super::HANGAR_ENDPOINT, + project.namespace.slug, + version + )) + .query(params) + .send() + .await? + .json() + .await?, + ) } pub fn get_download_link(slug: T, name: T, platform: HangarPlatform) -> String { - format!( - "{}/api/v1/projects/{}/versions/{}/{}/download", - super::HANGAR_ENDPOINT, - slug, - name, - platform - ) + format!( + "{}/api/v1/projects/{}/versions/{}/{}/download", + super::HANGAR_ENDPOINT, + slug, + name, + platform + ) } #[cfg(test)] mod tests { - use super::*; - use crate::{search_project, SearchQueryBuilder, VersionQueryBuilder}; + use super::*; + use crate::{search_project, SearchQueryBuilder, VersionQueryBuilder}; - #[tokio::test] - async fn test_get_versions() { - let client = Client::new(); - let pquery = SearchQueryBuilder::default() - .query("ViaVersion") - .version("1.20.1") - .build(); + #[tokio::test] + async fn test_get_versions() { + let client = Client::new(); + let pquery = SearchQueryBuilder::default() + .query("ViaVersion") + .version("1.20.1") + .build(); - let projects = search_project(&client, &pquery).await.unwrap(); - let project = projects.result.first().unwrap(); + let projects = search_project(&client, &pquery).await.unwrap(); + let project = projects.result.first().unwrap(); - let vquery = VersionQueryBuilder::default() - .platform(HangarPlatform::Paper) - .build(); - let versions = get_versions(&client, project, &vquery).await; + let vquery = VersionQueryBuilder::default() + .platform(HangarPlatform::Paper) + .build(); + let versions = get_versions(&client, project, &vquery).await; - assert!(versions.is_ok_and(|v| !v.result.is_empty())) - } + assert!(versions.is_ok_and(|v| !v.result.is_empty())) + } } diff --git a/hangar/src/types/mod.rs b/hangar/src/types/mod.rs index f374811..77802f0 100644 --- a/hangar/src/types/mod.rs +++ b/hangar/src/types/mod.rs @@ -15,34 +15,34 @@ type DateTime = chrono::DateTime; #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub enum HangarVisibility { - Public, - New, - NeedsChanges, - NeedsApproval, - SoftDelete, + Public, + New, + NeedsChanges, + NeedsApproval, + SoftDelete, } #[derive(Debug, Default, Deserialize, Serialize, Hash, PartialEq, Eq)] #[serde(rename_all = "UPPERCASE")] pub enum HangarPlatform { - #[default] - Paper, - Waterfall, - Velocity, + #[default] + Paper, + Waterfall, + Velocity, } impl Display for HangarPlatform { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Self::Paper => "PAPER", - Self::Waterfall => "WATERFALL", - Self::Velocity => "VELOCITY", - } - ) - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Paper => "PAPER", + Self::Waterfall => "WATERFALL", + Self::Velocity => "VELOCITY", + } + ) + } } bitflags! { @@ -55,27 +55,27 @@ bitflags! { } impl<'de> Deserialize<'de> for HangarTags { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let tags: Vec = Vec::deserialize(deserializer)?; - let mut flags = Self::empty(); + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let tags: Vec = Vec::deserialize(deserializer)?; + let mut flags = Self::empty(); - for tag in tags { - match tag.as_str() { - "ADDON" => flags |= Self::ADDON, - "LIBRARY" => flags |= Self::LIBRARY, - "SUPPORTS_FOLIA" => flags |= Self::SUPPORTS_FOLIA, - other => { - return Err(serde::de::Error::unknown_variant( - other, - &["ADDON", "LIBRARY", "SUPPORTS_FOLIA"], - )) - } - } + for tag in tags { + match tag.as_str() { + "ADDON" => flags |= Self::ADDON, + "LIBRARY" => flags |= Self::LIBRARY, + "SUPPORTS_FOLIA" => flags |= Self::SUPPORTS_FOLIA, + other => { + return Err(serde::de::Error::unknown_variant( + other, + &["ADDON", "LIBRARY", "SUPPORTS_FOLIA"], + )) } - - Ok(flags) + } } + + Ok(flags) + } } diff --git a/hangar/src/types/project.rs b/hangar/src/types/project.rs index a5facd4..48e8866 100644 --- a/hangar/src/types/project.rs +++ b/hangar/src/types/project.rs @@ -5,138 +5,138 @@ use std::{fmt::Debug, rc::Rc}; #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct HangarProjects { - pub pagination: HangarProjectsPagination, - pub result: Rc<[HangarProject]>, + pub pagination: HangarProjectsPagination, + pub result: Rc<[HangarProject]>, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct HangarProjectsPagination { - pub limit: u8, - pub offset: u8, - pub count: u16, + pub limit: u8, + pub offset: u8, + pub count: u16, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct HangarProject { - pub created_at: DateTime, - pub name: Rc, - pub namespace: HangarProjectNamespace, - pub last_updated: DateTime, - pub avatar_url: Rc, - pub description: Rc, - pub category: HangarProjectCategory, - pub visibility: HangarVisibility, - pub settings: HangarProjectSettings, + pub created_at: DateTime, + pub name: Rc, + pub namespace: HangarProjectNamespace, + pub last_updated: DateTime, + pub avatar_url: Rc, + pub description: Rc, + pub category: HangarProjectCategory, + pub visibility: HangarVisibility, + pub settings: HangarProjectSettings, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct HangarProjectSettings { - pub links: Option>, - pub tags: HangarTags, - pub license: HangarProjectLicense, - pub keywords: Vec>, + pub links: Option>, + pub tags: HangarTags, + pub license: HangarProjectLicense, + pub keywords: Vec>, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct HangarProjectNamespace { - pub owner: Rc, - pub slug: Rc, + pub owner: Rc, + pub slug: Rc, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct HangarProjectLinks { - #[serde(deserialize_with = "deserialize_links")] - pub links: Rc<[HangarProjectLink]>, + #[serde(deserialize_with = "deserialize_links")] + pub links: Rc<[HangarProjectLink]>, } #[derive(Debug, Deserialize)] pub struct HangarProjectLink { - pub id: u8, - pub name: Rc, - #[serde(deserialize_with = "deserialize_null_default")] - pub url: Rc, + pub id: u8, + pub name: Rc, + #[serde(deserialize_with = "deserialize_null_default")] + pub url: Rc, } #[derive(Debug, Deserialize)] #[serde(rename_all = "snake_case")] pub enum HangarProjectCategory { - AdminTools, - Chat, - DevTools, - Economy, - Gameplay, - Games, - Protection, - RolePlaying, - WorldManagement, - Misc, - Undefined, + AdminTools, + Chat, + DevTools, + Economy, + Gameplay, + Games, + Protection, + RolePlaying, + WorldManagement, + Misc, + Undefined, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct HangarProjectLicense { - pub name: Option>, - pub url: Option>, + pub name: Option>, + pub url: Option>, - #[serde(rename = "type")] - pub license_type: Rc, + #[serde(rename = "type")] + pub license_type: Rc, } fn deserialize_links<'de, D>(deserializer: D) -> Result, D::Error> where - D: serde::Deserializer<'de>, + D: serde::Deserializer<'de>, { - struct LinksVisitor; + struct LinksVisitor; - impl<'de> serde::de::Visitor<'de> for LinksVisitor { - type Value = Rc<[HangarProjectLink]>; + impl<'de> serde::de::Visitor<'de> for LinksVisitor { + type Value = Rc<[HangarProjectLink]>; - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a list of HangarProjectLinks") - } - - fn visit_seq(self, mut seq: A) -> Result - where - A: serde::de::SeqAccess<'de>, - { - let mut links = Vec::new(); + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a list of HangarProjectLinks") + } - while let Some(link) = seq.next_element::()? { - if !link.url.is_empty() { - links.push(link); - } - } + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut links = Vec::new(); - Ok(Rc::from_iter(links.into_boxed_slice())) + while let Some(link) = seq.next_element::()? { + if !link.url.is_empty() { + links.push(link); } + } + + Ok(Rc::from_iter(links.into_boxed_slice())) } + } - deserializer.deserialize_seq(LinksVisitor) + deserializer.deserialize_seq(LinksVisitor) } fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result where - T: Default + Deserialize<'de>, - D: serde::Deserializer<'de>, + T: Default + Deserialize<'de>, + D: serde::Deserializer<'de>, { - let opt = Option::deserialize(deserializer)?; - Ok(opt.unwrap_or_default()) + let opt = Option::deserialize(deserializer)?; + Ok(opt.unwrap_or_default()) } #[cfg(test)] mod tests { - use super::*; - use serde_json::from_str; + use super::*; + use serde_json::from_str; - #[test] - fn one_project() { - let raw = r#" + #[test] + fn one_project() { + let raw = r#" { "createdAt": "2022-12-22T14:04:48.773082Z", "name": "Maintenance", @@ -214,15 +214,15 @@ mod tests { } } }"#; - let project = from_str(raw); - assert!(project.is_ok()); + let project = from_str(raw); + assert!(project.is_ok()); - let _project: HangarProject = project.unwrap(); - } + let _project: HangarProject = project.unwrap(); + } - #[test] - fn many_projects() { - let raw = r#" + #[test] + fn many_projects() { + let raw = r#" { "pagination": { "limit": 25, @@ -2261,10 +2261,10 @@ mod tests { } "#; - let projects = from_str(raw); + let projects = from_str(raw); - assert!(projects.is_ok()); + assert!(projects.is_ok()); - let _projects: HangarProjects = projects.unwrap(); - } + let _projects: HangarProjects = projects.unwrap(); + } } diff --git a/hangar/src/types/query/mod.rs b/hangar/src/types/query/mod.rs index b078edc..f9006b2 100644 --- a/hangar/src/types/query/mod.rs +++ b/hangar/src/types/query/mod.rs @@ -8,39 +8,39 @@ use serde::Serialize; #[derive(Debug, Serialize)] pub struct GenericPagination { - pub(crate) limit: u8, - pub(crate) offset: u8, + pub(crate) limit: u8, + pub(crate) offset: u8, } impl Default for GenericPagination { - fn default() -> Self { - Self { - limit: 25, - offset: 0, - } + fn default() -> Self { + Self { + limit: 25, + offset: 0, } + } } impl GenericPagination { - pub fn set_limit(&mut self, limit: u8) { - self.limit = limit; - } + pub fn set_limit(&mut self, limit: u8) { + self.limit = limit; + } - pub fn set_offset(&mut self, offset: u8) { - self.offset = offset; - } + pub fn set_offset(&mut self, offset: u8) { + self.offset = offset; + } } #[cfg(test)] mod tests { - use super::*; - use serde_urlencoded::to_string; + use super::*; + use serde_urlencoded::to_string; - #[test] - fn pagination_serialization() { - let pagination = GenericPagination::default(); - let res = to_string(&pagination); + #[test] + fn pagination_serialization() { + let pagination = GenericPagination::default(); + let res = to_string(&pagination); - assert_eq!(&res.unwrap(), "limit=25&offset=0"); - } + assert_eq!(&res.unwrap(), "limit=25&offset=0"); + } } diff --git a/hangar/src/types/query/search.rs b/hangar/src/types/query/search.rs index af7171d..ea09305 100644 --- a/hangar/src/types/query/search.rs +++ b/hangar/src/types/query/search.rs @@ -7,126 +7,126 @@ use std::rc::Rc; #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct SearchQuery { - pub(crate) prioritize_exact_match: bool, - #[serde(flatten)] - pub(crate) pagination: GenericPagination, - pub(crate) sort: SortBy, - #[serde(skip_serializing_if = "str::is_empty")] - pub(crate) category: Rc, - pub(crate) platform: HangarPlatform, - #[serde(skip_serializing_if = "str::is_empty")] - pub(crate) owner: Rc, - #[serde(skip_serializing_if = "str::is_empty")] - pub(crate) query: Rc, - #[serde(skip_serializing_if = "str::is_empty")] - pub(crate) license: Rc, - #[serde(skip_serializing_if = "str::is_empty")] - pub(crate) version: Rc, - #[serde(skip_serializing_if = "HangarTags::is_empty")] - pub(crate) tag: HangarTags, + pub(crate) prioritize_exact_match: bool, + #[serde(flatten)] + pub(crate) pagination: GenericPagination, + pub(crate) sort: SortBy, + #[serde(skip_serializing_if = "str::is_empty")] + pub(crate) category: Rc, + pub(crate) platform: HangarPlatform, + #[serde(skip_serializing_if = "str::is_empty")] + pub(crate) owner: Rc, + #[serde(skip_serializing_if = "str::is_empty")] + pub(crate) query: Rc, + #[serde(skip_serializing_if = "str::is_empty")] + pub(crate) license: Rc, + #[serde(skip_serializing_if = "str::is_empty")] + pub(crate) version: Rc, + #[serde(skip_serializing_if = "HangarTags::is_empty")] + pub(crate) tag: HangarTags, } #[derive(Debug, Serialize, Default)] #[serde(rename_all = "snake_case")] pub enum SortBy { - Views, - #[default] - Downloads, - Newest, - Stars, - Updated, - RecentDownloads, - RecentViews, - Slugs, + Views, + #[default] + Downloads, + Newest, + Stars, + Updated, + RecentDownloads, + RecentViews, + Slugs, } #[derive(Debug, Default)] pub struct SearchQueryBuilder { - prioritize_exact_match: Option, - pagination: Option, - sort: Option, - category: Option>, - platform: Option, - owner: Option>, - query: Option>, - license: Option>, - version: Option>, - tag: Option, + prioritize_exact_match: Option, + pagination: Option, + sort: Option, + category: Option>, + platform: Option, + owner: Option>, + query: Option>, + license: Option>, + version: Option>, + tag: Option, } impl SearchQueryBuilder { - pub fn prioritize_exact_match(mut self, prioritize_exact_match: bool) -> Self { - self.prioritize_exact_match = Some(prioritize_exact_match); + pub fn prioritize_exact_match(mut self, prioritize_exact_match: bool) -> Self { + self.prioritize_exact_match = Some(prioritize_exact_match); - self - } + self + } - pub fn pagination(mut self, pagination: GenericPagination) -> Self { - self.pagination = Some(pagination); + pub fn pagination(mut self, pagination: GenericPagination) -> Self { + self.pagination = Some(pagination); - self - } + self + } - pub fn sort(mut self, sort: SortBy) -> Self { - self.sort = Some(sort); + pub fn sort(mut self, sort: SortBy) -> Self { + self.sort = Some(sort); - self - } + self + } - pub fn category(mut self, category: T) -> Self { - self.category = Some(Rc::from(category.to_string().into_boxed_str())); + pub fn category(mut self, category: T) -> Self { + self.category = Some(Rc::from(category.to_string().into_boxed_str())); - self - } + self + } - pub fn platform(mut self, platform: HangarPlatform) -> Self { - self.platform = Some(platform); + pub fn platform(mut self, platform: HangarPlatform) -> Self { + self.platform = Some(platform); - self - } + self + } - pub fn owner(mut self, owner: T) -> Self { - self.owner = Some(Rc::from(owner.to_string().into_boxed_str())); + pub fn owner(mut self, owner: T) -> Self { + self.owner = Some(Rc::from(owner.to_string().into_boxed_str())); - self - } + self + } - pub fn query(mut self, query: T) -> Self { - self.query = Some(Rc::from(query.to_string().into_boxed_str())); + pub fn query(mut self, query: T) -> Self { + self.query = Some(Rc::from(query.to_string().into_boxed_str())); - self - } + self + } - pub fn license(mut self, license: T) -> Self { - self.license = Some(Rc::from(license.to_string().into_boxed_str())); + pub fn license(mut self, license: T) -> Self { + self.license = Some(Rc::from(license.to_string().into_boxed_str())); - self - } + self + } - pub fn version(mut self, version: T) -> Self { - self.version = Some(Rc::from(version.to_string().into_boxed_str())); + pub fn version(mut self, version: T) -> Self { + self.version = Some(Rc::from(version.to_string().into_boxed_str())); - self - } + self + } - pub fn tag(mut self, tag: HangarTags) -> Self { - self.tag = Some(tag); + pub fn tag(mut self, tag: HangarTags) -> Self { + self.tag = Some(tag); - self - } + self + } - pub fn build(self) -> SearchQuery { - SearchQuery { - prioritize_exact_match: self.prioritize_exact_match.unwrap_or(true), - pagination: self.pagination.unwrap_or_default(), - sort: self.sort.unwrap_or_default(), - category: self.category.unwrap_or_default(), - platform: self.platform.unwrap_or_default(), - owner: self.owner.unwrap_or_default(), - query: self.query.unwrap_or_default(), - license: self.license.unwrap_or_default(), - version: self.version.unwrap_or_default(), - tag: self.tag.unwrap_or_default(), - } + pub fn build(self) -> SearchQuery { + SearchQuery { + prioritize_exact_match: self.prioritize_exact_match.unwrap_or(true), + pagination: self.pagination.unwrap_or_default(), + sort: self.sort.unwrap_or_default(), + category: self.category.unwrap_or_default(), + platform: self.platform.unwrap_or_default(), + owner: self.owner.unwrap_or_default(), + query: self.query.unwrap_or_default(), + license: self.license.unwrap_or_default(), + version: self.version.unwrap_or_default(), + tag: self.tag.unwrap_or_default(), } + } } diff --git a/hangar/src/types/query/version.rs b/hangar/src/types/query/version.rs index f151468..c2d7125 100644 --- a/hangar/src/types/query/version.rs +++ b/hangar/src/types/query/version.rs @@ -7,53 +7,53 @@ use std::rc::Rc; #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct VersionQuery { - #[serde(flatten)] - pub(crate) pagination: GenericPagination, - pub(crate) include_hidden_channels: bool, - pub(crate) platform: HangarPlatform, - #[serde(skip_serializing_if = "str::is_empty")] - pub(crate) platform_version: Rc, + #[serde(flatten)] + pub(crate) pagination: GenericPagination, + pub(crate) include_hidden_channels: bool, + pub(crate) platform: HangarPlatform, + #[serde(skip_serializing_if = "str::is_empty")] + pub(crate) platform_version: Rc, } #[derive(Debug, Default)] pub struct VersionQueryBuilder { - pagination: Option, - include_hidden_channels: Option, - platform: Option, - platform_version: Option>, + pagination: Option, + include_hidden_channels: Option, + platform: Option, + platform_version: Option>, } impl VersionQueryBuilder { - pub fn pagination(mut self, pagination: GenericPagination) -> Self { - self.pagination = Some(pagination); + pub fn pagination(mut self, pagination: GenericPagination) -> Self { + self.pagination = Some(pagination); - self - } + self + } - pub fn include_hidden_channels(mut self, include_hidden_channels: bool) -> Self { - self.include_hidden_channels = Some(include_hidden_channels); + pub fn include_hidden_channels(mut self, include_hidden_channels: bool) -> Self { + self.include_hidden_channels = Some(include_hidden_channels); - self - } + self + } - pub fn platform(mut self, platform: HangarPlatform) -> Self { - self.platform = Some(platform); + pub fn platform(mut self, platform: HangarPlatform) -> Self { + self.platform = Some(platform); - self - } + self + } - pub fn platform_version(mut self, platform_version: Rc) -> Self { - self.platform_version = Some(platform_version); + pub fn platform_version(mut self, platform_version: Rc) -> Self { + self.platform_version = Some(platform_version); - self - } + self + } - pub fn build(self) -> VersionQuery { - VersionQuery { - include_hidden_channels: self.include_hidden_channels.unwrap_or(true), - pagination: self.pagination.unwrap_or_default(), - platform: self.platform.unwrap_or_default(), - platform_version: self.platform_version.unwrap_or_default(), - } + pub fn build(self) -> VersionQuery { + VersionQuery { + include_hidden_channels: self.include_hidden_channels.unwrap_or(true), + pagination: self.pagination.unwrap_or_default(), + platform: self.platform.unwrap_or_default(), + platform_version: self.platform_version.unwrap_or_default(), } + } } diff --git a/hangar/src/types/version.rs b/hangar/src/types/version.rs index 4977fda..03bba6b 100644 --- a/hangar/src/types/version.rs +++ b/hangar/src/types/version.rs @@ -8,102 +8,102 @@ mod traits; #[derive(Debug, Deserialize)] pub struct HangarVersions { - pub pagination: HangarVersionsPagination, - pub result: Vec, + pub pagination: HangarVersionsPagination, + pub result: Vec, } #[derive(Debug, Deserialize)] pub struct HangarVersionsPagination { - pub limit: u8, - pub offset: u8, - pub count: u16, + pub limit: u8, + pub offset: u8, + pub count: u16, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct HangarVersion { - pub created_at: DateTime, - pub name: Rc, - pub visibility: HangarVisibility, - pub description: Rc, - pub author: Rc, - #[serde(deserialize_with = "traits::deserialize_kv")] - pub downloads: Vec, - #[serde(deserialize_with = "traits::deserialize_kv")] - pub plugin_dependencies: Vec, - #[serde(deserialize_with = "traits::deserialize_kv")] - pub platform_dependencies: Vec, + pub created_at: DateTime, + pub name: Rc, + pub visibility: HangarVisibility, + pub description: Rc, + pub author: Rc, + #[serde(deserialize_with = "traits::deserialize_kv")] + pub downloads: Vec, + #[serde(deserialize_with = "traits::deserialize_kv")] + pub plugin_dependencies: Vec, + #[serde(deserialize_with = "traits::deserialize_kv")] + pub platform_dependencies: Vec, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct HangarVersionDownload { - pub platform: HangarPlatform, + pub platform: HangarPlatform, - #[serde(flatten)] - pub details: HPDownloadDetails, + #[serde(flatten)] + pub details: HPDownloadDetails, } impl traits::KeyValueType for HangarVersionDownload { - type Key = HangarPlatform; - type Value = HPDownloadDetails; + type Key = HangarPlatform; + type Value = HPDownloadDetails; - fn init(key: Self::Key, value: Self::Value) -> Self { - Self { - platform: key, - details: value, - } + fn init(key: Self::Key, value: Self::Value) -> Self { + Self { + platform: key, + details: value, } + } } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct HangarVersionPluginDependencies { - pub name: Rc, + pub name: Rc, - #[serde(flatten)] - pub details: Rc<[HPPluginDependencyDetails]>, + #[serde(flatten)] + pub details: Rc<[HPPluginDependencyDetails]>, } impl traits::KeyValueType for HangarVersionPluginDependencies { - type Key = Rc; - type Value = Rc<[HPPluginDependencyDetails]>; + type Key = Rc; + type Value = Rc<[HPPluginDependencyDetails]>; - fn init(key: Self::Key, value: Self::Value) -> Self { - Self { - name: key, - details: value, - } + fn init(key: Self::Key, value: Self::Value) -> Self { + Self { + name: key, + details: value, } + } } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct HangarVersionPlatformDependencies { - pub platform: HangarPlatform, - pub version: Vec>, + pub platform: HangarPlatform, + pub version: Vec>, } impl traits::KeyValueType for HangarVersionPlatformDependencies { - type Key = HangarPlatform; - type Value = Vec>; + type Key = HangarPlatform; + type Value = Vec>; - fn init(key: Self::Key, value: Self::Value) -> Self { - Self { - platform: key, - version: value, - } + fn init(key: Self::Key, value: Self::Value) -> Self { + Self { + platform: key, + version: value, } + } } #[cfg(test)] mod tests { - use super::*; - use serde_json::from_str; + use super::*; + use serde_json::from_str; - #[test] - fn one_version() { - let raw = r#" + #[test] + fn one_version() { + let raw = r#" { "createdAt": "2024-05-17T13:48:41.703391Z", "name": "4.2.1", @@ -240,15 +240,15 @@ mod tests { } } "#; - let version = from_str(raw); + let version = from_str(raw); - assert!(version.is_ok()); - let _version: HangarVersion = version.unwrap(); - } + assert!(version.is_ok()); + let _version: HangarVersion = version.unwrap(); + } - #[test] - fn many_versions() { - let raw = r#" + #[test] + fn many_versions() { + let raw = r#" { "pagination": { "limit": 10, @@ -844,9 +844,9 @@ mod tests { } "#; - let versions = from_str(raw); + let versions = from_str(raw); - assert!(versions.is_ok()); - let _versions: HangarVersions = versions.unwrap(); - } + assert!(versions.is_ok()); + let _versions: HangarVersions = versions.unwrap(); + } } diff --git a/hangar/src/types/version/details.rs b/hangar/src/types/version/details.rs index fbcd014..798fbcb 100644 --- a/hangar/src/types/version/details.rs +++ b/hangar/src/types/version/details.rs @@ -6,23 +6,23 @@ use std::rc::Rc; #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct HPDownloadDetails { - pub file_info: HangarVersionDownloadFile, - pub external_url: Option>, - pub download_url: Option>, + pub file_info: HangarVersionDownloadFile, + pub external_url: Option>, + pub download_url: Option>, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct HPPluginDependencyDetails { - pub required: bool, - pub external_url: Option>, - pub platform: HangarPlatform, + pub required: bool, + pub external_url: Option>, + pub platform: HangarPlatform, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct HangarVersionDownloadFile { - pub name: Rc, - pub size_bytes: usize, - pub sha_256_hash: Rc, + pub name: Rc, + pub size_bytes: usize, + pub sha_256_hash: Rc, } diff --git a/hangar/src/types/version/traits.rs b/hangar/src/types/version/traits.rs index 18232f1..f9c90e7 100644 --- a/hangar/src/types/version/traits.rs +++ b/hangar/src/types/version/traits.rs @@ -3,24 +3,24 @@ use std::{collections::HashMap, hash::Hash}; use serde::{Deserialize, Deserializer}; pub(super) trait KeyValueType { - type Key; - type Value; + type Key; + type Value; - fn init(key: Self::Key, value: Self::Value) -> Self; + fn init(key: Self::Key, value: Self::Value) -> Self; } pub(super) fn deserialize_kv<'de, D, T>(deserializer: D) -> Result, D::Error> where - D: Deserializer<'de>, - T: KeyValueType, - T::Key: Deserialize<'de> + Hash + Eq, - T::Value: Deserialize<'de>, + D: Deserializer<'de>, + T: KeyValueType, + T::Key: Deserialize<'de> + Hash + Eq, + T::Value: Deserialize<'de>, { - let raw_map: HashMap = HashMap::deserialize(deserializer)?; - let deps = raw_map - .into_iter() - .map(|(key, value)| T::init(key, value)) - .collect(); + let raw_map: HashMap = HashMap::deserialize(deserializer)?; + let deps = raw_map + .into_iter() + .map(|(key, value)| T::init(key, value)) + .collect(); - Ok(deps) + Ok(deps) } diff --git a/mar/src/repository.rs b/mar/src/repository.rs index 4bfbc60..39a6b80 100644 --- a/mar/src/repository.rs +++ b/mar/src/repository.rs @@ -7,112 +7,112 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum RepositoryError { - #[error("version already available")] - VersionAvailable, + #[error("version already available")] + VersionAvailable, - #[error("version not available")] - NoVersion, + #[error("version not available")] + NoVersion, - #[error("http error: {0}")] - Http(#[from] reqwest::Error), + #[error("http error: {0}")] + Http(#[from] reqwest::Error), - #[error("xml deserialization error: {0}")] - XmlParse(#[from] quick_xml::DeError), + #[error("xml deserialization error: {0}")] + XmlParse(#[from] quick_xml::DeError), } pub async fn get_versions( - artifact: &MavenArtifact, + artifact: &MavenArtifact, ) -> Result { - if artifact.version.is_some() { - Err(RepositoryError::VersionAvailable) - } else { - debug!( - "got url: {}", - format!( - "{}/{}/{}/maven-metadata.xml", - artifact.base_url, - artifact.group_id.replace('.', "/"), - artifact.artifact_id - ) - ); - - let raw = get(format!( - "{}/{}/{}/maven-metadata.xml", - artifact.base_url, - artifact.group_id.replace('.', "/"), - artifact.artifact_id - )) - .await? - .text() - .await?; - - let parsed = from_str(&raw)?; - - Ok(parsed) - } + if artifact.version.is_some() { + Err(RepositoryError::VersionAvailable) + } else { + debug!( + "got url: {}", + format!( + "{}/{}/{}/maven-metadata.xml", + artifact.base_url, + artifact.group_id.replace('.', "/"), + artifact.artifact_id + ) + ); + + let raw = get(format!( + "{}/{}/{}/maven-metadata.xml", + artifact.base_url, + artifact.group_id.replace('.', "/"), + artifact.artifact_id + )) + .await? + .text() + .await?; + + let parsed = from_str(&raw)?; + + Ok(parsed) + } } pub fn get_artifact( - artifact_data: &MavenArtifact, - artifact_name: T, + artifact_data: &MavenArtifact, + artifact_name: T, ) -> Result { - if artifact_data.version.is_none() { - Err(RepositoryError::NoVersion) - } else { - let artifact_url = format!( - "{}/{}/{}/{}/{}", - artifact_data.base_url, - artifact_data.group_id.replace('.', "/"), - artifact_data.artifact_id, - artifact_data.version.clone().unwrap(), - artifact_name.to_string() - ); - - debug!("artifact url: {}", artifact_url); - Ok(artifact_url) - } + if artifact_data.version.is_none() { + Err(RepositoryError::NoVersion) + } else { + let artifact_url = format!( + "{}/{}/{}/{}/{}", + artifact_data.base_url, + artifact_data.group_id.replace('.', "/"), + artifact_data.artifact_id, + artifact_data.version.clone().unwrap(), + artifact_name.to_string() + ); + + debug!("artifact url: {}", artifact_url); + Ok(artifact_url) + } } #[cfg(test)] mod tests { - use super::*; - - #[tokio::test] - async fn test_get_versions() { - let artifact = MavenArtifactBuilder::default() - .with_base_url("https://maven.minecraftforge.net") - .with_artifact_id("forge") - .with_group_id("net.minecraftforge") - .build() - .unwrap(); - - let versions = get_versions(&artifact).await; - - assert!(versions.is_ok()); - } - - #[tokio::test] - async fn test_get_version() { - let mut artifact = MavenArtifactBuilder::default() - .with_base_url("https://maven.minecraftforge.net") - .with_artifact_id("forge") - .with_group_id("net.minecraftforge") - .build() - .unwrap(); - - let versions_list = get_versions(&artifact).await.unwrap(); - let selected_version = versions_list.versioning.latest(); - - artifact.set_version(selected_version); - let artifact_name = format!("forge-{}-installer.jar", selected_version); - let expected_artifact_url = format!( - "https://maven.minecraftforge.net/net/minecraftforge/forge/{0}/forge-{0}-installer.jar", - selected_version - ); - let artifact_url = get_artifact(&artifact, artifact_name); - - assert!(artifact_url.is_ok()); - let artifact_url = artifact_url.unwrap(); - assert_eq!(artifact_url, expected_artifact_url); - } + use super::*; + + #[tokio::test] + async fn test_get_versions() { + let artifact = MavenArtifactBuilder::default() + .with_base_url("https://maven.minecraftforge.net") + .with_artifact_id("forge") + .with_group_id("net.minecraftforge") + .build() + .unwrap(); + + let versions = get_versions(&artifact).await; + + assert!(versions.is_ok()); + } + + #[tokio::test] + async fn test_get_version() { + let mut artifact = MavenArtifactBuilder::default() + .with_base_url("https://maven.minecraftforge.net") + .with_artifact_id("forge") + .with_group_id("net.minecraftforge") + .build() + .unwrap(); + + let versions_list = get_versions(&artifact).await.unwrap(); + let selected_version = versions_list.versioning.latest(); + + artifact.set_version(selected_version); + let artifact_name = format!("forge-{}-installer.jar", selected_version); + let expected_artifact_url = format!( + "https://maven.minecraftforge.net/net/minecraftforge/forge/{0}/forge-{0}-installer.jar", + selected_version + ); + let artifact_url = get_artifact(&artifact, artifact_name); + + assert!(artifact_url.is_ok()); + let artifact_url = artifact_url.unwrap(); + assert_eq!(artifact_url, expected_artifact_url); + } } diff --git a/mar/src/types.rs b/mar/src/types.rs index 510d523..04f2ed4 100644 --- a/mar/src/types.rs +++ b/mar/src/types.rs @@ -6,150 +6,150 @@ pub use deserialize::*; #[derive(Default)] pub struct MavenArtifactBuilder { - pub(crate) base_url: Option, - pub(crate) group_id: Option, - pub(crate) artifact_id: Option, - pub(crate) version: Option, + pub(crate) base_url: Option, + pub(crate) group_id: Option, + pub(crate) artifact_id: Option, + pub(crate) version: Option, } #[derive(Debug)] pub struct MavenArtifact { - pub(crate) base_url: String, - pub(crate) group_id: String, - pub(crate) artifact_id: String, - pub(crate) version: Option, + pub(crate) base_url: String, + pub(crate) group_id: String, + pub(crate) artifact_id: String, + pub(crate) version: Option, } #[derive(Debug, Error)] pub enum MavenArtifactParseError { - #[error("input string is malformed: expected 3 semicolons, got {0}")] - TooLittleSemiColons(usize), - #[error("input string is malformed: expected at least 3 components")] - NotEnoughComponents, - #[error("input string is malformed")] - Malformed, + #[error("input string is malformed: expected 3 semicolons, got {0}")] + TooLittleSemiColons(usize), + #[error("input string is malformed: expected at least 3 components")] + NotEnoughComponents, + #[error("input string is malformed")] + Malformed, } impl FromStr for MavenArtifact { - type Err = MavenArtifactParseError; - - fn from_str(s: &str) -> Result { - match s.chars().filter(|c| *c == ':').count() { - 0 => return Err(MavenArtifactParseError::TooLittleSemiColons(0)), - 3 => (), - other => return Err(MavenArtifactParseError::TooLittleSemiColons(other)), - }; - - let parts = s.split_terminator(':').collect::>(); - - if parts.len() < 3 { - return Err(MavenArtifactParseError::NotEnoughComponents); - } - - let base_url = format!( - "https://{}", - parts.first().ok_or(MavenArtifactParseError::Malformed)? - ); - let group_id = parts - .get(1) - .ok_or(MavenArtifactParseError::Malformed)? - .to_string(); - let artifact_id = parts - .get(2) - .ok_or(MavenArtifactParseError::Malformed)? - .to_string(); - let version = parts.get(3).map(|v| v.to_string()); - - Ok(Self { - base_url, - group_id, - artifact_id, - version, - }) + type Err = MavenArtifactParseError; + + fn from_str(s: &str) -> Result { + match s.chars().filter(|c| *c == ':').count() { + 0 => return Err(MavenArtifactParseError::TooLittleSemiColons(0)), + 3 => (), + other => return Err(MavenArtifactParseError::TooLittleSemiColons(other)), + }; + + let parts = s.split_terminator(':').collect::>(); + + if parts.len() < 3 { + return Err(MavenArtifactParseError::NotEnoughComponents); } + + let base_url = format!( + "https://{}", + parts.first().ok_or(MavenArtifactParseError::Malformed)? + ); + let group_id = parts + .get(1) + .ok_or(MavenArtifactParseError::Malformed)? + .to_string(); + let artifact_id = parts + .get(2) + .ok_or(MavenArtifactParseError::Malformed)? + .to_string(); + let version = parts.get(3).map(|v| v.to_string()); + + Ok(Self { + base_url, + group_id, + artifact_id, + version, + }) + } } impl MavenArtifact { - pub fn set_version(&mut self, version: T) { - self.version = Some(version.to_string()); - } + pub fn set_version(&mut self, version: T) { + self.version = Some(version.to_string()); + } } #[derive(Debug, Error)] pub enum MavenArtifactBuildError { - #[error("missing field: base_url. run with_base_url to fix")] - BaseURL, - #[error("missing field: group_id. run with_group_id to fix")] - GroupID, - #[error("missing field: artifact_id. run with_artifact_id to fix")] - ArtifactID, + #[error("missing field: base_url. run with_base_url to fix")] + BaseURL, + #[error("missing field: group_id. run with_group_id to fix")] + GroupID, + #[error("missing field: artifact_id. run with_artifact_id to fix")] + ArtifactID, } impl MavenArtifactBuilder { - pub fn with_base_url(mut self, base_url: T) -> Self { - self.base_url = Some(base_url); - - self - } - - pub fn with_group_id(mut self, group_id: T) -> Self { - self.group_id = Some(group_id); - - self - } - - pub fn with_artifact_id(mut self, artifact_id: T) -> Self { - self.artifact_id = Some(artifact_id); - - self - } - - pub fn with_version(mut self, version: T) -> Self { - self.version = Some(version); - - self - } - - pub fn build(self) -> Result { - let base_url = self - .base_url - .ok_or(MavenArtifactBuildError::BaseURL) - .map(|base_url| base_url.to_string())?; - let group_id = self - .group_id - .ok_or(MavenArtifactBuildError::GroupID) - .map(|group_id| group_id.to_string())?; - let artifact_id = self - .artifact_id - .ok_or(MavenArtifactBuildError::ArtifactID) - .map(|artifact_id| artifact_id.to_string())?; - let version = self.version.map(|version| version.to_string()); - - Ok(MavenArtifact { - base_url, - group_id, - artifact_id, - version, - }) - } + pub fn with_base_url(mut self, base_url: T) -> Self { + self.base_url = Some(base_url); + + self + } + + pub fn with_group_id(mut self, group_id: T) -> Self { + self.group_id = Some(group_id); + + self + } + + pub fn with_artifact_id(mut self, artifact_id: T) -> Self { + self.artifact_id = Some(artifact_id); + + self + } + + pub fn with_version(mut self, version: T) -> Self { + self.version = Some(version); + + self + } + + pub fn build(self) -> Result { + let base_url = self + .base_url + .ok_or(MavenArtifactBuildError::BaseURL) + .map(|base_url| base_url.to_string())?; + let group_id = self + .group_id + .ok_or(MavenArtifactBuildError::GroupID) + .map(|group_id| group_id.to_string())?; + let artifact_id = self + .artifact_id + .ok_or(MavenArtifactBuildError::ArtifactID) + .map(|artifact_id| artifact_id.to_string())?; + let version = self.version.map(|version| version.to_string()); + + Ok(MavenArtifact { + base_url, + group_id, + artifact_id, + version, + }) + } } #[cfg(test)] mod tests { - use super::*; - - #[test] - fn test_artifact_parsing() { - // Assert that all of these are valid artifact strings - assert!([ - "maven.minecraftforge.net:net.minecraftforge:forge:", - "maven.neoforged.net/releases:net.neoforged:neoforge:", - "maven.fabricmc.net:net.fabricmc:fabric-installer:", - "maven.quiltmc.org/repository/release:org.quiltm:quilt-installer:", - "repo.glowstone.net/content/repositories/snapshots:net.glowstone:glowstone:", - ] - .iter() - .map(|artifact| artifact.parse::()) - .all(|artifact: Result<_, _>| artifact.is_ok())); - } + use super::*; + + #[test] + fn test_artifact_parsing() { + // Assert that all of these are valid artifact strings + assert!([ + "maven.minecraftforge.net:net.minecraftforge:forge:", + "maven.neoforged.net/releases:net.neoforged:neoforge:", + "maven.fabricmc.net:net.fabricmc:fabric-installer:", + "maven.quiltmc.org/repository/release:org.quiltm:quilt-installer:", + "repo.glowstone.net/content/repositories/snapshots:net.glowstone:glowstone:", + ] + .iter() + .map(|artifact| artifact.parse::()) + .all(|artifact: Result<_, _>| artifact.is_ok())); + } } diff --git a/mar/src/types/deserialize.rs b/mar/src/types/deserialize.rs index d31690a..b70dc71 100644 --- a/mar/src/types/deserialize.rs +++ b/mar/src/types/deserialize.rs @@ -7,42 +7,42 @@ use std::sync::Arc; #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MavenArtifactVersions { - pub group_id: Arc, - pub artifact_id: Arc, - pub versioning: MavenArtifactVersionVersioning, + pub group_id: Arc, + pub artifact_id: Arc, + pub versioning: MavenArtifactVersionVersioning, } #[cfg(feature = "types")] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MavenArtifactVersionVersioning { - release: Arc, - latest: Arc, - last_updated: u64, - versions: MAVersioningVersions, + release: Arc, + latest: Arc, + last_updated: u64, + versions: MAVersioningVersions, } #[cfg(feature = "types")] impl MavenArtifactVersionVersioning { - pub fn release(&self) -> &str { - &self.release - } + pub fn release(&self) -> &str { + &self.release + } - pub fn latest(&self) -> &str { - &self.latest - } + pub fn latest(&self) -> &str { + &self.latest + } - pub fn last_updated(&self) -> u64 { - self.last_updated - } + pub fn last_updated(&self) -> u64 { + self.last_updated + } - pub fn versions(&self) -> Arc<[Arc]> { - self.versions.version.clone() - } + pub fn versions(&self) -> Arc<[Arc]> { + self.versions.version.clone() + } } #[cfg(feature = "types")] #[derive(Debug, Deserialize)] pub struct MAVersioningVersions { - version: Arc<[Arc]>, + version: Arc<[Arc]>, } diff --git a/modparser/Cargo.toml b/modparser/Cargo.toml index ce418f2..8ab23a7 100644 --- a/modparser/Cargo.toml +++ b/modparser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "modparser" -version = "1.0.0" +version = "1.1.0" edition = "2021" description = "Library to parse minecraft mod (*.jar) files" license = "GPL-v3" diff --git a/modparser/src/types/fabric.rs b/modparser/src/types/fabric.rs index 871e917..0c54e32 100644 --- a/modparser/src/types/fabric.rs +++ b/modparser/src/types/fabric.rs @@ -5,131 +5,131 @@ use std::str::FromStr; #[derive(Debug, Deserialize)] pub struct FabricMod { - #[serde(rename = "schemaVersion")] - _schema_version: u8, - #[serde(rename = "entrypoints")] - _entrypoints: Option, Vec>>>, - #[serde(rename = "accessWidener")] - _access_widener: Option>, - #[serde(rename = "jars")] - _jars: Option, Rc>>>, - - #[serde(rename = "id")] - pub mod_id: Rc, - pub icon: Rc, - #[serde(rename = "version")] - pub mod_version: Rc, - pub name: Option>, - pub description: Option>, - pub authors: Option>>, - pub contributors: Option>>, - pub contact: Option, - pub license: Option>, - #[serde(rename = "depends")] - pub dependencies: Option, FabricDependencyVersion>>, - pub recommends: Option, FabricDependencyVersion>>, - pub conflicts: Option, FabricDependencyVersion>>, - pub breaks: Option, FabricDependencyVersion>>, + #[serde(rename = "schemaVersion")] + _schema_version: u8, + #[serde(rename = "entrypoints")] + _entrypoints: Option, Vec>>>, + #[serde(rename = "accessWidener")] + _access_widener: Option>, + #[serde(rename = "jars")] + _jars: Option, Rc>>>, + + #[serde(rename = "id")] + pub mod_id: Rc, + pub icon: Rc, + #[serde(rename = "version")] + pub mod_version: Rc, + pub name: Option>, + pub description: Option>, + pub authors: Option>>, + pub contributors: Option>>, + pub contact: Option, + pub license: Option>, + #[serde(rename = "depends")] + pub dependencies: Option, FabricDependencyVersion>>, + pub recommends: Option, FabricDependencyVersion>>, + pub conflicts: Option, FabricDependencyVersion>>, + pub breaks: Option, FabricDependencyVersion>>, } #[derive(Debug, Deserialize)] pub struct FabricModContact { - pub homepage: Option>, - pub issues: Option>, - pub sources: Option>, - pub email: Option>, - pub irc: Option>, + pub homepage: Option>, + pub issues: Option>, + pub sources: Option>, + pub email: Option>, + pub irc: Option>, } #[derive(Debug, Deserialize)] pub enum FabricDependencyVersionMode { - Any, - ExactMatch, - SameMinor, - SameMajor, - GreaterThan, - LesserThan, - GreaterThanEqual, - LesserThanEqual, + Any, + ExactMatch, + SameMinor, + SameMajor, + GreaterThan, + LesserThan, + GreaterThanEqual, + LesserThanEqual, } #[derive(Debug)] pub struct FabricDependencyVersion { - pub mode: FabricDependencyVersionMode, - pub version: Rc, + pub mode: FabricDependencyVersionMode, + pub version: Rc, } impl<'de> Deserialize<'de> for FabricDependencyVersion { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let mod_data = String::deserialize(deserializer)?; - mod_data.parse().map_err(serde::de::Error::custom) - } + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let mod_data = String::deserialize(deserializer)?; + mod_data.parse().map_err(serde::de::Error::custom) + } } impl FabricDependencyVersion { - fn check_equals(s: &str) -> bool { - // For some reason, there is a chance for - // a "*" to end up in here, so we gotta check - // for out-of-bounds edge cases and immidiately - // return false if we can't even begin to - // check - if s.len() < 2 { - false - } else { - s.chars().nth(1).unwrap() == '=' - } + fn check_equals(s: &str) -> bool { + // For some reason, there is a chance for + // a "*" to end up in here, so we gotta check + // for out-of-bounds edge cases and immidiately + // return false if we can't even begin to + // check + if s.len() < 2 { + false + } else { + s.chars().nth(1).unwrap() == '=' } + } } impl FromStr for FabricDependencyVersion { - type Err = String; - - fn from_str(s: &str) -> Result { - #[allow(clippy::wildcard_in_or_patterns)] - let mode = match s.chars().next().unwrap() { - any_char if any_char.is_numeric() => FabricDependencyVersionMode::ExactMatch, - '>' if Self::check_equals(s) => FabricDependencyVersionMode::GreaterThanEqual, - '<' if Self::check_equals(s) => FabricDependencyVersionMode::LesserThanEqual, - '>' => FabricDependencyVersionMode::GreaterThan, - '<' => FabricDependencyVersionMode::LesserThan, - '^' => FabricDependencyVersionMode::SameMajor, - '~' => FabricDependencyVersionMode::SameMinor, - '*' | _ => FabricDependencyVersionMode::Any, - }; - - let version_start = if Self::check_equals(s) { 2 } else { 1 }; - let version = Rc::from(s[version_start..].to_string().into_boxed_str()); - - Ok(Self { mode, version }) - } + type Err = String; + + fn from_str(s: &str) -> Result { + #[expect(clippy::wildcard_in_or_patterns)] + let mode = match s.chars().next().unwrap() { + any_char if any_char.is_numeric() => FabricDependencyVersionMode::ExactMatch, + '>' if Self::check_equals(s) => FabricDependencyVersionMode::GreaterThanEqual, + '<' if Self::check_equals(s) => FabricDependencyVersionMode::LesserThanEqual, + '>' => FabricDependencyVersionMode::GreaterThan, + '<' => FabricDependencyVersionMode::LesserThan, + '^' => FabricDependencyVersionMode::SameMajor, + '~' => FabricDependencyVersionMode::SameMinor, + '*' | _ => FabricDependencyVersionMode::Any, + }; + + let version_start = if Self::check_equals(s) { 2 } else { 1 }; + let version = Rc::from(s[version_start..].to_string().into_boxed_str()); + + Ok(Self { mode, version }) + } } #[cfg(test)] mod tests { - use super::*; - use crate::unzip::grab_meta_file; - use serde_json::from_str; - use std::fs::read_dir; - - #[test] - fn mod_manifest() { - for file in read_dir("samples/fabric/").unwrap() { - let file = file.unwrap(); - - if file.file_type().unwrap().is_dir() { - continue; - } - - let mod_meta = from_str::( - &grab_meta_file(file.path()) - .expect("expected meta file to be grabbed") - .raw, - ); - - assert!(mod_meta.is_ok()); - } + use super::*; + use crate::unzip::grab_meta_file; + use serde_json::from_str; + use std::fs::read_dir; + + #[test] + fn mod_manifest() { + for file in read_dir("samples/fabric/").unwrap() { + let file = file.unwrap(); + + if file.file_type().unwrap().is_dir() { + continue; + } + + let mod_meta = from_str::( + &grab_meta_file(file.path()) + .expect("expected meta file to be grabbed") + .raw, + ); + + assert!(mod_meta.is_ok()); } + } } diff --git a/modparser/src/types/forge.rs b/modparser/src/types/forge.rs index 7fe9a72..d5b3659 100644 --- a/modparser/src/types/forge.rs +++ b/modparser/src/types/forge.rs @@ -1,511 +1,713 @@ -use std::collections::HashMap; -use std::path::PathBuf; -use std::rc::Rc; -use std::str::FromStr; - -use serde::de::{Deserializer, MapAccess, Visitor}; -use serde::Deserialize; -use thiserror::Error; - -#[derive(Deserialize, Debug)] -pub struct ForgeMod { - #[serde(rename = "modLoader")] - pub mod_loader: Rc, - #[serde(rename = "loaderVersion")] - pub loader_version: ForgeModVersion, - pub license: Rc, - #[serde(rename = "issueTrackerURL")] - pub issue_tracker: Option>, - #[serde(rename = "displayURL")] - pub homepage_url: Option>, - pub mods: Rc<[ForgeModMetadata]>, - pub dependencies: Option, Rc<[ForgeModDependency]>>>, -} - -#[derive(Deserialize, Debug)] -pub struct ForgeModMetadata { - #[serde(rename = "modId")] - pub id: Rc, - pub version: Rc, - #[serde(rename = "displayName")] - pub display_name: Rc, - pub authors: Option, - pub credits: Option>, - pub description: Rc, - #[serde(rename = "updateJSONURL")] - pub update_url: Option>, - #[serde(rename = "displayURL")] - pub homepage_url: Option>, - #[serde(rename = "logoFile")] - pub logo: Option, -} - -#[derive(Deserialize, Debug)] -pub struct ForgeModDependency { - #[serde(rename = "modId")] - pub id: Rc, - // pub version: ModVersion, - pub mandatory: bool, - #[serde(rename = "versionRange")] - pub version_range: ForgeModVersion, - pub ordering: Option>, - pub side: Rc, -} - -#[derive(Debug, PartialEq, Eq)] -pub struct ModSemver { - pub major: Option, - pub minor: Option, - pub patch: Option, -} - -#[derive(Debug)] -pub struct ModVersionRange { - pub from: ModSemver, - pub to: Option, - pub mode: ModVersionRangeMode, -} - -#[derive(Debug, Deserialize)] -#[serde(untagged)] -pub enum ForgeModAuthors { - SingleAuthor(String), - MultipleAuthors(Vec), -} - -#[derive(Debug)] -pub enum ForgeModVersion { - Any, - VersionRange(ModVersionRange), - SpecificVersion(ModSemver), -} - -impl<'de> Deserialize<'de> for ForgeModVersion { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct ModLoeaderVersionVisitor; - - impl<'de> Visitor<'de> for ModLoeaderVersionVisitor { - type Value = ForgeModVersion; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("valid string or struct representing ModLoeaderVersion variant") - } - - fn visit_str(self, value: &str) -> Result - where - E: serde::de::Error, - { - match value { - "*" => Ok(ForgeModVersion::Any), - version if version.chars().nth(0).unwrap().is_numeric() => { - Ok(ForgeModVersion::SpecificVersion( - version.parse().map_err(serde::de::Error::custom)?, - )) - } - version => Ok(if version.starts_with('[') { - ForgeModVersion::VersionRange( - version.parse().map_err(serde::de::Error::custom)?, - ) - } else { - ForgeModVersion::SpecificVersion( - version.parse().map_err(serde::de::Error::custom)?, - ) - }), - } - } - - fn visit_map(self, mut access: A) -> Result - where - A: MapAccess<'de>, - { - let my_struct = ModVersionRange::deserialize( - serde::de::value::MapAccessDeserializer::new(&mut access), - )?; - Ok(ForgeModVersion::VersionRange(my_struct)) - } - } - - deserializer.deserialize_any(ModLoeaderVersionVisitor) - } -} - -impl<'de> Deserialize<'de> for ModSemver { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let mod_data = String::deserialize(deserializer)?; - mod_data.parse().map_err(serde::de::Error::custom) - } -} - -impl FromStr for ModSemver { - type Err = ModVersionParseError; - - fn from_str(s: &str) -> Result { - let a = s - .split('.') - .map(|component| { - component - .parse::() - .map_err(ModVersionParseError::Parse) - // .map(|num| if num == 0 { None } else { Some(num) }) - }) - .collect::>>(); - - Ok(ModSemver { - major: a.first().and_then(|some_case| some_case.to_owned().ok()), - minor: a.get(1).and_then(|some_case| some_case.to_owned().ok()), - patch: a.get(2).and_then(|some_case| some_case.to_owned().ok()), - }) - } -} - -#[derive(Error, Debug, PartialEq, Eq, Clone)] -pub enum ModVersionParseError { - #[error("error while parsing version: {0}")] - Parse(#[from] std::num::ParseIntError), -} - -impl<'de> Deserialize<'de> for ModVersionRange { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let mod_data = String::deserialize(deserializer)?; - mod_data.parse().map_err(serde::de::Error::custom) - } -} - -impl ModVersionRange { - fn is_infinity(version: &Option) -> bool { - if let Some(max_version) = version { - max_version.major.is_none() - && max_version.minor.is_none() - && max_version.patch.is_none() - } else { - true - } - } -} - -impl FromStr for ModVersionRange { - type Err = ModVersionRangeParseError; - - fn from_str(s: &str) -> Result { - let delimeter_loc = s.find(','); - let closing_loc = s.find(']').or(s.find(')')); - let mut is_strict_version = false; - - if delimeter_loc.is_none() && closing_loc.is_none() { - // we assume that we will find a comma somewhere - return Err(ModVersionRangeParseError::Malformed(s.to_string())); - } else if delimeter_loc.is_none() && closing_loc.is_some() { - // we assume that we are given a 'strict version requirement' - // e.g. STRICTLY 1.19.2 and no other version - is_strict_version = true; - } else if delimeter_loc.unwrap() == 1 { - // we assume that we will find a minimum version at the beginning - return Err(ModVersionRangeParseError::NoMinimum); - } - - if closing_loc.is_none() { - // we assume that we will find a closing `]` or `)` somewhere - return Err(ModVersionRangeParseError::Unclosed); - } - - let delimeter_loc = delimeter_loc.unwrap_or(closing_loc.unwrap()); - let strlen = s.len(); - - let ver_min = s[1..delimeter_loc].parse::(); - let ver_max = if is_strict_version { - None - } else { - s[delimeter_loc + 1..strlen - 1].parse::().ok() - }; - let mode = if is_strict_version { - ModVersionRangeMode::None - } else if Self::is_infinity(&ver_max) { - ModVersionRangeMode::GreaterThan - } else { - match s.chars().nth(closing_loc.unwrap()).unwrap() { - ')' => ModVersionRangeMode::Between, - ']' => ModVersionRangeMode::BetweenInclusive, - _ => ModVersionRangeMode::None, - } - }; - - Ok(ModVersionRange { - from: ver_min?, - to: ver_max, - mode, - }) - } -} - -#[derive(Error, Debug, PartialEq, Eq)] -pub enum ModVersionRangeParseError { - #[error("string {0:?} is malformed")] - Malformed(String), - - #[error("string does not supply a minimum version")] - NoMinimum, - - #[error("expected `]` or `)` from string, found none")] - Unclosed, - - #[error("unable to parse mod version: {0}")] - Parse(#[from] ModVersionParseError), -} - -#[derive(Debug, PartialEq, Eq)] -pub enum ModVersionRangeMode { - // "any version greater than or equal to a" - GreaterThan, - // // "any version lesser than or equal to a" - // LesserThan, - - // TODO: Give this a better name - // "any version between a and b, including a and b" - BetweenInclusive, - - // "any version between a and b, including a but excluding b" - Between, - - // specifically version a - None, -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::unzip::grab_meta_file; - use std::fs::read_dir; - use toml::from_str; - - #[test] - fn mod_version() { - let version_raw = "1.2.3"; - - let version = version_raw.parse::(); - assert!(version.is_ok()); - - let version_parsed = version.unwrap(); - - assert_eq!(version_parsed.major, Some(1)); - assert_eq!(version_parsed.minor, Some(2)); - assert_eq!(version_parsed.patch, Some(3)); - } - - #[test] - fn mod_version_major() { - let version_raw = "1"; - - let version = version_raw.parse::(); - assert!(version.is_ok()); - - let version_parsed = version.unwrap(); - - assert_eq!(version_parsed.major, Some(1)); - assert_eq!(version_parsed.minor, None); - assert_eq!(version_parsed.patch, None); - } - - #[test] - fn mod_version_major_minor() { - let version_raw = "1.2"; - - let version = version_raw.parse::(); - assert!(version.is_ok()); - - let version_parsed = version.unwrap(); - - assert_eq!(version_parsed.major, Some(1)); - assert_eq!(version_parsed.minor, Some(2)); - assert_eq!(version_parsed.patch, None); - } - - #[test] - fn mod_version_major_minor_patch() { - let version_raw = "1.2.3"; - - let version = version_raw.parse::(); - assert!(version.is_ok()); - - let version_parsed = version.unwrap(); - - assert_eq!(version_parsed.major, Some(1)); - assert_eq!(version_parsed.minor, Some(2)); - assert_eq!(version_parsed.patch, Some(3)); - } - - #[test] - fn version_range_unclosed() { - let version = "[1.2.3,"; - - let mod_version = version.parse::(); - assert!(mod_version.is_err()); - assert_eq!( - mod_version.unwrap_err(), - ModVersionRangeParseError::Unclosed - ) - } - - #[test] - fn version_range_malformed() { - let version = "[1.2.3"; - - let mod_version = version.parse::(); - assert!(mod_version.is_err()); - assert_eq!( - mod_version.unwrap_err(), - ModVersionRangeParseError::Malformed("[1.2.3".to_string()) - ) - } - - #[test] - fn version_range_no_minimum() { - let version = "[,)"; - - let mod_version = version.parse::(); - assert!(mod_version.is_err()); - assert_eq!( - mod_version.unwrap_err(), - ModVersionRangeParseError::NoMinimum - ) - } - - #[test] - fn version_range_shouldnt_fail() { - let version = "[1.2.3,4.5.6)"; - - let mod_version = version.parse::(); - assert!(mod_version.is_ok()); - - let mod_version = mod_version.unwrap(); - - assert_eq!( - mod_version.from, - ModSemver { - major: Some(1), - minor: Some(2), - patch: Some(3) - } - ); - - assert_eq!( - mod_version.to, - Some(ModSemver { - major: Some(4), - minor: Some(5), - patch: Some(6) - }) - ); - - assert_eq!(mod_version.mode, ModVersionRangeMode::Between) - } - - #[test] - fn version_range_shouldnt_fail_inclusive() { - let version = "[1.2.3,4.5.6]"; - - let mod_version = version.parse::(); - assert!(mod_version.is_ok()); - - let mod_version = mod_version.unwrap(); - - assert_eq!( - mod_version.from, - ModSemver { - major: Some(1), - minor: Some(2), - patch: Some(3) - } - ); - - assert_eq!( - mod_version.to, - Some(ModSemver { - major: Some(4), - minor: Some(5), - patch: Some(6) - }) - ); - - assert_eq!(mod_version.mode, ModVersionRangeMode::BetweenInclusive) - } - - #[test] - fn version_range_shouldnt_fail_single_inclusive() { - let version = "[1.2.3]"; - - let mod_version = version.parse::(); - assert!(mod_version.is_ok()); - - let mod_version = mod_version.unwrap(); - - assert_eq!( - mod_version.from, - ModSemver { - major: Some(1), - minor: Some(2), - patch: Some(3) - } - ); - - assert_eq!(mod_version.to, None); - assert_eq!(mod_version.mode, ModVersionRangeMode::None) - } - - #[test] - fn version_range_shouldnt_fail_greater() { - let version = "[1.2.3,)"; - - let mod_version = version.parse::(); - assert!(mod_version.is_ok()); - - let mod_version = mod_version.unwrap(); - - assert_eq!( - mod_version.from, - ModSemver { - major: Some(1), - minor: Some(2), - patch: Some(3) - } - ); - - assert_eq!( - mod_version.to, - Some(ModSemver { - major: None, - minor: None, - patch: None - }) - ); - - assert_eq!(mod_version.mode, ModVersionRangeMode::GreaterThan) - } - - #[test] - fn mod_manifest() { - for file in read_dir("samples/forge/").unwrap() { - let file = file.unwrap(); - - if file.file_type().unwrap().is_dir() { - continue; - } - - let mod_meta = from_str::( - &grab_meta_file(file.path()) - .expect("expected meta file to be grabbed") - .raw, - ); - - assert!(mod_meta.is_ok()); - } - } -} +use std::collections::HashMap; +use std::path::PathBuf; +use std::rc::Rc; +use std::str::FromStr; + +use serde::de::{Deserializer, MapAccess, Visitor}; +use serde::Deserialize; +use thiserror::Error; + +#[derive(Deserialize, Debug)] +pub struct ForgeMod { + #[serde(rename = "modLoader")] + pub mod_loader: Rc, + #[serde(rename = "loaderVersion")] + pub loader_version: ForgeModVersion, + pub license: Rc, + #[serde(rename = "issueTrackerURL")] + pub issue_tracker: Option>, + // #[serde(rename = "displayURL")] + // pub homepage_url: Option>, + pub mods: Rc<[ForgeModMetadata]>, + pub dependencies: Option, Rc<[ForgeModDependency]>>>, +} + +#[derive(Deserialize, Debug)] +pub struct ForgeModMetadata { + #[serde(rename = "modId")] + pub id: Rc, + pub version: ModSemver, + #[serde(rename = "displayName")] + pub display_name: Rc, + pub authors: Option, + pub credits: Option>, + pub description: Rc, + #[serde(rename = "updateJSONURL")] + pub update_url: Option>, + #[serde(rename = "displayURL")] + pub homepage_url: Option>, + #[serde(rename = "logoFile")] + pub logo: Option, +} + +#[derive(Deserialize, Debug)] +pub struct ForgeModDependency { + #[serde(rename = "modId")] + pub id: Rc, + // pub version: ModVersion, + pub mandatory: bool, + #[serde(rename = "versionRange")] + pub version_range: ForgeModVersion, + // pub ordering: Option>, + // pub side: Option>, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct ModSemver { + pub major: Option, + pub minor: Option, + pub patch: Option, +} + +#[derive(Debug)] +pub struct ModVersionRange { + pub from: ModSemver, + pub to: Option, + pub mode: ModVersionRangeMode, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum ForgeModAuthors { + SingleAuthor(String), + MultipleAuthors(Vec), +} + +#[derive(Debug)] +pub enum ForgeModVersion { + Any, + Custom(Rc), + VersionRange(ModVersionRange), + SpecificVersion(ModSemver), +} + +impl ModVersionRange { + fn within_range(&self, other: &ModSemver) -> bool { + match self.mode { + ModVersionRangeMode::None => { + panic!("this is a weird case. `ModVersionRange.mode` is `None`!") + } + ModVersionRangeMode::Between => { + self.from < *other + && if let Some(to) = &self.to { + other < to + } else { + true + } + } + ModVersionRangeMode::BetweenInclusive => { + self.from <= *other + && if let Some(to) = &self.to { + other <= to + } else { + true + } + } + ModVersionRangeMode::GreaterThan => self.from < *other, + } + } +} + +impl ForgeModVersion { + pub fn satisfies(&self, version: &ModSemver) -> bool { + match &self { + ForgeModVersion::Any => true, + ForgeModVersion::Custom(version) => !version.is_empty(), + ForgeModVersion::SpecificVersion(this_version) => this_version == version, + ForgeModVersion::VersionRange(range) => range.within_range(version), + } + } +} + +impl<'de> Deserialize<'de> for ForgeModVersion { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct ModLoaderVersionVisitor; + + impl<'de> Visitor<'de> for ModLoaderVersionVisitor { + type Value = ForgeModVersion; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("valid string or struct representing ModLoaderVersion variant") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + if value == "*" { + return Ok(ForgeModVersion::Any); + } + + if value.starts_with('[') { + return Ok(ForgeModVersion::VersionRange( + value.parse().map_err(serde::de::Error::custom)?, + )); + } + + Ok(if let Ok(parsed_value) = value.parse() { + ForgeModVersion::SpecificVersion(parsed_value) + } else { + ForgeModVersion::Custom(Rc::from(value)) + }) + + // match value { + // "*" => Ok(ForgeModVersion::Any), + // version + // if version + // .chars() + // .nth(0) + // .ok_or(serde::de::Error::custom( + // "expected version string to not be empty (version.char().nth(0) returned None)", + // ))? + // .is_numeric() => + // { + // Ok(ForgeModVersion::SpecificVersion( + // version.parse().map_err(serde::de::Error::custom)?, + // )) + // } + // version => Ok(if version.starts_with('[') { + // ForgeModVersion::VersionRange(version.parse().map_err(serde::de::Error::custom)?) + // } else { + // ForgeModVersion::SpecificVersion(version.parse().map_err(serde::de::Error::custom)?) + // }), + // } + } + + fn visit_map(self, mut access: A) -> Result + where + A: MapAccess<'de>, + { + let my_struct = + ModVersionRange::deserialize(serde::de::value::MapAccessDeserializer::new(&mut access))?; + Ok(ForgeModVersion::VersionRange(my_struct)) + } + } + + deserializer.deserialize_any(ModLoaderVersionVisitor) + } +} + +impl<'de> Deserialize<'de> for ModSemver { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let mut mod_data = String::deserialize(deserializer)?; + + assert!(!mod_data.is_empty()); + let processed_mod_data = mod_data + .contains("-") + .then(|| { + let sep_char = mod_data + .find("-") + .expect("expected a `-` to exist since this passed"); + + mod_data.split_off(sep_char) + }) + .unwrap_or(mod_data); + + processed_mod_data.parse().map_err(serde::de::Error::custom) + } +} + +impl FromStr for ModSemver { + type Err = ModVersionParseError; + + fn from_str(s: &str) -> Result { + let a = s + .split('.') + .map(|component| { + component + .parse::() + .map_err(ModVersionParseError::Parse) + }) + .collect::>(); + + Ok(ModSemver { + major: a.first().map_or(Ok(None), |res| match res { + Ok(num) => Ok(Some(*num)), + Err(err) => Err(err.clone()), + })?, + + minor: a.get(1).map_or(Ok(None), |res| match res { + Ok(num) => Ok(Some(*num)), + Err(err) => Err(err.clone()), + })?, + + patch: a.get(2).map_or(Ok(None), |res| match res { + Ok(num) => Ok(Some(*num)), + Err(err) => Err(err.clone()), + })?, + }) + } +} + +impl Ord for ModSemver { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self + .major + .unwrap_or_default() + .cmp(&other.major.unwrap_or_default()) + .then( + self + .minor + .unwrap_or_default() + .cmp(&other.minor.unwrap_or_default()), + ) + .then( + self + .patch + .unwrap_or_default() + .cmp(&other.patch.unwrap_or_default()), + ) + } +} + +impl PartialOrd for ModSemver { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +#[derive(Error, Debug, PartialEq, Eq, Clone)] +pub enum ModVersionParseError { + #[error("error while parsing version: {0}")] + Parse(#[from] std::num::ParseIntError), +} + +impl<'de> Deserialize<'de> for ModVersionRange { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let mod_data = String::deserialize(deserializer)?; + mod_data.parse().map_err(serde::de::Error::custom) + } +} + +impl ModVersionRange { + fn is_infinity(version: &Option) -> bool { + if let Some(max_version) = version { + max_version.major.is_none() && max_version.minor.is_none() && max_version.patch.is_none() + } else { + true + } + } +} + +impl FromStr for ModVersionRange { + type Err = ModVersionRangeParseError; + + fn from_str(s: &str) -> Result { + let delimiter_loc = s.find(','); + let closing_loc = s.find(']').or(s.find(')')); + let mut is_strict_version = false; + + if delimiter_loc.is_none() && closing_loc.is_none() { + // we assume that we will find a comma somewhere + return Err(ModVersionRangeParseError::Malformed(s.to_string())); + } else if delimiter_loc.is_none() && closing_loc.is_some() { + // we assume that we are given a 'strict version requirement' + // e.g. STRICTLY 1.19.2 and no other version + is_strict_version = true; + } else if delimiter_loc.unwrap() == 1 { + // we assume that we will find a minimum version at the beginning + return Err(ModVersionRangeParseError::NoMinimum); + } + + if closing_loc.is_none() { + // we assume that we will find a closing `]` or `)` somewhere + return Err(ModVersionRangeParseError::Unclosed); + } + + let delimiter_loc = delimiter_loc.unwrap_or(closing_loc.unwrap()); + let strlen = s.len(); + + let ver_min = s[1..delimiter_loc].parse::(); + let ver_max = if is_strict_version { + None + } else { + s[delimiter_loc + 1..strlen - 1].parse::().ok() + }; + let mode = if is_strict_version { + ModVersionRangeMode::None + } else if Self::is_infinity(&ver_max) { + ModVersionRangeMode::GreaterThan + } else { + match s.chars().nth(closing_loc.unwrap()).unwrap() { + ')' => ModVersionRangeMode::Between, + ']' => ModVersionRangeMode::BetweenInclusive, + _ => ModVersionRangeMode::None, + } + }; + + Ok(ModVersionRange { + from: ver_min?, + to: ver_max, + mode, + }) + } +} + +#[derive(Error, Debug, PartialEq, Eq)] +pub enum ModVersionRangeParseError { + #[error("string {0:?} is malformed")] + Malformed(String), + + #[error("string does not supply a minimum version")] + NoMinimum, + + #[error("expected `]` or `)` from string, found none")] + Unclosed, + + #[error("unable to parse mod version: {0}")] + Parse(#[from] ModVersionParseError), +} + +#[derive(Debug, PartialEq, Eq)] +pub enum ModVersionRangeMode { + // "any version greater than or equal to a" + GreaterThan, + // // "any version lesser than or equal to a" + // LesserThan, + + // TODO: Give this a better name + // "any version between a and b, including a and b" + BetweenInclusive, + + // "any version between a and b, including a but excluding b" + Between, + + // specifically version a + None, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn mod_version() { + let version_raw = "1.2.3"; + + let version = version_raw.parse::(); + assert!(version.is_ok()); + + let version_parsed = version.unwrap(); + + assert_eq!(version_parsed.major, Some(1)); + assert_eq!(version_parsed.minor, Some(2)); + assert_eq!(version_parsed.patch, Some(3)); + } + + #[test] + fn mod_version_major() { + let version_raw = "1"; + + let version = version_raw.parse::(); + assert!(version.is_ok()); + + let version_parsed = version.unwrap(); + + assert_eq!(version_parsed.major, Some(1)); + assert_eq!(version_parsed.minor, None); + assert_eq!(version_parsed.patch, None); + } + + #[test] + fn mod_version_major_minor() { + let version_raw = "1.2"; + + let version = version_raw.parse::(); + assert!(version.is_ok()); + + let version_parsed = version.unwrap(); + + assert_eq!(version_parsed.major, Some(1)); + assert_eq!(version_parsed.minor, Some(2)); + assert_eq!(version_parsed.patch, None); + } + + #[test] + fn mod_version_major_minor_patch() { + let version_raw = "1.2.3"; + + let version = version_raw.parse::(); + assert!(version.is_ok()); + + let version_parsed = version.unwrap(); + + assert_eq!(version_parsed.major, Some(1)); + assert_eq!(version_parsed.minor, Some(2)); + assert_eq!(version_parsed.patch, Some(3)); + } + + #[test] + fn version_range_unclosed() { + let version = "[1.2.3,"; + + let mod_version = version.parse::(); + assert!(mod_version.is_err()); + assert_eq!( + mod_version.unwrap_err(), + ModVersionRangeParseError::Unclosed + ) + } + + #[test] + fn version_range_malformed() { + let version = "[1.2.3"; + + let mod_version = version.parse::(); + assert!(mod_version.is_err()); + assert_eq!( + mod_version.unwrap_err(), + ModVersionRangeParseError::Malformed("[1.2.3".to_string()) + ) + } + + #[test] + fn version_range_no_minimum() { + let version = "[,)"; + + let mod_version = version.parse::(); + assert!(mod_version.is_err()); + assert_eq!( + mod_version.unwrap_err(), + ModVersionRangeParseError::NoMinimum + ) + } + + #[test] + fn version_range_shouldnt_fail() { + let version = "[1.2.3,4.5.6)"; + + let mod_version = version.parse::(); + assert!(mod_version.is_ok()); + + let mod_version = mod_version.unwrap(); + + assert_eq!( + mod_version.from, + ModSemver { + major: Some(1), + minor: Some(2), + patch: Some(3) + } + ); + + assert_eq!( + mod_version.to, + Some(ModSemver { + major: Some(4), + minor: Some(5), + patch: Some(6) + }) + ); + + assert_eq!(mod_version.mode, ModVersionRangeMode::Between) + } + + #[test] + fn version_range_shouldnt_fail_inclusive() { + let version = "[1.2.3,4.5.6]"; + + let mod_version = version.parse::(); + assert!(mod_version.is_ok()); + + let mod_version = mod_version.unwrap(); + + assert_eq!( + mod_version.from, + ModSemver { + major: Some(1), + minor: Some(2), + patch: Some(3) + } + ); + + assert_eq!( + mod_version.to, + Some(ModSemver { + major: Some(4), + minor: Some(5), + patch: Some(6) + }) + ); + + assert_eq!(mod_version.mode, ModVersionRangeMode::BetweenInclusive) + } + + #[test] + fn version_range_shouldnt_fail_single_inclusive() { + let version = "[1.2.3]"; + + let mod_version = version.parse::(); + assert!(mod_version.is_ok()); + + let mod_version = mod_version.unwrap(); + + assert_eq!( + mod_version.from, + ModSemver { + major: Some(1), + minor: Some(2), + patch: Some(3) + } + ); + + assert_eq!(mod_version.to, None); + assert_eq!(mod_version.mode, ModVersionRangeMode::None) + } + + #[test] + fn version_range_shouldnt_fail_greater() { + let version = "[1.2.3,)"; + + let mod_version = version.parse::(); + assert!(mod_version.is_ok()); + + let mod_version = mod_version.unwrap(); + + assert_eq!( + mod_version.from, + ModSemver { + major: Some(1), + minor: Some(2), + patch: Some(3) + } + ); + + assert_eq!(mod_version.to, None); + + assert_eq!(mod_version.mode, ModVersionRangeMode::GreaterThan) + } + + #[test] + fn version_range_satisfies_inclusive() { + let version = "[1.2.3,4.5.6]".parse::(); + let version_within = "4.5.6".parse::(); + let version_outside = "9.3.4".parse::(); + + assert!(version.is_ok()); + assert!(version_within.is_ok()); + assert!(version_outside.is_ok()); + + let version = version.unwrap(); + let version_within = version_within.unwrap(); + let version_outside = version_outside.unwrap(); + + assert!(version.within_range(&version_within)); + assert!(!version.within_range(&version_outside)); + } + + #[test] + fn version_range_satisfies_exclusive() { + let version = "[1.2.3,4.5.6)".parse::(); + let version_within = "4.5.5".parse::(); + let version_outside = "4.5.6".parse::(); + + assert!(version.is_ok()); + assert!(version_within.is_ok()); + assert!(version_outside.is_ok()); + + let version = version.unwrap(); + let version_within = version_within.unwrap(); + let version_outside = version_outside.unwrap(); + + assert!(version.within_range(&version_within)); + assert!(!version.within_range(&version_outside)); + } + + #[test] + fn version_range_satisfies_greater() { + let version = "[1.2.3,)".parse::(); + let version_within = "1.3.4".parse::(); + let version_outside = "1.2.2".parse::(); + + assert!(version.is_ok()); + assert!(version_within.is_ok()); + assert!(version_outside.is_ok()); + + let version = version.unwrap(); + let version_within = version_within.unwrap(); + let version_outside = version_outside.unwrap(); + + assert!(version.within_range(&version_within)); + assert!(!version.within_range(&version_outside)); + } + + #[test] + fn mod_sem_version_compare() { + let mod_a_version = "1.2.3".parse::(); + let mod_b_version = "1.2.4".parse::(); + + assert!(mod_a_version.is_ok()); + assert!(mod_b_version.is_ok()); + assert!(mod_b_version.unwrap() > mod_a_version.unwrap()); + } + + #[test] + fn test_manifest() { + let raw = r#"modLoader="lowcodefml" +loaderVersion="[1,)" +license="Stardust Labs License" +issueTrackerURL="https://github.com/Stardust-Labs-MC/Terralith/issues" + +[[mods]] + modId="terralith" + version="2.5.4" + displayName="Terralith" + displayURL="https://www.stardustlabs.net/" + logoFile="pack.png" + authors="Stardust Labs" + credits="Starmute: Owner. catter1: Maintainer. TheKingWhale: Builder. Apollounknowndev: Maintainer" + description="Explore almost 100 new biomes consisting of both realism and light fantasy, using just Vanilla blocks. Complete with several immersive structures to compliment the overhauled terrain." + +[[dependencies.terralith]] + modId="forge" + mandatory=false + versionRange="[46,)" + ordering="NONE" + side="BOTH" +[[dependencies.terralith]] + modId="neoforge" + mandatory=false + versionRange="[20.2,)" + ordering="NONE" + side="BOTH" +[[dependencies.terralith]] + modId="minecraft" + mandatory=true + versionRange="[1.20,1.21)" + ordering="NONE" + side="BOTH""#; + let parsed = toml::from_str(raw); + + assert!(parsed.is_ok()); + + let mod_manifest: ForgeMod = parsed.unwrap(); + + assert!(mod_manifest.mods.first().unwrap().homepage_url.is_some()); + assert!(mod_manifest.dependencies.is_some_and(|deps| { + if deps.is_empty() || !deps.contains_key("terralith") { + return false; + } + + let terralith_deps = deps + .get("terralith") + .expect("expected `terralith` to exist"); + + let minecraft_exists = terralith_deps + .iter() + .any(|dep| dep.id == "minecraft".into()); + let neoforge_exists = terralith_deps.iter().any(|dep| dep.id == "neoforge".into()); + let forge_exists = terralith_deps.iter().any(|dep| dep.id == "forge".into()); + + minecraft_exists && neoforge_exists && forge_exists + })) + } +} diff --git a/modparser/src/unzip.rs b/modparser/src/unzip.rs index f01a8d9..172422a 100644 --- a/modparser/src/unzip.rs +++ b/modparser/src/unzip.rs @@ -1,105 +1,105 @@ -use std::fs::File; -use std::io::prelude::*; -use std::path::Path; -use std::rc::Rc; - -use log::{debug, error, info}; -use thiserror::Error; -use zip::ZipArchive; - -const FORGE_META: &str = "META-INF/mods.toml"; -const FABRIC_META: &str = "fabric.mod.json"; - -pub struct ModMeta { - pub loader: ModLoader, - pub raw: Rc, -} - -pub enum ModLoader { - Forge, - Fabric, - None, -} - -#[derive(Error, Debug)] -pub enum UnzipError { - #[error("unable to open file: {0}")] - FileOpen(#[from] std::io::Error), - - #[error("error during reading the zip file: {0}")] - ZipRead(#[from] zip::result::ZipError), - - #[error("/META-INF/mods.toml file not found within mod")] - MetaFileNotFound, - - #[error("temporary file was not made")] - TempFileNotMade, - - #[error("write to temporary file was not made")] - WriteToTempFile, -} - -pub fn grab_meta_file>(file: F) -> Result { - let zipfile = File::open(file)?; - let mut archive = ZipArchive::new(zipfile)?; - - let (config_file, loader) = if archive.by_name(FORGE_META).is_ok() { - info!("Modpack manifest found at {}", FORGE_META); - (FORGE_META, ModLoader::Forge) - } else if archive.by_name(FABRIC_META).is_ok() { - info!("Modpack manifest found at {}", FABRIC_META); - (FABRIC_META, ModLoader::Fabric) - } else { - error!("No manifest found!"); - ("", ModLoader::None) - }; - - let mut file = archive - .by_name(config_file) - .or(Err(UnzipError::MetaFileNotFound))?; - - info!("Reading manifest file at {}", config_file); - let mut raw = String::new(); - let len = file.read_to_string(&mut raw)?; - debug!("Read {} bytes to buffer", len); - - Ok(ModMeta { - loader, - raw: Rc::from(raw.into_boxed_str()), - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn meta_get_forge() { - let file = "samples/forge/architectury-6.6.92-forge.jar"; - let res = grab_meta_file(file); - - assert!(res.is_ok()); - assert!(!res.unwrap().raw.is_empty()); - } - - #[test] - fn meta_get_fabric() { - let file = "samples/fabric/antique-atlas-2.5.0+1.20.jar"; - let res = grab_meta_file(file); - - assert!(res.is_ok()); - assert!(!res.unwrap().raw.is_empty()); - } - - #[test] - fn meta_readable() { - let forge_mod = grab_meta_file("samples/forge/architectury-6.6.92-forge.jar"); - let fabric_mod = grab_meta_file("samples/fabric/antique-atlas-2.5.0+1.20.jar"); - - assert!(forge_mod.is_ok()); - assert!(!forge_mod.unwrap().raw.is_empty()); - - assert!(fabric_mod.is_ok()); - assert!(!fabric_mod.unwrap().raw.is_empty()); - } -} +use std::fs::File; +use std::io::prelude::*; +use std::path::Path; +use std::rc::Rc; + +use log::{debug, error, info}; +use thiserror::Error; +use zip::ZipArchive; + +const FORGE_META: &str = "META-INF/mods.toml"; +const FABRIC_META: &str = "fabric.mod.json"; + +pub struct ModMeta { + pub loader: ModLoader, + pub raw: Rc, +} + +pub enum ModLoader { + Forge, + Fabric, + None, +} + +#[derive(Error, Debug)] +pub enum UnzipError { + #[error("unable to open file: {0}")] + FileOpen(#[from] std::io::Error), + + #[error("error during reading the zip file: {0}")] + ZipRead(#[from] zip::result::ZipError), + + #[error("/META-INF/mods.toml file not found within mod")] + MetaFileNotFound, + + #[error("temporary file was not made")] + TempFileNotMade, + + #[error("write to temporary file was not made")] + WriteToTempFile, +} + +pub fn grab_meta_file>(file: F) -> Result { + let zipfile = File::open(file)?; + let mut archive = ZipArchive::new(zipfile)?; + + let (config_file, loader) = if archive.by_name(FORGE_META).is_ok() { + info!("Modpack manifest found at {}", FORGE_META); + (FORGE_META, ModLoader::Forge) + } else if archive.by_name(FABRIC_META).is_ok() { + info!("Modpack manifest found at {}", FABRIC_META); + (FABRIC_META, ModLoader::Fabric) + } else { + error!("No manifest found!"); + ("", ModLoader::None) + }; + + let mut file = archive + .by_name(config_file) + .or(Err(UnzipError::MetaFileNotFound))?; + + info!("Reading manifest file at {}", config_file); + let mut raw = String::new(); + let len = file.read_to_string(&mut raw)?; + debug!("Read {} bytes to buffer", len); + + Ok(ModMeta { + loader, + raw: Rc::from(raw.into_boxed_str()), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn meta_get_forge() { + let file = "samples/forge/architectury-6.6.92-forge.jar"; + let res = grab_meta_file(file); + + assert!(res.is_ok()); + assert!(!res.unwrap().raw.is_empty()); + } + + #[test] + fn meta_get_fabric() { + let file = "samples/fabric/antique-atlas-2.5.0+1.20.jar"; + let res = grab_meta_file(file); + + assert!(res.is_ok()); + assert!(!res.unwrap().raw.is_empty()); + } + + #[test] + fn meta_readable() { + let forge_mod = grab_meta_file("samples/forge/architectury-6.6.92-forge.jar"); + let fabric_mod = grab_meta_file("samples/fabric/antique-atlas-2.5.0+1.20.jar"); + + assert!(forge_mod.is_ok()); + assert!(!forge_mod.unwrap().raw.is_empty()); + + assert!(fabric_mod.is_ok()); + assert!(!fabric_mod.unwrap().raw.is_empty()); + } +} diff --git a/modrinth/src/api/dependency.rs b/modrinth/src/api/dependency.rs index 55925ba..d477eaa 100644 --- a/modrinth/src/api/dependency.rs +++ b/modrinth/src/api/dependency.rs @@ -1,11 +1,11 @@ use super::APIError; use crate::{ - types::{ - query::VersionQuery, - version::{ResolvedVersionDependency, VersionDependency}, - }, - version::{get_version, get_versions}, - ModrinthProjectVersion, + types::{ + query::VersionQuery, + version::{ResolvedVersionDependency, VersionDependency}, + }, + version::{get_version, get_versions}, + ModrinthProjectVersion, }; use log::{debug, info, warn}; use reqwest::Client; @@ -52,125 +52,123 @@ use reqwest::Client; /// } /// ``` pub async fn resolve_dependencies( - client: &Client, - project: &mut ModrinthProjectVersion, - version_params: &VersionQuery, - resolver: F, + client: &Client, + project: &mut ModrinthProjectVersion, + version_params: &VersionQuery, + resolver: F, ) -> Result<(), APIError> where - F: Fn(Vec) -> ModrinthProjectVersion + Copy, + F: Fn(Vec) -> ModrinthProjectVersion + Copy, { - if project.dependencies.is_none() || project.dependencies.as_ref().is_some_and(|v| v.is_empty()) - { - return Err(APIError::NoDependencies); + if project.dependencies.is_none() || project.dependencies.as_ref().is_some_and(|v| v.is_empty()) { + return Err(APIError::NoDependencies); + } + + info!("Resolving dependencies for mod {}", project.name); + for dependency in project.dependencies.as_mut().unwrap().iter_mut() { + let unresolved_dependency = match dependency { + VersionDependency::Resolved(ver) => { + Err(APIError::ResolvedDependency(ver.dependency.name.clone())) + } + VersionDependency::Unresolved(ver) => Ok(ver), + }?; + + if unresolved_dependency.version_id.is_none() && unresolved_dependency.project_id.is_none() { + return Err(APIError::UnresolvableDependency); } - info!("Resolving dependnecies for mod {}", project.name); - for dependency in project.dependencies.as_mut().unwrap().iter_mut() { - let unresolved_dependency = match dependency { - VersionDependency::Resolved(ver) => { - Err(APIError::ResolvedDependency(ver.dependency.name.clone())) - } - VersionDependency::Unresolved(ver) => Ok(ver), - }?; - - if unresolved_dependency.version_id.is_none() && unresolved_dependency.project_id.is_none() - { - return Err(APIError::UnresolvableDependency); - } - - let mut resolved_version = if unresolved_dependency.version_id.is_some() { - get_version(client, unresolved_dependency).await? - } else { - warn!( - "No version ID supplied for project {:?}", - unresolved_dependency.project_id.as_ref().unwrap() - ); - let version_list = get_versions(client, unresolved_dependency, version_params).await?; - - if version_list.len() == 1 { - debug!("Only 1 version found, returning that"); - version_list.into_iter().next().unwrap() - } else { - debug!( - "{} versions found with the matching criterion", - version_list.len() - ); - resolver(version_list) - } - }; - - if resolved_version - .dependencies - .as_ref() - .is_some_and(|deps| !deps.is_empty()) - { - debug!( - "Resolving dependency {}'s dependencies", - resolved_version.name - ); - Box::pin(resolve_dependencies( - client, - &mut resolved_version, - version_params, - resolver, - )) - .await?; - } - - info!( - "Mod with ID {} resolved to {}", - resolved_version.id, resolved_version.name + let mut resolved_version = if unresolved_dependency.version_id.is_some() { + get_version(client, unresolved_dependency).await? + } else { + warn!( + "No version ID supplied for project {:?}", + unresolved_dependency.project_id.as_ref().unwrap() + ); + let version_list = get_versions(client, unresolved_dependency, version_params).await?; + + if version_list.len() == 1 { + debug!("Only 1 version found, returning that"); + version_list.into_iter().next().unwrap() + } else { + debug!( + "{} versions found with the matching criterion", + version_list.len() ); - *dependency = VersionDependency::Resolved(ResolvedVersionDependency { - dependency: resolved_version, - dependency_type: unresolved_dependency.dependency_type.clone(), - }); + resolver(version_list) + } + }; + + if resolved_version + .dependencies + .as_ref() + .is_some_and(|deps| !deps.is_empty()) + { + debug!( + "Resolving dependency {}'s dependencies", + resolved_version.name + ); + Box::pin(resolve_dependencies( + client, + &mut resolved_version, + version_params, + resolver, + )) + .await?; } - info!("All dependnecies resolved!"); - Ok(()) + info!( + "Mod with ID {} resolved to {}", + resolved_version.id, resolved_version.name + ); + *dependency = VersionDependency::Resolved(ResolvedVersionDependency { + dependency: resolved_version, + dependency_type: unresolved_dependency.dependency_type.clone(), + }); + } + + info!("All dependnecies resolved!"); + Ok(()) } #[cfg(test)] mod test { - use super::*; - use crate::{ - get_client, search_project, IndexBy, Loader, ProjectQueryBuilder, VersionQueryBuilder, - }; - - #[tokio::test] - async fn check_dep_resolution() { - let client = get_client().await.unwrap(); - - let query = ProjectQueryBuilder::new() - .query("BotaniaCombat") - .limit(1) - .index_by(IndexBy::Relevance) - .build(); - - let res = search_project(&client, &query).await.unwrap(); - let project = res.hits.first().unwrap(); - - let v_query = VersionQueryBuilder::new() - .featured(true) - .versions(vec!["1.20.1"]) - .loaders(vec![Loader::Fabric]) - .build(); - - let mut versions = get_versions(&client, &project, &v_query).await.unwrap(); - let version = versions.get_mut(0).unwrap(); - - let _err = resolve_dependencies(&client, version, &v_query, |versions| { - versions.into_iter().next().unwrap() - }) - .await; - - assert!(version - .dependencies - .as_ref() - .unwrap() - .iter() - .all(|dep| dep.is_resolved())); - } + use super::*; + use crate::{ + get_client, search_project, IndexBy, Loader, ProjectQueryBuilder, VersionQueryBuilder, + }; + + #[tokio::test] + async fn check_dep_resolution() { + let client = get_client().await.unwrap(); + + let query = ProjectQueryBuilder::new() + .query("BotaniaCombat") + .limit(1) + .index_by(IndexBy::Relevance) + .build(); + + let res = search_project(&client, &query).await.unwrap(); + let project = res.hits.first().unwrap(); + + let v_query = VersionQueryBuilder::new() + .featured(true) + .versions(vec!["1.20.1"]) + .loaders(vec![Loader::Fabric]) + .build(); + + let mut versions = get_versions(&client, &project, &v_query).await.unwrap(); + let version = versions.get_mut(0).unwrap(); + + let _err = resolve_dependencies(&client, version, &v_query, |versions| { + versions.into_iter().next().unwrap() + }) + .await; + + assert!(version + .dependencies + .as_ref() + .unwrap() + .iter() + .all(|dep| dep.is_resolved())); + } } diff --git a/modrinth/src/api/mod.rs b/modrinth/src/api/mod.rs index f4196fe..4d0991f 100644 --- a/modrinth/src/api/mod.rs +++ b/modrinth/src/api/mod.rs @@ -1,4 +1,4 @@ -#![cfg_attr(not(feature = "api"), allow(unused_imports, dead_code))] +#![cfg_attr(not(feature = "api"), expect(unused_imports, dead_code))] use std::{rc::Rc, time::Duration}; @@ -28,17 +28,17 @@ const ENDPOINT: &str = "https://api.modrinth.com"; #[cfg(feature = "api")] #[derive(Debug, Error)] pub enum APIError { - #[error("http error: {0}")] - Http(#[from] reqwest::Error), + #[error("http error: {0}")] + Http(#[from] reqwest::Error), - #[error("dependency already resolved: {0}")] - ResolvedDependency(Rc), + #[error("dependency already resolved: {0}")] + ResolvedDependency(Rc), - #[error("provided mod has no dependencies")] - NoDependencies, + #[error("provided mod has no dependencies")] + NoDependencies, - #[error("provided mod has unresolvable dependencies")] - UnresolvableDependency, + #[error("provided mod has unresolvable dependencies")] + UnresolvableDependency, } #[cfg(feature = "api")] @@ -66,24 +66,24 @@ pub enum APIError { /// } /// ``` pub async fn check_api() -> Result<(bool, Client), APIError> { - debug!("building client"); - let client = Client::builder() - .user_agent(format!( - "{} using {} v{}", - std::env::var("CARGO_BIN_NAME").unwrap_or(String::from("")), - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_VERSION") - )) - .https_only(true) - .timeout(Duration::from_secs(30)) - .connection_verbose(false) - .redirect(reqwest::redirect::Policy::none()) - .build()?; - - debug!("vibe checking modrinth endpoint at {:?}", ENDPOINT); - let resp = client.get(ENDPOINT).send().await; - - Ok((resp.is_ok(), client)) + debug!("building client"); + let client = Client::builder() + .user_agent(format!( + "{} using {} v{}", + std::env::var("CARGO_BIN_NAME").unwrap_or(String::from("")), + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION") + )) + .https_only(true) + .timeout(Duration::from_secs(30)) + .connection_verbose(false) + .redirect(reqwest::redirect::Policy::none()) + .build()?; + + debug!("vibe checking modrinth endpoint at {:?}", ENDPOINT); + let resp = client.get(ENDPOINT).send().await; + + Ok((resp.is_ok(), client)) } #[cfg(feature = "api")] @@ -103,30 +103,33 @@ pub async fn check_api() -> Result<(bool, Client), APIError> { /// } /// ``` pub async fn get_client() -> Option { - info!("Checking api"); - let api_check = check_api().await; - - if let Err(api_err) = api_check { - error!("Error while testing Modrinth api: {:}. Are you sure you are connected to the internet?", api_err); - return None; - } - let (_labrinth_responding, client) = api_check.unwrap(); - - Some(client) + info!("Checking api"); + let api_check = check_api().await; + + if let Err(api_err) = api_check { + error!( + "Error while testing Modrinth api: {:}. Are you sure you are connected to the internet?", + api_err + ); + return None; + } + let (_labrinth_responding, client) = api_check.unwrap(); + + Some(client) } #[cfg(test)] #[cfg(feature = "api")] mod tests { - use super::check_api; + use super::check_api; - #[tokio::test] - async fn check_api_works() { - let api_check = check_api().await; + #[tokio::test] + async fn check_api_works() { + let api_check = check_api().await; - assert!(api_check.is_ok()); - let (labrinth_responding, _client) = api_check.unwrap(); + assert!(api_check.is_ok()); + let (labrinth_responding, _client) = api_check.unwrap(); - assert!(labrinth_responding); - } + assert!(labrinth_responding); + } } diff --git a/modrinth/src/api/project.rs b/modrinth/src/api/project.rs index bb4e8e1..e4441e8 100644 --- a/modrinth/src/api/project.rs +++ b/modrinth/src/api/project.rs @@ -38,21 +38,21 @@ use crate::types::result::SearchProjectResult; /// } /// ``` pub async fn search_project( - client: &Client, - params: &ProjectQuery, + client: &Client, + params: &ProjectQuery, ) -> Result { - info!("Searching for project with params: {:?}", params); - let resp: SearchProjectResult = client - .get(format!("{}/v2/search", ENDPOINT)) - .query(params) - .send() - .await - .unwrap() - .json() - .await?; - - assert_eq!(resp.hits.len(), params.limit as usize); - Ok(resp) + info!("Searching for project with params: {:?}", params); + let resp: SearchProjectResult = client + .get(format!("{}/v2/search", ENDPOINT)) + .query(params) + .send() + .await + .unwrap() + .json() + .await?; + + assert_eq!(resp.hits.len(), params.limit as usize); + Ok(resp) } /// Gets a specific project, returned by `search_project` @@ -87,73 +87,75 @@ pub async fn search_project( /// } /// ``` pub async fn get_project( - client: &Client, - project: &SearchProjectHit, + client: &Client, + project: &SearchProjectHit, ) -> Result { - info!("Getting project information for {}", project.title); - Ok(client - .get(format!("{}/v2/project/{}", ENDPOINT, project.project_id)) - .send() - .await - .unwrap() - .json() - .await?) + info!("Getting project information for {}", project.title); + Ok( + client + .get(format!("{}/v2/project/{}", ENDPOINT, project.project_id)) + .send() + .await + .unwrap() + .json() + .await?, + ) } #[cfg(test)] mod test { - use super::*; - use crate::api::get_client; - use crate::types::query::ProjectQueryBuilder; - use crate::types::Facet; - use crate::types::Loader; - use crate::types::{IndexBy, ProjectType}; - - #[tokio::test] - async fn check_search_projects() { - let client = get_client().await.unwrap(); - - let query = ProjectQueryBuilder::new() - .query("gravestones") - .limit(3) - .index_by(IndexBy::Relevance) - .facets(vec![ - vec![Facet::Loader(Loader::Forge)], - vec![ - Facet::Category("adventure".to_string()), - Facet::Category("utility".to_string()), - ], - ]) - .build(); - - let res = search_project(&client, &query).await; - - assert!(res.is_ok()); - } - - #[tokio::test] - async fn check_get_project() { - let client = get_client().await.unwrap(); - - let query = ProjectQueryBuilder::new() - .query("kontraption") - .limit(1) - .index_by(IndexBy::Relevance) - .build(); - - let res = search_project(&client, &query).await.unwrap(); - - let res = res.hits.first().unwrap(); - assert_eq!(res.project_id, "5yJ5IDKm".into()); // https://modrinth.com/mod/kontraption - assert_eq!(res.project_type, ProjectType::Mod); - - let project = get_project(&client, res).await; - - assert!(project.is_ok()); - - let project = project.unwrap(); - - assert_eq!(project.id, "5yJ5IDKm".into()); - assert_eq!(project.project_type, ProjectType::Mod); - } + use super::*; + use crate::api::get_client; + use crate::types::query::ProjectQueryBuilder; + use crate::types::Facet; + use crate::types::Loader; + use crate::types::{IndexBy, ProjectType}; + + #[tokio::test] + async fn check_search_projects() { + let client = get_client().await.unwrap(); + + let query = ProjectQueryBuilder::new() + .query("gravestones") + .limit(3) + .index_by(IndexBy::Relevance) + .facets(vec![ + vec![Facet::Loader(Loader::Forge)], + vec![ + Facet::Category("adventure".to_string()), + Facet::Category("utility".to_string()), + ], + ]) + .build(); + + let res = search_project(&client, &query).await; + + assert!(res.is_ok()); + } + + #[tokio::test] + async fn check_get_project() { + let client = get_client().await.unwrap(); + + let query = ProjectQueryBuilder::new() + .query("kontraption") + .limit(1) + .index_by(IndexBy::Relevance) + .build(); + + let res = search_project(&client, &query).await.unwrap(); + + let res = res.hits.first().unwrap(); + assert_eq!(res.project_id, "5yJ5IDKm".into()); // https://modrinth.com/mod/kontraption + assert_eq!(res.project_type, ProjectType::Mod); + + let project = get_project(&client, res).await; + + assert!(project.is_ok()); + + let project = project.unwrap(); + + assert_eq!(project.id, "5yJ5IDKm".into()); + assert_eq!(project.project_type, ProjectType::Mod); + } } diff --git a/modrinth/src/api/version.rs b/modrinth/src/api/version.rs index f5118ee..5e505e4 100644 --- a/modrinth/src/api/version.rs +++ b/modrinth/src/api/version.rs @@ -8,7 +8,7 @@ use crate::types::query::VersionQuery; use crate::types::version::ModrinthProjectVersion; use crate::types::ModrinthProjectMeta; -#[allow(private_bounds)] +#[expect(private_bounds)] /// Lists versions of `project` /// ## Usage /// ``` @@ -43,89 +43,89 @@ use crate::types::ModrinthProjectMeta; /// } /// ``` pub async fn get_versions( - client: &Client, - project: &M, - params: &VersionQuery, + client: &Client, + project: &M, + params: &VersionQuery, ) -> Result, APIError> where - M: ModrinthProjectMeta, - ::Id: Display, + M: ModrinthProjectMeta, + ::Id: Display, { - info!("Searching for versions with params: {:?}", params); + info!("Searching for versions with params: {:?}", params); - let resp: Vec = client - // TODO: ADD ERROR - .get(format!( - "{}/v2/project/{}/version", - ENDPOINT, - project.project_id().unwrap() - )) - .query(params) - .send() - .await - .unwrap() - // .text() - .json() - .await?; + let resp: Vec = client + // TODO: ADD ERROR + .get(format!( + "{}/v2/project/{}/version", + ENDPOINT, + project.project_id().unwrap() + )) + .query(params) + .send() + .await + .unwrap() + // .text() + .json() + .await?; - Ok(resp) + Ok(resp) } pub(crate) async fn get_version( - client: &Client, - project: &M, + client: &Client, + project: &M, ) -> Result where - M: ModrinthProjectMeta, - ::Id: Display + Debug, + M: ModrinthProjectMeta, + ::Id: Display + Debug, { - info!("Searching for version: {:?}", project.version_id().unwrap()); + info!("Searching for version: {:?}", project.version_id().unwrap()); - let resp: ModrinthProjectVersion = client - // TODO: ADD ERROR - .get(format!( - "{}/v2/version/{}", - ENDPOINT, - project.version_id().unwrap() - )) - .send() - .await - .unwrap() - .json() - .await?; + let resp: ModrinthProjectVersion = client + // TODO: ADD ERROR + .get(format!( + "{}/v2/version/{}", + ENDPOINT, + project.version_id().unwrap() + )) + .send() + .await + .unwrap() + .json() + .await?; - Ok(resp) + Ok(resp) } #[cfg(test)] mod test { - use super::*; - use crate::{ - get_client, search_project, IndexBy, Loader, ProjectQueryBuilder, VersionQueryBuilder, - }; + use super::*; + use crate::{ + get_client, search_project, IndexBy, Loader, ProjectQueryBuilder, VersionQueryBuilder, + }; - #[tokio::test] - async fn check_get_versions() { - let client = get_client().await.unwrap(); + #[tokio::test] + async fn check_get_versions() { + let client = get_client().await.unwrap(); - let query = ProjectQueryBuilder::new() - .query("kontraption") - .limit(1) - .index_by(IndexBy::Relevance) - .build(); + let query = ProjectQueryBuilder::new() + .query("kontraption") + .limit(1) + .index_by(IndexBy::Relevance) + .build(); - let res = search_project(&client, &query).await.unwrap(); - let project = res.hits.first().unwrap(); + let res = search_project(&client, &query).await.unwrap(); + let project = res.hits.first().unwrap(); - let v_query = VersionQueryBuilder::new() - .featured(true) - .versions(vec!["1.20.1"]) - .loaders(vec![Loader::Forge]) - .build(); + let v_query = VersionQueryBuilder::new() + .featured(true) + .versions(vec!["1.20.1"]) + .loaders(vec![Loader::Forge]) + .build(); - let version = get_versions(&client, &project, &v_query).await; + let version = get_versions(&client, &project, &v_query).await; - assert!(version.is_ok()); - assert!(!version.unwrap().is_empty()); - } + assert!(version.is_ok()); + assert!(!version.unwrap().is_empty()); + } } diff --git a/modrinth/src/types/mod.rs b/modrinth/src/types/mod.rs index b0b5362..2e388ad 100644 --- a/modrinth/src/types/mod.rs +++ b/modrinth/src/types/mod.rs @@ -1,4 +1,4 @@ -#![allow(clippy::ptr_arg)] +#![expect(clippy::ptr_arg)] #[cfg(feature = "types")] use serde::{Deserialize, Serialize, Serializer}; use std::rc::Rc; @@ -17,12 +17,12 @@ pub use query::{Facet, FacetOp}; #[cfg(feature = "types")] pub(crate) trait ModrinthProjectMeta { - type Id; + type Id; - fn project_id(&self) -> Option; - fn version_id(&self) -> Option { - None - } + fn project_id(&self) -> Option; + fn version_id(&self) -> Option { + None + } } #[cfg(feature = "types")] @@ -31,26 +31,26 @@ pub(crate) trait ModrinthProjectMeta { /// The license of a project. Can be /// a `Single` or `Detailed` license pub enum License { - /// License Type - Single(Rc), - Detailed { - /// License ID - id: Rc, - /// License pretty name - name: Rc, - /// URL where the license can be found - url: Option>, - }, + /// License Type + Single(Rc), + Detailed { + /// License ID + id: Rc, + /// License pretty name + name: Rc, + /// URL where the license can be found + url: Option>, + }, } #[cfg(feature = "types")] #[derive(Debug, Deserialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum ModRequirement { - Optional, - Required, - Unsupported, - Unknown, + Optional, + Required, + Unsupported, + Unknown, } #[cfg(feature = "types")] @@ -58,12 +58,12 @@ pub enum ModRequirement { #[serde(rename_all = "lowercase")] /// The mode to index by pub enum IndexBy { - #[default] - Relevance, - Downloads, - Follows, - Newest, - Updated, + #[default] + Relevance, + Downloads, + Follows, + Newest, + Updated, } #[cfg(feature = "types")] @@ -71,149 +71,151 @@ pub enum IndexBy { #[serde(rename_all = "lowercase")] /// The loaders Modrinth supports pub enum Loader { - Bukkit, - Bungeecord, - Canvas, - Datapack, - Fabric, - Folia, - Forge, - Iris, - Liteloader, - Minecraft, - Modloader, - Neoforge, - Optifine, - Purpur, - Quilt, - Rift, - Spigot, - Sponge, - Vanilla, - Velocity, - Waterfall, + Bukkit, + Bungeecord, + Canvas, + Datapack, + Fabric, + Folia, + Forge, + Iris, + Liteloader, + Minecraft, + Modloader, + Neoforge, + Optifine, + Purpur, + Quilt, + Rift, + Spigot, + Sponge, + Vanilla, + Velocity, + Waterfall, } impl ToString for Loader { - fn to_string(&self) -> String { - match self { - Self::Bukkit => "bukkit", - Self::Bungeecord => "bungeecord", - Self::Canvas => "canvas", - Self::Datapack => "datapack", - Self::Fabric => "fabric", - Self::Folia => "folia", - Self::Forge => "forge", - Self::Iris => "iris", - Self::Liteloader => "liteloader", - Self::Minecraft => "minecraft", - Self::Modloader => "modloader", - Self::Neoforge => "neoforge", - Self::Optifine => "optifine", - Self::Purpur => "purpur", - Self::Quilt => "quilt", - Self::Rift => "rift", - Self::Spigot => "spigot", - Self::Sponge => "sponge", - Self::Vanilla => "vanilla", - Self::Velocity => "velocity", - Self::Waterfall => "waterfall", - } - .to_string() + fn to_string(&self) -> String { + match self { + Self::Bukkit => "bukkit", + Self::Bungeecord => "bungeecord", + Self::Canvas => "canvas", + Self::Datapack => "datapack", + Self::Fabric => "fabric", + Self::Folia => "folia", + Self::Forge => "forge", + Self::Iris => "iris", + Self::Liteloader => "liteloader", + Self::Minecraft => "minecraft", + Self::Modloader => "modloader", + Self::Neoforge => "neoforge", + Self::Optifine => "optifine", + Self::Purpur => "purpur", + Self::Quilt => "quilt", + Self::Rift => "rift", + Self::Spigot => "spigot", + Self::Sponge => "sponge", + Self::Vanilla => "vanilla", + Self::Velocity => "velocity", + Self::Waterfall => "waterfall", } + .to_string() + } } impl Serialize for Loader { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&self.to_string()) - } + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } } #[cfg(feature = "types")] #[derive(Debug, Deserialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum ProjectType { - Mod, - Modpack, - Resourcepack, - Shader, + Mod, + Modpack, + Resourcepack, + Shader, } impl ToString for ProjectType { - fn to_string(&self) -> String { - match self { - Self::Mod => "mod", - Self::Modpack => "modpack", - Self::Resourcepack => "resourcepack", - Self::Shader => "shader", - } - .to_string() + fn to_string(&self) -> String { + match self { + Self::Mod => "mod", + Self::Modpack => "modpack", + Self::Resourcepack => "resourcepack", + Self::Shader => "shader", } + .to_string() + } } impl Serialize for ProjectType { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&self.to_string()) - } + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } } #[cfg(feature = "types")] #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum Gallery { - Single(Rc), - Multiple(Vec>), + Single(Rc), + Multiple(Vec>), } pub(crate) fn serialize_vec_urlencoded(vec: &Vec, serializer: S) -> Result where - S: Serializer, - T: Serialize + ToString, + S: Serializer, + T: Serialize + ToString, { - let vec_str = serialize_vec(vec); + let vec_str = serialize_vec(vec); - serializer.serialize_str(&vec_str) + serializer.serialize_str(&vec_str) } pub(crate) fn serialize_vec_nested( - vec: &Vec>, - serializer: S, + vec: &Vec>, + serializer: S, ) -> Result where - S: Serializer, - T: Serialize + ToString, + S: Serializer, + T: Serialize + ToString, { - let vec_vec_str = format!( - "[{}]", - vec.iter() - .map(serialize_vec) - .collect::>() - .join(", ") - ); - - serializer.serialize_str(&vec_vec_str) + let vec_vec_str = format!( + "[{}]", + vec + .iter() + .map(serialize_vec) + .collect::>() + .join(", ") + ); + + serializer.serialize_str(&vec_vec_str) } fn serialize_vec(vec: &Vec) -> String where - T: ToString, + T: ToString, { - format!( - "[{}]", - vec.iter() - .map(|a| format!("{:?}", a.to_string())) - .collect::>() - .join(", ") - ) + format!( + "[{}]", + vec + .iter() + .map(|a| format!("{:?}", a.to_string())) + .collect::>() + .join(", ") + ) } -#[allow(clippy::trivially_copy_pass_by_ref)] +#[expect(clippy::trivially_copy_pass_by_ref)] pub(in crate::types) fn is_zero(num: &u8) -> bool { - *num == 0 + *num == 0 } diff --git a/modrinth/src/types/project.rs b/modrinth/src/types/project.rs index 258a30b..41da07d 100644 --- a/modrinth/src/types/project.rs +++ b/modrinth/src/types/project.rs @@ -10,96 +10,96 @@ use super::{Loader, ModRequirement, ProjectType}; /// *have been copied over from [Modrinth's documentation](https://docs.modrinth.com/#tag/project_model)* #[derive(Debug, Deserialize)] pub struct ModrinthProject { - /// The slug of a project, used for vanity URLs. Regex: `^[\w!@$()`.+,"\-']{3,64}$` - pub slug: Rc, - /// The title or name of the project - pub title: Rc, - /// A short description of the project - pub description: Rc, - /// A list of the categories that the project has - pub categories: Vec>, - /// The client side support of the project - pub client_side: ModRequirement, - /// The client side support of the project - pub server_side: ModRequirement, - /// A long form description of the project - pub body: Rc, - /// The status of the project - pub status: Status, + /// The slug of a project, used for vanity URLs. Regex: `^[\w!@$()`.+,"\-']{3,64}$` + pub slug: Rc, + /// The title or name of the project + pub title: Rc, + /// A short description of the project + pub description: Rc, + /// A list of the categories that the project has + pub categories: Vec>, + /// The client side support of the project + pub client_side: ModRequirement, + /// The client side support of the project + pub server_side: ModRequirement, + /// A long form description of the project + pub body: Rc, + /// The status of the project + pub status: Status, - /// A list of categories which are searchable but non-primary - pub additional_categories: Option>>, - /// An optional link to where to submit bugs or issues with the project - pub issues_url: Option>, - /// An optional link to the source code of the project - pub source_url: Option>, - /// An optional link to the project's wiki page or other relevant information - pub wiki_url: Option>, - /// An optional invite link to the project's discord - pub discord_url: Option>, + /// A list of categories which are searchable but non-primary + pub additional_categories: Option>>, + /// An optional link to where to submit bugs or issues with the project + pub issues_url: Option>, + /// An optional link to the source code of the project + pub source_url: Option>, + /// An optional link to the project's wiki page or other relevant information + pub wiki_url: Option>, + /// An optional invite link to the project's discord + pub discord_url: Option>, - /// The project type of the project - pub project_type: ProjectType, - /// The URL of the project's icon - pub icon_url: Option>, + /// The project type of the project + pub project_type: ProjectType, + /// The URL of the project's icon + pub icon_url: Option>, - /// The RGB color of the project, automatically generated from the project icon - pub color: Option, + /// The RGB color of the project, automatically generated from the project icon + pub color: Option, - /// The ID of the project, encoded as a base62 Rc - pub id: Rc, - /// The ID of the team that has ownership of this project - pub team: Rc, + /// The ID of the project, encoded as a base62 Rc + pub id: Rc, + /// The ID of the team that has ownership of this project + pub team: Rc, - /// The date the project was published - pub published: Rc, - /// The date the project was last updated - pub updated: Rc, + /// The date the project was published + pub published: Rc, + /// The date the project was last updated + pub updated: Rc, - /// A list of the version IDs of the project (will never be empty unless `draft` status) - pub versions: Vec>, - /// A list of all of the game versions supported by the project - pub game_versions: Vec>, - /// A list of all of the loaders supported by the project - pub loaders: Rc<[Loader]>, - /// A list of images that have been uploaded to the project's gallery - pub gallery: Option>, + /// A list of the version IDs of the project (will never be empty unless `draft` status) + pub versions: Vec>, + /// A list of all of the game versions supported by the project + pub game_versions: Vec>, + /// A list of all of the loaders supported by the project + pub loaders: Rc<[Loader]>, + /// A list of images that have been uploaded to the project's gallery + pub gallery: Option>, } impl super::ModrinthProjectMeta for ModrinthProject { - type Id = Rc; + type Id = Rc; - fn project_id(&self) -> Option { - Some(self.id.clone()) - } + fn project_id(&self) -> Option { + Some(self.id.clone()) + } } #[derive(Debug, Deserialize)] /// Represents an image in a gallery pub struct GalleryEntry { - /// The URL of the image - pub url: Rc, - /// The image's title - pub title: Option>, - /// The image's description - pub description: Option>, - /// When the image was uploaded - pub created: Rc, - /// What order/index the image should be at - pub ordering: Option, + /// The URL of the image + pub url: Rc, + /// The image's title + pub title: Option>, + /// The image's description + pub description: Option>, + /// When the image was uploaded + pub created: Rc, + /// What order/index the image should be at + pub ordering: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "lowercase")] pub enum Status { - Approved, - Archived, - Rejected, - Draft, - Unlisted, - Processing, - Withheld, - Scheduled, - Private, - Unknown, + Approved, + Archived, + Rejected, + Draft, + Unlisted, + Processing, + Withheld, + Scheduled, + Private, + Unknown, } diff --git a/modrinth/src/types/query/facets.rs b/modrinth/src/types/query/facets.rs index 72b20d0..a87fd8f 100644 --- a/modrinth/src/types/query/facets.rs +++ b/modrinth/src/types/query/facets.rs @@ -11,70 +11,70 @@ use serde::Serialize; /// and the `Custom` variant works /// can be found [here](https://docs.modrinth.com/#tag/projects) pub enum Facet { - /// Project must be of type... - ProjectType(ProjectType), - /// Project must be categorized as... - Category(String), - /// Project must be loadable by... - Loader(Loader), - /// Project must be supported by minecraft version... - Version(String), - /// Project must be open source? - OpenSource(bool), - /// Project must be licensed under... - License(String), - Custom { - _type: String, - op: FacetOp, - value: String, - }, + /// Project must be of type... + ProjectType(ProjectType), + /// Project must be categorized as... + Category(String), + /// Project must be loadable by... + Loader(Loader), + /// Project must be supported by minecraft version... + Version(String), + /// Project must be open source? + OpenSource(bool), + /// Project must be licensed under... + License(String), + Custom { + _type: String, + op: FacetOp, + value: String, + }, } impl ToString for Facet { - fn to_string(&self) -> String { - match self { - Self::ProjectType(project_type) => format!("project_type:{}", project_type.to_string()), - Self::Category(category) => format!("categories:{}", category), - Self::Loader(loader) => format!("categories:{}", loader.to_string()), - Self::Version(version) => format!("version:{}", version), - Self::OpenSource(open_source) => format!("open_source:{}", open_source), - Self::License(license) => format!("license:{}", license), - Self::Custom { _type, op, value } => format!("{}{}{}", _type, op.to_string(), value), - } + fn to_string(&self) -> String { + match self { + Self::ProjectType(project_type) => format!("project_type:{}", project_type.to_string()), + Self::Category(category) => format!("categories:{}", category), + Self::Loader(loader) => format!("categories:{}", loader.to_string()), + Self::Version(version) => format!("version:{}", version), + Self::OpenSource(open_source) => format!("open_source:{}", open_source), + Self::License(license) => format!("license:{}", license), + Self::Custom { _type, op, value } => format!("{}{}{}", _type, op.to_string(), value), } + } } impl Serialize for Facet { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let facet_str = self.to_string(); + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let facet_str = self.to_string(); - serializer.serialize_str(&facet_str) - } + serializer.serialize_str(&facet_str) + } } #[derive(Debug)] pub enum FacetOp { - Equal, // = - NotEqual, // != - GreaterEqual, // >= - GreaterThan, // > - LesserEqual, // <= - LesserThan, // < + Equal, // = + NotEqual, // != + GreaterEqual, // >= + GreaterThan, // > + LesserEqual, // <= + LesserThan, // < } impl ToString for FacetOp { - fn to_string(&self) -> String { - match self { - Self::Equal => "=", - Self::NotEqual => "!=", - Self::GreaterEqual => ">=", - Self::GreaterThan => ">", - Self::LesserEqual => "<=", - Self::LesserThan => "<", - } - .to_string() + fn to_string(&self) -> String { + match self { + Self::Equal => "=", + Self::NotEqual => "!=", + Self::GreaterEqual => ">=", + Self::GreaterThan => ">", + Self::LesserEqual => "<=", + Self::LesserThan => "<", } + .to_string() + } } diff --git a/modrinth/src/types/query/query.rs b/modrinth/src/types/query/query.rs index 1518a34..0dd50a1 100644 --- a/modrinth/src/types/query/query.rs +++ b/modrinth/src/types/query/query.rs @@ -6,120 +6,120 @@ use serde::Serialize; /// Represents a built complex search query for /// `search_projects` pub struct ProjectQuery { - pub(crate) query: String, - #[serde( - skip_serializing_if = "Vec::is_empty", - serialize_with = "crate::types::serialize_vec_nested" - )] - pub(crate) facets: Vec>, - // TODO: some sort of is_default thingy - // so that serde omits this if its - // set to its defaults - pub(crate) index: IndexBy, - #[serde(skip_serializing_if = "crate::types::is_zero")] - pub(crate) offset: u8, - #[serde(skip_serializing_if = "crate::types::is_zero")] - pub(crate) limit: u8, + pub(crate) query: String, + #[serde( + skip_serializing_if = "Vec::is_empty", + serialize_with = "crate::types::serialize_vec_nested" + )] + pub(crate) facets: Vec>, + // TODO: some sort of is_default thingy + // so that serde omits this if its + // set to its defaults + pub(crate) index: IndexBy, + #[serde(skip_serializing_if = "crate::types::is_zero")] + pub(crate) offset: u8, + #[serde(skip_serializing_if = "crate::types::is_zero")] + pub(crate) limit: u8, } #[derive(Debug, Default)] /// Represents a complex search query for /// `search_projects`. Use `.build()` to build /// the query pub struct ProjectQueryBuilder { - query: Option, - facets: Option>>, - index_by: Option, - offset: Option, - limit: Option, + query: Option, + facets: Option>>, + index_by: Option, + offset: Option, + limit: Option, } impl ProjectQueryBuilder { - /// Creates a new query - pub fn new() -> Self { - Self::default() - } + /// Creates a new query + pub fn new() -> Self { + Self::default() + } - /// The project to search for - pub fn query(mut self, query: S) -> Self { - self.query = Some(query.to_string()); - self - } + /// The project to search for + pub fn query(mut self, query: S) -> Self { + self.query = Some(query.to_string()); + self + } - /// Facets are an essential concept for understanding how to filter out results. - /// These are the most commonly used facet types: - /// - /// - `project_type` - /// - `categories` (loaders are lumped in with categories in search) - /// - `versions` - /// - `client_side` - /// - `server_side` - /// - `open_source` - /// - /// Several others are also available for use, though these should not be used outside very specific use cases. - /// - /// - `title` - /// - `author` - /// - `follows` - /// - `project_id` - /// - `license` - /// - `downloads` - /// - `color` - /// - `created_timestamp` - /// - `modified_timestamp` - /// - /// In order to then use these facets, you need a value to filter by, as well as an operation to perform on this value. The most common operation is : (same as =), though you can also use !=, >=, >, <=, and <. Join together the type, operation, and value, and you've got your string. - /// > `{type} {operation} {value}` - /// - /// Examples: - /// - `categories = adventure` - /// - `versions != 1.20.1` - /// - `downloads <= 100` - /// - /// You then join these strings together in arrays to signal AND and OR operators. - /// ### OR - /// - /// All elements in a single array are considered to be joined by OR statements. - /// For example, the search `[["versions:1.16.5", "versions:1.17.1"]]` translates to *Projects that support 1.16.5 OR 1.17.1.* - /// ### AND - /// - /// Separate arrays are considered to be joined by AND statements. - /// For example, the search `[["versions:1.16.5"], ["project_type:modpack"]]` translates to *Projects that support 1.16.5 AND are modpacks*. - pub fn facets(mut self, facets: Vec>) -> Self { - self.facets = Some(facets); - self - } + /// Facets are an essential concept for understanding how to filter out results. + /// These are the most commonly used facet types: + /// + /// - `project_type` + /// - `categories` (loaders are lumped in with categories in search) + /// - `versions` + /// - `client_side` + /// - `server_side` + /// - `open_source` + /// + /// Several others are also available for use, though these should not be used outside very specific use cases. + /// + /// - `title` + /// - `author` + /// - `follows` + /// - `project_id` + /// - `license` + /// - `downloads` + /// - `color` + /// - `created_timestamp` + /// - `modified_timestamp` + /// + /// In order to then use these facets, you need a value to filter by, as well as an operation to perform on this value. The most common operation is : (same as =), though you can also use !=, >=, >, <=, and <. Join together the type, operation, and value, and you've got your string. + /// > `{type} {operation} {value}` + /// + /// Examples: + /// - `categories = adventure` + /// - `versions != 1.20.1` + /// - `downloads <= 100` + /// + /// You then join these strings together in arrays to signal AND and OR operators. + /// ### OR + /// + /// All elements in a single array are considered to be joined by OR statements. + /// For example, the search `[["versions:1.16.5", "versions:1.17.1"]]` translates to *Projects that support 1.16.5 OR 1.17.1.* + /// ### AND + /// + /// Separate arrays are considered to be joined by AND statements. + /// For example, the search `[["versions:1.16.5"], ["project_type:modpack"]]` translates to *Projects that support 1.16.5 AND are modpacks*. + pub fn facets(mut self, facets: Vec>) -> Self { + self.facets = Some(facets); + self + } - /// TThe offset into the search. Skips this number of results - pub fn offset(mut self, offset: u8) -> Self { - self.offset = Some(offset); - self - } + /// TThe offset into the search. Skips this number of results + pub fn offset(mut self, offset: u8) -> Self { + self.offset = Some(offset); + self + } - /// The sorting method used for sorting search results - pub fn index_by(mut self, index_by: IndexBy) -> Self { - self.index_by = Some(index_by); - self - } + /// The sorting method used for sorting search results + pub fn index_by(mut self, index_by: IndexBy) -> Self { + self.index_by = Some(index_by); + self + } - /// The number of results returned by the search - /// - /// # Disclaimer - /// This function silently does nothing if the supplied - /// `limit` is above 100 in accordance to modrinth's limits - pub fn limit(mut self, limit: u8) -> Self { - self.limit = limit.lt(&100).then_some(limit); + /// The number of results returned by the search + /// + /// # Disclaimer + /// This function silently does nothing if the supplied + /// `limit` is above 100 in accordance to modrinth's limits + pub fn limit(mut self, limit: u8) -> Self { + self.limit = limit.lt(&100).then_some(limit); - self - } + self + } - /// Builds the query - pub fn build(self) -> ProjectQuery { - ProjectQuery { - query: self.query.unwrap_or_default(), - facets: self.facets.unwrap_or_default(), - index: self.index_by.unwrap_or_default(), - offset: self.offset.unwrap_or_default(), - limit: self.limit.unwrap_or(10), - } + /// Builds the query + pub fn build(self) -> ProjectQuery { + ProjectQuery { + query: self.query.unwrap_or_default(), + facets: self.facets.unwrap_or_default(), + index: self.index_by.unwrap_or_default(), + offset: self.offset.unwrap_or_default(), + limit: self.limit.unwrap_or(10), } + } } diff --git a/modrinth/src/types/query/version.rs b/modrinth/src/types/query/version.rs index 8a47548..c7d7c3a 100644 --- a/modrinth/src/types/query/version.rs +++ b/modrinth/src/types/query/version.rs @@ -5,17 +5,17 @@ use serde::Serialize; /// Represents a built complex search query for /// `get_versions`. pub struct VersionQuery { - #[serde( - skip_serializing_if = "Vec::is_empty", - serialize_with = "crate::types::serialize_vec_urlencoded" - )] - pub(crate) loaders: Vec, - #[serde( - skip_serializing_if = "Vec::is_empty", - serialize_with = "crate::types::serialize_vec_urlencoded" - )] - pub(crate) game_versions: Vec, - pub(crate) featured: bool, + #[serde( + skip_serializing_if = "Vec::is_empty", + serialize_with = "crate::types::serialize_vec_urlencoded" + )] + pub(crate) loaders: Vec, + #[serde( + skip_serializing_if = "Vec::is_empty", + serialize_with = "crate::types::serialize_vec_urlencoded" + )] + pub(crate) game_versions: Vec, + pub(crate) featured: bool, } #[derive(Debug, Default)] @@ -23,41 +23,41 @@ pub struct VersionQuery { /// `get_versions`. Use `.build()` to build /// the query pub struct VersionQueryBuilder { - pub loaders: Option>, - pub versions: Option>, - pub featured: Option, + pub loaders: Option>, + pub versions: Option>, + pub featured: Option, } impl VersionQueryBuilder { - /// Creates a new query - pub fn new() -> Self { - Self::default() - } + /// Creates a new query + pub fn new() -> Self { + Self::default() + } - /// Version must support being loaded by... - pub fn loaders(mut self, loaders: Vec) -> Self { - self.loaders = Some(loaders); - self - } + /// Version must support being loaded by... + pub fn loaders(mut self, loaders: Vec) -> Self { + self.loaders = Some(loaders); + self + } - /// Version must support Minecraft version... - pub fn versions(mut self, versions: Vec) -> Self { - self.versions = Some(versions.iter().map(|a| a.to_string()).collect()); - self - } + /// Version must support Minecraft version... + pub fn versions(mut self, versions: Vec) -> Self { + self.versions = Some(versions.iter().map(|a| a.to_string()).collect()); + self + } - /// Version has to be featured - pub fn featured(mut self, featured: bool) -> Self { - self.featured = Some(featured); - self - } + /// Version has to be featured + pub fn featured(mut self, featured: bool) -> Self { + self.featured = Some(featured); + self + } - /// Build the query - pub fn build(self) -> VersionQuery { - VersionQuery { - loaders: self.loaders.unwrap_or_default(), - game_versions: self.versions.unwrap_or_default(), - featured: self.featured.unwrap_or_default(), - } + /// Build the query + pub fn build(self) -> VersionQuery { + VersionQuery { + loaders: self.loaders.unwrap_or_default(), + game_versions: self.versions.unwrap_or_default(), + featured: self.featured.unwrap_or_default(), } + } } diff --git a/modrinth/src/types/result.rs b/modrinth/src/types/result.rs index 336ce69..40090d6 100644 --- a/modrinth/src/types/result.rs +++ b/modrinth/src/types/result.rs @@ -4,10 +4,10 @@ use std::rc::Rc; #[derive(Debug, Deserialize)] pub struct SearchProjectResult { - pub hits: Rc<[SearchProjectHit]>, - pub offset: u8, - pub limit: u8, - pub total_hits: u16, + pub hits: Rc<[SearchProjectHit]>, + pub offset: u8, + pub limit: u8, + pub total_hits: u16, } #[derive(Debug, Deserialize)] @@ -16,58 +16,58 @@ pub struct SearchProjectResult { /// *The documentation for this struct's fields have* /// *been copied over from [Modrinth's documentation](https://docs.modrinth.com/#tag/project_result_model)* pub struct SearchProjectHit { - /// The slug of a project, used for vanity URLs. Regex: `^[\w!@$()`.+,"\-']{3,64}$` - pub slug: Rc, - /// The title or name of the project - pub title: Rc, - /// A short description of the project - pub description: Rc, - /// A list of the categories that the project has - pub categories: Vec>, - /// The client side support of the project - pub client_side: ModRequirement, - /// The server side support of the project - pub server_side: ModRequirement, - /// The project type of the project - pub project_type: ProjectType, - /// The total number of downloads of the project - pub downloads: u32, - /// The URL of the project's icon - pub icon_url: Rc, - /// The RGB color of the project, automatically generated from the project icon - pub color: u32, - /// The ID of the project - pub project_id: Rc, - /// The username of the project's author - pub author: Rc, - /// A list of the minecraft versions supported by the project - pub versions: Vec>, - /// The date the project was added to search - pub date_created: Rc, - /// The date the project was last modified - pub date_modified: Rc, - /// The latest version of minecraft that this project supports - pub latest_version: Rc, - /// The SPDX license ID of a project - pub license: License, - /// All gallery images attached to the project - pub gallery: Gallery, - /// The featured gallery image of the project - pub featured_gallery: Option>, + /// The slug of a project, used for vanity URLs. Regex: `^[\w!@$()`.+,"\-']{3,64}$` + pub slug: Rc, + /// The title or name of the project + pub title: Rc, + /// A short description of the project + pub description: Rc, + /// A list of the categories that the project has + pub categories: Vec>, + /// The client side support of the project + pub client_side: ModRequirement, + /// The server side support of the project + pub server_side: ModRequirement, + /// The project type of the project + pub project_type: ProjectType, + /// The total number of downloads of the project + pub downloads: u32, + /// The URL of the project's icon + pub icon_url: Rc, + /// The RGB color of the project, automatically generated from the project icon + pub color: u32, + /// The ID of the project + pub project_id: Rc, + /// The username of the project's author + pub author: Rc, + /// A list of the minecraft versions supported by the project + pub versions: Vec>, + /// The date the project was added to search + pub date_created: Rc, + /// The date the project was last modified + pub date_modified: Rc, + /// The latest version of minecraft that this project supports + pub latest_version: Rc, + /// The SPDX license ID of a project + pub license: License, + /// All gallery images attached to the project + pub gallery: Gallery, + /// The featured gallery image of the project + pub featured_gallery: Option>, } impl ModrinthProjectMeta for SearchProjectHit { - type Id = Rc; + type Id = Rc; - fn project_id(&self) -> Option { - Some(self.project_id.clone()) - } + fn project_id(&self) -> Option { + Some(self.project_id.clone()) + } } impl ModrinthProjectMeta for &SearchProjectHit { - type Id = Rc; + type Id = Rc; - fn project_id(&self) -> Option { - Some(self.project_id.clone()) - } + fn project_id(&self) -> Option { + Some(self.project_id.clone()) + } } diff --git a/modrinth/src/types/version.rs b/modrinth/src/types/version.rs index 02c1c6a..109a0c3 100644 --- a/modrinth/src/types/version.rs +++ b/modrinth/src/types/version.rs @@ -9,152 +9,152 @@ use crate::Loader; /// *The documentation for this struct's fields have* /// *been copied over from [Modrinth's documentation](https://docs.modrinth.com/#tag/version_model)* pub struct ModrinthProjectVersion { - /// The name of this version - pub name: Rc, - /// The version number. Ideally will follow semantic versioning - pub version_number: Rc, - /// The changelog for this version - pub changelog: Option>, - /// A list of specific versions of projects that this version depends on - pub dependencies: Option>, - /// The release channel for this version - pub game_versions: Vec>, - /// A list of versions of Minecraft that this version supports - pub version_type: VersionType, - /// The mod loaders that this version supports - pub loaders: Option>, - /// Whether the version is featured or not - pub featured: bool, - /// The ID of the version, encoded as a base62 Rc - pub id: Rc, - /// The ID of the project this version is for - pub project_id: Rc, - /// The ID of the author who published this version - pub author_id: Rc, - /// The date this version has been published - pub date_published: Rc, - /// The number of times this version has been downloaded - pub downloads: usize, - /// A list of files available for download for this version - pub files: Vec, + /// The name of this version + pub name: Rc, + /// The version number. Ideally will follow semantic versioning + pub version_number: Rc, + /// The changelog for this version + pub changelog: Option>, + /// A list of specific versions of projects that this version depends on + pub dependencies: Option>, + /// The release channel for this version + pub game_versions: Vec>, + /// A list of versions of Minecraft that this version supports + pub version_type: VersionType, + /// The mod loaders that this version supports + pub loaders: Option>, + /// Whether the version is featured or not + pub featured: bool, + /// The ID of the version, encoded as a base62 Rc + pub id: Rc, + /// The ID of the project this version is for + pub project_id: Rc, + /// The ID of the author who published this version + pub author_id: Rc, + /// The date this version has been published + pub date_published: Rc, + /// The number of times this version has been downloaded + pub downloads: usize, + /// A list of files available for download for this version + pub files: Vec, } #[derive(Debug, Deserialize)] #[serde(rename_all = "lowercase")] pub enum VersionType { - Release, - Beta, - Alpha, + Release, + Beta, + Alpha, } #[derive(Debug, Deserialize)] #[serde(untagged)] /// Represents a dependency of a `ModrinthProjectVersion` pub enum VersionDependency { - /// The dependency has yet to be resolved - Unresolved(UnresolvedVersionDependency), - #[serde(skip)] - /// The dependency has been resolved - Resolved(ResolvedVersionDependency), + /// The dependency has yet to be resolved + Unresolved(UnresolvedVersionDependency), + #[serde(skip)] + /// The dependency has been resolved + Resolved(ResolvedVersionDependency), } impl VersionDependency { - pub fn is_resolved(&self) -> bool { - match self { - Self::Resolved(_) => true, - Self::Unresolved(_) => false, - } + pub fn is_resolved(&self) -> bool { + match self { + Self::Resolved(_) => true, + Self::Unresolved(_) => false, } - pub fn is_unresolved(&self) -> bool { - match self { - Self::Resolved(_) => false, - Self::Unresolved(_) => true, - } + } + pub fn is_unresolved(&self) -> bool { + match self { + Self::Resolved(_) => false, + Self::Unresolved(_) => true, } + } } #[derive(Debug, Deserialize)] /// Represents a unresolved dependency of a `ModrinthProjectVersion` pub struct UnresolvedVersionDependency { - /// The version id of the unresolved dependency - pub version_id: Option>, - /// The project id of the unresolved dependency - pub project_id: Option>, - /// The file name of the unresolved dependency - pub file_name: Option>, - /// The requirement type (Required, Optional, etc.) of the unresolved dependency - pub dependency_type: DependencyType, + /// The version id of the unresolved dependency + pub version_id: Option>, + /// The project id of the unresolved dependency + pub project_id: Option>, + /// The file name of the unresolved dependency + pub file_name: Option>, + /// The requirement type (Required, Optional, etc.) of the unresolved dependency + pub dependency_type: DependencyType, } impl super::ModrinthProjectMeta for UnresolvedVersionDependency { - type Id = Rc; + type Id = Rc; - fn project_id(&self) -> Option { - self.project_id.clone() - } + fn project_id(&self) -> Option { + self.project_id.clone() + } - fn version_id(&self) -> Option { - self.version_id.clone() - } + fn version_id(&self) -> Option { + self.version_id.clone() + } } #[derive(Debug)] /// Represents a resolved dependency of a `ModrinthProjectVersion` pub struct ResolvedVersionDependency { - /// the resolved project dependency - pub dependency: ModrinthProjectVersion, - /// The requirement type (Required, Optional, etc.) of the unresolved dependency - pub dependency_type: DependencyType, + /// the resolved project dependency + pub dependency: ModrinthProjectVersion, + /// The requirement type (Required, Optional, etc.) of the unresolved dependency + pub dependency_type: DependencyType, } #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "lowercase")] /// Represents the relationships a dependency can take pub enum DependencyType { - /// Dependency is required for this version - Required, - /// Dependency is optional for this version, - /// no need to download - Optional, - /// Dependency cannot work with this version - Incompatible, - /// Dependency is embedded in this version, - /// no need to download - Embedded, + /// Dependency is required for this version + Required, + /// Dependency is optional for this version, + /// no need to download + Optional, + /// Dependency cannot work with this version + Incompatible, + /// Dependency is embedded in this version, + /// no need to download + Embedded, } #[derive(Debug, Deserialize, PartialEq, Eq)] /// Represents a file listed in the `.files` map pub struct VersionFile { - /// Hashes of the file provided by Modrinth - pub hashes: VersionFileHashes, - /// URL pointing to the resource to download - pub url: Rc, - /// Name of the file - pub filename: Rc, - /// Is the file a primary file - pub primary: bool, - /// Size of the file - pub size: usize, - /// The type of the file - pub file_type: Option, + /// Hashes of the file provided by Modrinth + pub hashes: VersionFileHashes, + /// URL pointing to the resource to download + pub url: Rc, + /// Name of the file + pub filename: Rc, + /// Is the file a primary file + pub primary: bool, + /// Size of the file + pub size: usize, + /// The type of the file + pub file_type: Option, } #[derive(Debug, Deserialize, PartialEq, Eq)] pub struct VersionFileHashes { - /// SHA512 hash of the file - pub sha512: Rc, - /// SHA1 hash of the file - pub sha1: Rc, + /// SHA512 hash of the file + pub sha512: Rc, + /// SHA1 hash of the file + pub sha1: Rc, } #[derive(Debug, Deserialize, PartialEq, Eq)] /// Represents the relationships a non-dependency file can take pub enum VersionFileType { - /// Non-dependency file is required - #[serde(rename = "required-resource-pack")] - Required, - /// Non-dependency file is optional - #[serde(rename = "optional-resource-pack")] - Optional, + /// Non-dependency file is required + #[serde(rename = "required-resource-pack")] + Required, + /// Non-dependency file is optional + #[serde(rename = "optional-resource-pack")] + Optional, } diff --git a/mparse/src/types/forge.rs b/mparse/src/types/forge.rs index 501f2c1..19a715c 100644 --- a/mparse/src/types/forge.rs +++ b/mparse/src/types/forge.rs @@ -3,44 +3,44 @@ use serde::Deserialize; use std::rc::Rc; #[derive(Debug, Deserialize)] pub struct ForgeModpack { - pub minecraft: ModpackLoaderMeta, - pub name: Rc, - pub version: Rc, - pub author: Rc, - pub files: Rc<[ModpackFiles]>, - overrides: Rc, + pub minecraft: ModpackLoaderMeta, + pub name: Rc, + pub version: Rc, + pub author: Rc, + pub files: Rc<[ModpackFiles]>, + overrides: Rc, } impl ModpackProviderMetadata for ForgeModpack { - type Out = Rc; + type Out = Rc; - fn overrides_dir(&self) -> Self::Out { - self.overrides.clone() - } + fn overrides_dir(&self) -> Self::Out { + self.overrides.clone() + } - fn modpack_name(&self) -> Self::Out { - self.name.clone() - } + fn modpack_name(&self) -> Self::Out { + self.name.clone() + } } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ModpackLoaderMeta { - pub version: Rc, - pub mod_loaders: Vec, + pub version: Rc, + pub mod_loaders: Vec, } #[derive(Debug, Deserialize)] pub struct ModpackLoaderVersion { - pub id: Rc, - pub primary: bool, + pub id: Rc, + pub primary: bool, } #[derive(Debug, Deserialize)] pub struct ModpackFiles { - #[serde(rename = "projectID")] - pub project_id: u32, - #[serde(rename = "fileID")] - pub file_id: u32, - pub required: bool, + #[serde(rename = "projectID")] + pub project_id: u32, + #[serde(rename = "fileID")] + pub file_id: u32, + pub required: bool, } diff --git a/mparse/src/types/mod.rs b/mparse/src/types/mod.rs index 4616a71..0a9095f 100644 --- a/mparse/src/types/mod.rs +++ b/mparse/src/types/mod.rs @@ -6,14 +6,14 @@ pub use modrinth::ModrinthModpack; #[derive(PartialEq)] pub enum ModpackProvider { - Forge, - Modrinth, - None, + Forge, + Modrinth, + None, } pub trait ModpackProviderMetadata { - type Out; + type Out; - fn overrides_dir(&self) -> Self::Out; - fn modpack_name(&self) -> Self::Out; + fn overrides_dir(&self) -> Self::Out; + fn modpack_name(&self) -> Self::Out; } diff --git a/mparse/src/types/modrinth.rs b/mparse/src/types/modrinth.rs index 1c9cb37..8e5f9bc 100644 --- a/mparse/src/types/modrinth.rs +++ b/mparse/src/types/modrinth.rs @@ -7,67 +7,67 @@ use serde::{Deserialize, Deserializer}; #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ModrinthModpack { - pub game: Rc, - pub version_id: Rc, - pub name: Rc, - pub summary: Option>, - pub files: Rc<[ModrinthModpackFiles]>, - #[serde(deserialize_with = "deserialize_deps")] - pub dependencies: Rc<[ModrinthModpackDependency]>, + pub game: Rc, + pub version_id: Rc, + pub name: Rc, + pub summary: Option>, + pub files: Rc<[ModrinthModpackFiles]>, + #[serde(deserialize_with = "deserialize_deps")] + pub dependencies: Rc<[ModrinthModpackDependency]>, } impl ModpackProviderMetadata for ModrinthModpack { - type Out = Rc; + type Out = Rc; - fn overrides_dir(&self) -> Self::Out { - "overrides".into() - } + fn overrides_dir(&self) -> Self::Out { + "overrides".into() + } - fn modpack_name(&self) -> Self::Out { - self.name.clone() - } + fn modpack_name(&self) -> Self::Out { + self.name.clone() + } } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ModrinthModpackFiles { - pub path: Box, - pub hashes: ModpackFileHashes, - pub env: Option, - pub downloads: Vec>, - pub file_size: usize, + pub path: Box, + pub hashes: ModpackFileHashes, + pub env: Option, + pub downloads: Vec>, + pub file_size: usize, } #[derive(Debug, Deserialize)] pub struct ModpackFileHashes { - pub sha1: Rc, - pub sha512: Rc, + pub sha1: Rc, + pub sha512: Rc, } #[derive(Debug, Deserialize)] pub struct ModpackEnv { - pub server: Rc, - pub client: Rc, + pub server: Rc, + pub client: Rc, } #[derive(Debug, Deserialize)] pub struct ModrinthModpackDependency { - pub dependency: Rc, - pub version: Rc, + pub dependency: Rc, + pub version: Rc, } fn deserialize_deps<'de, D>(deserializer: D) -> Result, D::Error> where - D: Deserializer<'de>, + D: Deserializer<'de>, { - let raw_map: HashMap, Rc> = HashMap::deserialize(deserializer)?; - let deps = raw_map - .into_iter() - .map(|(dependency, version)| ModrinthModpackDependency { - dependency, - version, - }) - .collect(); + let raw_map: HashMap, Rc> = HashMap::deserialize(deserializer)?; + let deps = raw_map + .into_iter() + .map(|(dependency, version)| ModrinthModpackDependency { + dependency, + version, + }) + .collect(); - Ok(deps) + Ok(deps) } diff --git a/mparse/src/unzip/mod.rs b/mparse/src/unzip/mod.rs index ce90655..13d40ab 100644 --- a/mparse/src/unzip/mod.rs +++ b/mparse/src/unzip/mod.rs @@ -14,91 +14,91 @@ const MODRINTH_META: &str = "modrinth.index.json"; #[derive(Debug, Error)] pub enum UnzipError { - #[error("I/O error: {0}")] - Io(#[from] std::io::Error), - #[error("zip error: {0}")] - Zip(#[from] zip::result::ZipError), + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + #[error("zip error: {0}")] + Zip(#[from] zip::result::ZipError), - #[error("no manifest from modrinth or forge found!")] - NoManifest, + #[error("no manifest from modrinth or forge found!")] + NoManifest, } pub struct ModpackMetadata { - pub loader: ModpackProvider, - pub raw: String, + pub loader: ModpackProvider, + pub raw: String, } pub fn get_modpack_manifest>(file: &F) -> Result { - let zipfile = File::open(file)?; - let mut archive = ZipArchive::new(zipfile)?; - - let (manifest_file, loader) = if archive.by_name(FORGE_META).is_ok() { - info!("Modpack manifest found at {}", FORGE_META); - (FORGE_META, ModpackProvider::Forge) - } else if archive.by_name(MODRINTH_META).is_ok() { - info!("Modpack manifest found at {}", MODRINTH_META); - (MODRINTH_META, ModpackProvider::Modrinth) - } else { - error!("No manifest found!"); - ("", ModpackProvider::None) - }; - - if loader == ModpackProvider::None { - return Err(UnzipError::NoManifest); - } - - let mut file = archive.by_name(manifest_file).expect("expected that by here, modpack provider should be either forge or modrinth, so this should not appear at all"); - - info!("Reading manifest file at {}", manifest_file); - let mut raw = String::new(); - let len = file.read_to_string(&mut raw)?; - debug!("Read {} bytes to buffer", len); - - Ok(ModpackMetadata { loader, raw }) + let zipfile = File::open(file)?; + let mut archive = ZipArchive::new(zipfile)?; + + let (manifest_file, loader) = if archive.by_name(FORGE_META).is_ok() { + info!("Modpack manifest found at {}", FORGE_META); + (FORGE_META, ModpackProvider::Forge) + } else if archive.by_name(MODRINTH_META).is_ok() { + info!("Modpack manifest found at {}", MODRINTH_META); + (MODRINTH_META, ModpackProvider::Modrinth) + } else { + error!("No manifest found!"); + ("", ModpackProvider::None) + }; + + if loader == ModpackProvider::None { + return Err(UnzipError::NoManifest); + } + + let mut file = archive.by_name(manifest_file).expect("expected that by here, modpack provider should be either forge or modrinth, so this should not appear at all"); + + info!("Reading manifest file at {}", manifest_file); + let mut raw = String::new(); + let len = file.read_to_string(&mut raw)?; + debug!("Read {} bytes to buffer", len); + + Ok(ModpackMetadata { loader, raw }) } pub fn unzip_modpack_to(zipfile: Fz, dir: &Fd, manifest: &M) -> Result<(), UnzipError> where - Fz: AsRef, - Fd: AsRef, - M: ModpackProviderMetadata, - ::Out: ToString, + Fz: AsRef, + Fd: AsRef, + M: ModpackProviderMetadata, + ::Out: ToString, { - let zipfile = File::open(zipfile)?; - let mut archive = ZipArchive::new(zipfile)?; - let overrides_dir = manifest.overrides_dir(); - - info!("Extracting archive"); - for i in 0..archive.len() { - let mut infile = archive.by_index(i)?; - let arcfile = infile - .enclosed_name() - .unwrap() - .components() - .enumerate() - .filter_map(|(i, comp)| if i != 0 { Some(comp) } else { None }) - .collect::(); - - let outpath = absolute(dir.as_ref().join(&arcfile))?; - - if !infile.name().starts_with(&overrides_dir.to_string()) { - continue; - } - - if infile.is_dir() { - info!("Creating dir: {}", outpath.display()); - create_dir_all(outpath)?; - } else { - info!("Extracting {} to {}", infile.name(), outpath.display()); - if !outpath.parent().unwrap().exists() { - create_dir_all(outpath.parent().unwrap())?; - } - - let mut outfile = File::create(outpath).unwrap(); - std::io::copy(&mut infile, &mut outfile)?; - debug!("Extracted {}!", arcfile.display()); - } + let zipfile = File::open(zipfile)?; + let mut archive = ZipArchive::new(zipfile)?; + let overrides_dir = manifest.overrides_dir(); + + info!("Extracting archive"); + for i in 0..archive.len() { + let mut infile = archive.by_index(i)?; + let arcfile = infile + .enclosed_name() + .unwrap() + .components() + .enumerate() + .filter_map(|(i, comp)| if i != 0 { Some(comp) } else { None }) + .collect::(); + + let outpath = absolute(dir.as_ref().join(&arcfile))?; + + if !infile.name().starts_with(&overrides_dir.to_string()) { + continue; + } + + if infile.is_dir() { + info!("Creating dir: {}", outpath.display()); + create_dir_all(outpath)?; + } else { + info!("Extracting {} to {}", infile.name(), outpath.display()); + if !outpath.parent().unwrap().exists() { + create_dir_all(outpath.parent().unwrap())?; + } + + let mut outfile = File::create(outpath).unwrap(); + std::io::copy(&mut infile, &mut outfile)?; + debug!("Extracted {}!", arcfile.display()); } + } - Ok(()) + Ok(()) }