From 96cc5257de09682df345e768dc2a91303f9b36c9 Mon Sep 17 00:00:00 2001 From: fxqnlr Date: Thu, 3 Nov 2022 21:34:04 +0100 Subject: added update beginnings; init of tests --- config.ini | 1 + data.db | Bin 36864 -> 36864 bytes src/apis/modrinth.rs | 18 ++--- src/commands/add.rs | 52 ------------ src/commands/list.rs | 16 +++- src/commands/mod.rs | 6 +- src/commands/modification.rs | 88 +++++++++++++++++++++ src/commands/update.rs | 40 ++++++++++ src/config.rs | 3 +- src/db.rs | 183 +++++++++++++++++++++++++++++++++++++++---- src/input.rs | 12 +-- src/lib.rs | 22 +++++- tests/db/mod.rs | 5 ++ tests/db_integration.rs | 7 ++ 14 files changed, 363 insertions(+), 90 deletions(-) delete mode 100644 src/commands/add.rs create mode 100644 src/commands/modification.rs create mode 100644 src/commands/update.rs create mode 100644 tests/db/mod.rs create mode 100644 tests/db_integration.rs diff --git a/config.ini b/config.ini index ce33108..0ffe0ac 100644 --- a/config.ini +++ b/config.ini @@ -1,4 +1,5 @@ data = "./" +clean_remove = false [apis] modrinth = "http://localhost:8080/" diff --git a/data.db b/data.db index 60431f1..73464d8 100644 Binary files a/data.db and b/data.db differ diff --git a/src/apis/modrinth.rs b/src/apis/modrinth.rs index 3af5bbd..0c3eca5 100644 --- a/src/apis/modrinth.rs +++ b/src/apis/modrinth.rs @@ -1,6 +1,6 @@ use serde::Deserialize; -use crate::Modloader; +use crate::{Modloader, List}; #[derive(Debug, Deserialize)] pub struct Project { @@ -68,7 +68,7 @@ pub enum Status { unknown } -#[derive(Debug, Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct Version { pub name: String, pub version_number: String, @@ -86,14 +86,14 @@ pub struct Version { } #[allow(non_camel_case_types)] -#[derive(Debug, Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub enum VersionType { release, beta, alpha } -#[derive(Debug, Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct VersionFile { pub hashes: Hash, pub url: String, @@ -102,7 +102,7 @@ pub struct VersionFile { pub size: u32, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct Hash { pub sha512: String, pub sha1: String, @@ -130,7 +130,7 @@ pub async fn project(api: String, name: &str) -> Project { serde_json::from_slice(&data.await.unwrap()).unwrap() } -pub async fn projects(api: String, ids: Vec<&str>) -> Vec { +pub async fn projects(api: String, ids: Vec) -> Vec { let all = ids.join(r#"",""#); let url = format!(r#"projects?ids=["{}"]"#, all); println!("{}", url); @@ -140,14 +140,14 @@ pub async fn projects(api: String, ids: Vec<&str>) -> Vec { serde_json::from_slice(&data.await.unwrap()).unwrap() } -pub async fn versions(api: String, id: String, loader: Modloader, mc_version: String) -> Vec { +pub async fn versions(api: String, id: String, list: List) -> Vec { - let loaderstr = match loader { + let loaderstr = match list.modloader { Modloader::Forge => String::from("forge"), Modloader::Fabric => String::from("fabric"), }; - let url = format!(r#"project/{}/version?loaders=["{}"]&game_versions=["{}"]"#, id, loaderstr, mc_version); + let url = format!(r#"project/{}/version?loaders=["{}"]&game_versions=["{}"]"#, id, loaderstr, list.mc_version); let data = get(api, url); diff --git a/src/commands/add.rs b/src/commands/add.rs deleted file mode 100644 index ed4a6d8..0000000 --- a/src/commands/add.rs +++ /dev/null @@ -1,52 +0,0 @@ -use std::io::{Error, ErrorKind}; - -use crate::{modrinth::{project, versions}, config::Cfg, db::insert_mod, Modloader, input::Input}; - -pub async fn modification(config: Cfg, args: Option>) -> Result<(), Box> { - - if args.is_none() { return Err(Box::new(Error::new(ErrorKind::InvalidInput, "TOO_FEW_ARGUMENTS"))) } - - let arguments = Input::from(args.unwrap().join(" "))?; - - if arguments.args.is_none() { return Err(Box::new(Error::new(ErrorKind::InvalidInput, "TOO_FEW_ARGUMENTS"))); }; - - match arguments.command.as_str() { - "add" => { - add(config, arguments.args.unwrap()).await - }, - _ => Err(Box::new(Error::new(ErrorKind::InvalidInput, "UNKNOWN_SUBCOMMAND"))) - } -} - -pub async fn add(config: Cfg, args: Vec) -> Result<(), Box> { - - if args.len() < 1 { return Err(Box::new(Error::new(ErrorKind::InvalidInput, "TOO_FEW_ARGUMENTS"))); }; - - let project = project(String::from(&config.apis.modrinth), &args[0]).await; - - dbg!(&project); - - let loader = Modloader::Fabric; - - if project.versions.is_empty() { panic!("This should never happen"); }; - - let current_version = get_current(config, String::from(&project.id)).await?; - - //add to current list and mod table - match insert_mod(project.id, project.title, current_version, project.versions, loader, String::from("1.19.2")) { - Err(err) => { Err(Box::new(err)) }, - Ok(()) => Ok(()), - } - -} - -async fn get_current(config: Cfg, id: String) -> Result> { - let available_versions = versions(config.apis.modrinth, id, Modloader::Fabric, String::from("1.19.2")).await; - - match available_versions.len() { - 0 => Err(Box::new(Error::new(ErrorKind::NotFound, "NO_VERSIONS_AVAILABLE"))), - //TODO compare publish dates - 1.. => Ok(available_versions[0].id.to_string()), - _ => panic!("available_versions should never be negative"), - } -} diff --git a/src/commands/list.rs b/src/commands/list.rs index 6c260ce..3dfe1ad 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -1,12 +1,19 @@ use std::io::{Error, ErrorKind}; -use crate::{db::{insert_list, remove_list, change_list, get_lists, get_current_list}, Modloader, config::Cfg, input::Input}; +use crate::{db::{insert_list, remove_list, change_list, get_lists, get_current_list_id, get_list}, Modloader, config::Cfg, input::Input}; + +#[derive(Clone)] +pub struct List { + pub id: String, + pub mc_version: String, + pub modloader: Modloader, +} pub fn list(config: Cfg, args: Option>) -> Result<(), Box> { if args.is_none() { let lists = get_lists(config.clone())?; - let current_list = get_current_list(config)?; + let current_list = get_current_list_id(config)?; println!("Your lists:\n{}\n-----\nCurrently selected list: \"{}\"", lists.join(",\n"), current_list); return Ok(()); } @@ -29,6 +36,11 @@ pub fn list(config: Cfg, args: Option>) -> Result<(), Box Result> { + let id = get_current_list_id(config.clone())?; + get_list(config, id) +} + fn add(config: Cfg, args: Vec) -> Result<(), Box> { match args.len() { 1 | 2 => Err(Box::new(Error::new(ErrorKind::InvalidInput, "TOO_FEW_ARGUMENTS"))), diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 6432746..5d008fd 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,5 +1,7 @@ -pub mod add; +pub mod modification; pub mod list; +pub mod update; -pub use add::*; +pub use modification::*; pub use list::*; +pub use update::*; diff --git a/src/commands/modification.rs b/src/commands/modification.rs new file mode 100644 index 0000000..43e2180 --- /dev/null +++ b/src/commands/modification.rs @@ -0,0 +1,88 @@ +use std::io::{Error, ErrorKind}; + +use crate::{modrinth::{project, versions, Version}, config::Cfg, db::{insert_mod, remove_mod_from_list, get_mod_id, insert_mod_in_list, get_mods, get_mods_from_list}, input::Input, get_current_list}; + +pub async fn modification(config: Cfg, args: Option>) -> Result<(), Box> { + + if args.is_none() { return Err(Box::new(Error::new(ErrorKind::InvalidInput, "TOO_FEW_ARGUMENTS"))) } + + let arguments = Input::from(args.unwrap().join(" "))?; + + if arguments.args.is_none() { return Err(Box::new(Error::new(ErrorKind::InvalidInput, "TOO_FEW_ARGUMENTS"))); }; + + match arguments.command.as_str() { + "add" => { + add(config, arguments.args.unwrap()).await + }, + "remove" => { + remove(config, arguments.args.unwrap()) + }, + _ => Err(Box::new(Error::new(ErrorKind::InvalidInput, "UNKNOWN_SUBCOMMAND"))) + } +} + +async fn add(config: Cfg, args: Vec) -> Result<(), Box> { + + if args.is_empty() { return Err(Box::new(Error::new(ErrorKind::InvalidInput, "TOO_FEW_ARGUMENTS"))); }; + + let current_list = get_current_list(config.clone())?; + + let project = project(String::from(&config.apis.modrinth), &args[0]).await; + + dbg!(&project); + + if project.versions.is_empty() { panic!("This should never happen"); }; + + let available_versions = versions(String::from(&config.apis.modrinth), String::from(&project.id), current_list.clone()).await; + + let current_version = extract_current_version(available_versions.clone())?; + + //add to current list and mod table + match get_mods_from_list(config.clone(), current_list.clone()) { + Ok(mods) => { + dbg!(&mods); + if mods.contains(&project.id) { + return Err(Box::new(Error::new(ErrorKind::Other, "MOD_ALREADY_ON_LIST"))); } + else { + insert_mod_in_list(config.clone(), current_list.clone(), String::from(&project.id), current_version, available_versions)?; + } + }, + Err(..) => insert_mod_in_list(config.clone(), current_list, String::from(&project.id), current_version, available_versions)?, + }; + + match get_mods(config.clone()) { + Ok(mods) => { + dbg!(&mods); + if mods.contains(&project.id) { + return Err(Box::new(Error::new(ErrorKind::Other, "MOD_ALREADY_IN_DATABASE"))) + } else { + insert_mod(config.clone(), String::from(&project.id), String::from(&project.title), project.versions)?; + } + }, + Err(..) => insert_mod(config.clone(), String::from(&project.id), String::from(&project.title), project.versions)?, + }; + + Ok(()) +} + +fn remove(config: Cfg, args: Vec) -> Result<(), Box> { + if args.is_empty() { return Err(Box::new(Error::new(ErrorKind::InvalidInput, "TOO_FEW_ARGUMENTS"))); }; + + let current_list = get_current_list(config.clone())?; + let mod_id = get_mod_id(config.clone(), String::from(&args[0]))?; + + //TODO implement remove from modlist if not in any other lists && config clean is true + match remove_mod_from_list(config, current_list, mod_id) { + Err(err) => { Err(Box::new(err)) }, + Ok(()) => Ok(()), + } +} + +fn extract_current_version(versions: Vec) -> Result> { + match versions.len() { + 0 => Err(Box::new(Error::new(ErrorKind::NotFound, "NO_VERSIONS_AVAILABLE"))), + //TODO compare publish dates + 1.. => Ok(versions[0].id.to_string()), + _ => panic!("available_versions should never be negative"), + } +} diff --git a/src/commands/update.rs b/src/commands/update.rs new file mode 100644 index 0000000..14c37ec --- /dev/null +++ b/src/commands/update.rs @@ -0,0 +1,40 @@ +use std::io::{Error, ErrorKind}; + +use crate::{config::Cfg, modrinth::projects, get_current_list, db::{get_mods_from_list, get_versions}}; + +pub async fn update(config: Cfg) -> Result<(), Box> { + + let current_list = get_current_list(config.clone())?; + + let mods = get_mods_from_list(config.clone(), current_list)?; + + let mut projects = projects(String::from(&config.apis.modrinth), mods.clone()).await; + + let mut versions = get_versions(config, mods)?; + + projects.sort_by_key(|p| p.id.clone()); + + versions.sort_by_key(|v| v.mod_id.clone()); + + let mut update_stack: Vec = vec![]; + + for (index, project) in projects.iter().enumerate() { + + let cmp_version = &versions[index]; + + let p_id = &project.id; + let v_id = &cmp_version.mod_id; + + if p_id != v_id { return Err(Box::new(Error::new(ErrorKind::Other, "COMPARE_SORTING_ERR"))); }; + println!("{}:{}", p_id, v_id); + + if project.versions.join("|") != cmp_version.versions { + update_stack.push(String::from(&project.id)); + }; + }; + + //TODO UPDATE + dbg!(update_stack); + + Ok(()) +} diff --git a/src/config.rs b/src/config.rs index ba1b46a..58d399a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,9 +1,10 @@ use config::{Config, File, FileFormat}; use serde::Deserialize; -#[derive(Debug, Clone,Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct Cfg { pub data: String, + pub clean_remove: bool, pub apis: Apis, } diff --git a/src/db.rs b/src/db.rs index bbbca87..33d8344 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,36 +1,164 @@ use std::io::ErrorKind; -use crate::{Modloader, config::Cfg}; +use crate::{Modloader, config::Cfg, List, modrinth::Version, get_modloader}; //TODO use prepared statements -pub fn insert_mod(id: String, name: String, current_version: String, old_versions: Vec, mod_loader: Modloader, desired_mc_version: String) -> Result<(), sqlite::Error> { +//MODS +pub fn insert_mod(config: Cfg, id: String, name: String, versions: Vec) -> Result<(), sqlite::Error> { - let connection = sqlite::open("./data.db").unwrap(); + println!("Inserting into modlist"); - let loader = match mod_loader { - Modloader::Fabric => "fabric", - Modloader::Forge => "forge", - }; + let data = format!("{}/data.db", config.data); + let connection = sqlite::open(data).unwrap(); + + let sql = format!("INSERT INTO mods VALUES ('{}', '{}', '{}')", id, name, versions.join("|")); + + connection.execute(sql) +} + +pub fn insert_mod_in_list(config: Cfg, list: List, id: String, current_version: String, applicable_versions: Vec) -> Result<(), sqlite::Error> { + + println!("Inserting into current list"); + + let data = format!("{}/data.db", config.data); + let connection = sqlite::open(data).unwrap(); + + let mut applicable_versions_vec = vec![]; + + for ver in applicable_versions { + applicable_versions_vec.push(ver.id); + } + + let sql = format!("INSERT INTO {} VALUES ('{}', '{}', '{}')", list.id, id, current_version, applicable_versions_vec.join("|")); + + connection.execute(sql) +} + +pub fn remove_mod_from_list(config: Cfg, list: List, mod_id: String) -> Result<(), sqlite::Error> { + let data = format!("{}/data.db", config.data); + let connection = sqlite::open(data).unwrap(); + + let sql = format!("DELETE FROM {} WHERE mod_id = '{}'", list.id, mod_id); - let sql = format!("INSERT INTO mods VALUES ('{}', '{}', '{}', '{}', '{}', '{}')", id, name, current_version, old_versions.join("|"), loader, desired_mc_version); + dbg!(&sql); connection.execute(sql) } -//LIST -pub fn insert_list(config: Cfg, id: String, mc_version: String, mod_loader: Modloader) -> Result<(), sqlite::Error> { +pub fn get_mods(config: Cfg) -> Result, Box> { + let data = format!("{}/data.db", config.data); let connection = sqlite::open(data).unwrap(); + + let sql = "SELECT id FROM mods"; - //Setup list in table - let loader = match mod_loader { - Modloader::Fabric => "fabric", - Modloader::Forge => "forge", + let mut mods: Vec = Vec::new(); + //TODO catch sql errors better + connection.iterate(sql, |ids| { + if ids.is_empty() { return false; }; + for &(_column, value) in ids.iter() { + mods.push(String::from(value.unwrap())); + } + true + }).unwrap(); + match mods.is_empty() { + true => Err(Box::new(std::io::Error::new(ErrorKind::NotFound, "NO_MODS"))), + false => Ok(mods), + } +} + +pub fn get_mods_from_list(config: Cfg, list: List) -> Result, Box> { + let data = format!("{}/data.db", config.data); + let connection = sqlite::open(data).unwrap(); + + let sql = format!("SELECT mod_id FROM {}", list.id); + + let mut mods: Vec = Vec::new(); + //TODO catch sql errors better + connection.iterate(sql, |ids| { + if ids.is_empty() { return false; }; + for &(_column, value) in ids.iter() { + mods.push(String::from(value.unwrap())); + } + true + }).unwrap(); + match mods.is_empty() { + true => Err(Box::new(std::io::Error::new(ErrorKind::NotFound, "NO_MODS"))), + false => Ok(mods), + } +} + +pub fn get_mod_id(config: Cfg, name: String) -> Result> { + let data = format!("{}/data.db", config.data); + let connection = sqlite::open(data).unwrap(); + + let sql = format!("SELECT id FROM mods WHERE name = '{}'", name); + + dbg!(&sql); + + let mut modification = String::new(); + //TODO catch sql errors better + connection.iterate(sql, |id| { + if id.is_empty() { return false; }; + for &(_column, value) in id.iter() { + dbg!(&(_column, value)); + modification = String::from(value.unwrap()); + } + true + }).unwrap(); + + dbg!(&modification); + + if modification.is_empty() { return Err(Box::new(std::io::Error::new(ErrorKind::NotFound, "MOD_NOT_IN_DATABASE"))) }; + + Ok(modification) +} + +#[derive(Debug, Clone)] +pub struct DBModlistVersions { + pub mod_id: String, + pub versions: String, +} + +pub fn get_versions(config: Cfg, mods: Vec) -> Result, Box> { + let data = format!("{}/data.db", config.data); + let connection = sqlite::open(data).unwrap(); + + let mut wherestr = String::from("WHERE"); + for (i, id) in mods.iter().enumerate() { + let mut or = " OR"; + if i == mods.len() - 1 { or = "" } + println!("Pushing {}({}) | OR: '{}'", id, i, or); + wherestr = format!("{} id = '{}'{}", wherestr, id, or); + } + + let sql = format!("SELECT id, versions FROM mods {}", wherestr); + + dbg!(&sql); + + let mut versionmaps: Vec = Vec::new(); + //TODO catch sql errors better + let mut cursor = connection.prepare(sql).unwrap().into_cursor(); + + while let Some(Ok(row)) = cursor.next() { + println!("{}: {}", row.get::(0), row.get::(1)); + versionmaps.push(DBModlistVersions { mod_id: row.get::(0), versions: row.get::(1) }) }; - let sql_list = format!("INSERT INTO lists VALUES ('{}', '{}', '{}')", id, mc_version, loader); - let sql_table = format!("CREATE TABLE '{}' ( 'mod_id' TEXT, 'current_version' TEXT, 'applicable_versions' BLOB, 'mod_loader' TEXT )", id); + if versionmaps.is_empty() { return Err(Box::new(std::io::Error::new(ErrorKind::Other, "NO_MODS_ON_LIST"))); }; + + Ok(versionmaps) +} + + +//LIST +pub fn insert_list(config: Cfg, id: String, mc_version: String, mod_loader: Modloader) -> Result<(), sqlite::Error> { + let data = format!("{}/data.db", config.data); + let connection = sqlite::open(data).unwrap(); + + let sql_list = format!("INSERT INTO lists VALUES ('{}', '{}', '{}')", id, mc_version, mod_loader.stringify()); + let sql_table = format!("CREATE TABLE '{}' ( 'mod_id' TEXT, 'current_version' TEXT, 'applicable_versions' BLOB)", id); let sql = format!("{};{};", sql_list, sql_table); connection.execute(sql) @@ -68,6 +196,27 @@ pub fn get_lists(config: Cfg) -> Result, Box> } } +pub fn get_list(config: Cfg, id: String) -> Result> { + let data = format!("{}/data.db", config.data); + let connection = sqlite::open(data).unwrap(); + + let sql = format!("SELECT mc_version, modloader FROM lists WHERE id = '{}'", id); + + let mut list = vec![]; + //TODO catch sql errors better + connection.iterate(sql, |ids| { + if ids.is_empty() { return false; }; + for &(_column, value) in ids.iter() { + list.push(String::from(value.unwrap())); + } + true + }).unwrap(); + + if list.len() != 2 { return Err(Box::new(std::io::Error::new(ErrorKind::InvalidData, "LIST_MISSING_DATA"))) }; + + Ok(List { id, mc_version: String::from(&list[0]), modloader: get_modloader(String::from(&list[1]))? }) +} + //config pub fn change_list(config: Cfg, id: String) -> Result<(), sqlite::Error> { let data = format!("{}/data.db", config.data); @@ -78,7 +227,7 @@ pub fn change_list(config: Cfg, id: String) -> Result<(), sqlite::Error> { connection.execute(sql) } -pub fn get_current_list(config: Cfg) -> Result> { +pub fn get_current_list_id(config: Cfg) -> Result> { let data = format!("{}/data.db", config.data); let connection = sqlite::open(data).unwrap(); diff --git a/src/input.rs b/src/input.rs index 061f1fd..0c13e67 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,5 +1,5 @@ use std::io::{stdin, Error, ErrorKind}; -use crate::{add, config::Cfg, list}; +use crate::{config::Cfg, list, modification, update}; pub struct Input { pub command: String, @@ -41,15 +41,15 @@ pub async fn get_input(config: Cfg) -> Result<(), Box> { let input = Input::from(user_input.trim().to_string())?; match input.command.as_str() { - "add" => { - if input.args == None { return Err(Box::new(Error::new(ErrorKind::InvalidInput, "TOO_FEW_ARGUMENTS"))) }; - if input.args.as_ref().unwrap().len() != 1 { return Err(Box::new(Error::new(ErrorKind::InvalidInput, "TOO_MANY_ARGUMENTS"))) }; - add(config, input.args.unwrap()[0].to_string()).await?; - Ok(()) + "mod" => { + modification(config, input.args).await }, "list" => { list(config, input.args) }, + "update" => { + update(config).await + }, _ => Err(Box::new(Error::new(ErrorKind::InvalidInput, "UNKNOWN_COMMAND"))), } } diff --git a/src/lib.rs b/src/lib.rs index 4ad7c39..e059293 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,11 +4,31 @@ pub mod commands; pub mod input; pub mod db; +use std::io::{Error, ErrorKind}; + pub use apis::*; pub use commands::*; -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum Modloader { Fabric, Forge } + +impl Modloader { + fn stringify(&self) -> String { + match self { + Modloader::Fabric => String::from("fabric"), + Modloader::Forge => String::from("forge"), + } + } + +} + +pub fn get_modloader(string: String) -> Result> { + match string.as_str() { + "forge" => Ok(Modloader::Forge), + "fabric" => Ok(Modloader::Fabric), + _ => Err(Box::new(Error::new(ErrorKind::InvalidData, "UNKNOWN_MODLOADER"))) + } +} diff --git a/tests/db/mod.rs b/tests/db/mod.rs new file mode 100644 index 0000000..b5aed75 --- /dev/null +++ b/tests/db/mod.rs @@ -0,0 +1,5 @@ +use std::fs::File; + +pub fn setup() { + File::create("./setuptests").unwrap(); +} diff --git a/tests/db_integration.rs b/tests/db_integration.rs new file mode 100644 index 0000000..82cfe0f --- /dev/null +++ b/tests/db_integration.rs @@ -0,0 +1,7 @@ +mod db; + +#[test] +fn test_add() { + db::setup(); + assert!(true); +} -- cgit v1.2.3