diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 0000000..3af847b --- /dev/null +++ b/src/api.rs @@ -0,0 +1,118 @@ +use console::style; +use serde::Deserialize; +use std::collections::HashMap; + +#[derive(Deserialize, Debug)] +pub struct SearchResponse { + pub hits: Vec, + pub offset: isize, + pub limit: isize, + pub total_hits: isize, +} + +#[derive(Deserialize, Debug)] +pub struct ModResult { + pub mod_id: String, // TODO parse to `local-xxxxx` with regex + pub project_type: Option, // NOTE this isn't in all search results? + pub author: String, + pub title: String, + pub description: String, + pub categories: Vec, + pub versions: Vec, + pub downloads: isize, + pub page_url: String, + pub icon_url: String, + pub author_url: String, + pub date_created: String, + pub date_modified: String, + pub latest_version: String, + pub license: String, + pub client_side: String, + pub server_side: String, + pub host: String, +} + +impl ModResult { + pub fn format_info(&self) -> String { + let title = style(self.title.clone()).bold(); + let downloads = style(self.downloads.clone()).bold().green(); + if let Some(latest_release) = self.versions.last() { + // TODO fetch version numbers to display + let latest_release = style(latest_release).bold().blue(); + format!("{} [{}] ({} downloads)", title, latest_release, downloads) + } else { + format!("{} [no releases]", title) + } + } + + pub fn format_description(&self) -> String { + self.description.to_owned() + } + + pub fn display(&self, index: usize) { + let index = style(index).magenta(); + let info = self.format_info(); + let description = self.format_description(); + println!("{:>2} {}\n {}", index, info, description); + } +} + +#[derive(Deserialize, Debug)] +pub struct ModInfo { + pub id: String, // TODO serialize mod id? + pub slug: String, + pub team: String, // TODO serialize team id? + pub title: String, + pub description: String, + pub body: String, + pub published: String, // TODO serialize datetime + pub updated: String, // TODO serialize datetime + pub status: String, + pub license: License, + pub client_side: String, // TODO serialize as enum + pub server_side: String, // TODO serialize as enum + pub downloads: isize, + pub followers: isize, + pub categories: Vec, + pub versions: Vec, + pub icon_url: Option, + pub issues_url: Option, + pub source_url: Option, + pub wiki_url: Option, + pub discord_url: Option, + pub donation_urls: Vec, +} + +#[derive(Deserialize, Debug)] +pub struct License { + pub id: String, + pub name: String, + pub url: String, +} + +#[derive(Deserialize, Debug)] +pub struct ModVersion { + pub id: String, // version id + pub mod_id: String, // mod id + pub author_id: String, // user id + // NOTE modrinth docs list this as a String, but is actually a bool? + // featured: String, // user id + pub name: String, + pub version_number: String, + pub changelog: Option, + pub changelog_url: Option, + pub date_published: String, // TODO serialize datetime + pub downloads: isize, + pub version_type: String, // TODO {alpha | beta | release} + pub files: Vec, + pub dependencies: Vec, // TODO dependency wrangling, thank you modrinth, very cool + pub game_versions: Vec, + pub loaders: Vec, +} + +#[derive(Deserialize, Debug)] +pub struct ModVersionFile { + pub hashes: HashMap, + pub url: String, + pub filename: String, +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..d715e8c --- /dev/null +++ b/src/config.rs @@ -0,0 +1,102 @@ +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use structopt::StructOpt; + +// TODO parameter to restrict target Minecraft version +#[derive(StructOpt, Clone, Debug)] +pub struct SearchArgs { + pub package_name: String, + + /// Restricts the target Minecraft version + #[structopt(short, long)] + pub version: Option>, +} + +// TODO use ColoredHelp by default? +#[derive(StructOpt, Clone, Debug)] +pub enum Command { + /// Adds a mod to the current instance + #[structopt(setting = structopt::clap::AppSettings::ColoredHelp)] + Add(SearchArgs), + /// Removes a mod + #[structopt(setting = structopt::clap::AppSettings::ColoredHelp)] + Remove { package_name: String }, + #[structopt(setting = structopt::clap::AppSettings::ColoredHelp)] + Get(SearchArgs), + #[structopt(setting = structopt::clap::AppSettings::ColoredHelp)] + Update, + #[structopt(setting = structopt::clap::AppSettings::ColoredHelp)] + Clean, +} + +// TODO move main body argument fields to substruct for ease of moving? +#[derive(StructOpt, Clone, Debug)] +#[structopt(name = "hopper", setting = structopt::clap::AppSettings::ColoredHelp)] +pub struct Args { + /// Path to configuration file + #[structopt(short, long, parse(from_os_str))] + pub config: Option, + + /// Path to mod lockfile + #[structopt(short, long, parse(from_os_str))] + pub lockfile: Option, + + /// Auto-accept confirmation dialogues + #[structopt(short = "y", long = "yes")] + pub auto_accept: bool, + + #[structopt(subcommand)] + pub command: Command, +} + +impl Args { + pub fn load_config(&self) -> Result { + if let Some(config_path) = &self.config { + confy::load_path(config_path) + } else { + confy::load("hopper") + } + } +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct Upstream { + /// Modrinth main server address + pub server_address: String, +} + +impl Default for Upstream { + fn default() -> Self { + Self { + server_address: "api.modrinth.com".into(), + } + } +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct Options { + /// Whether to reverse search results + pub reverse_search: bool, +} + +impl Default for Options { + fn default() -> Self { + Self { + reverse_search: true, + } + } +} + +#[derive(Deserialize, Serialize, Debug, Default)] +pub struct Config { + /// General settings + pub options: Options, + + /// Configuration for the upstream Modrinth server + pub upstream: Upstream, +} + +pub struct AppContext { + pub args: Args, + pub config: Config, +} diff --git a/src/main.rs b/src/main.rs index 679de8f..8b91853 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,226 +1,14 @@ -use console::style; -use log::*; use futures_util::StreamExt; -use serde::{Deserialize, Serialize}; +use log::*; use std::cmp::min; -use std::collections::HashMap; use std::io::Write; -use std::path::PathBuf; use structopt::StructOpt; -// TODO parameter to restrict target Minecraft version -#[derive(StructOpt, Clone, Debug)] -struct SearchArgs { - package_name: String, +mod api; +mod config; - /// Restricts the target Minecraft version - #[structopt(short, long)] - version: Option>, -} - -// TODO use ColoredHelp by default? -#[derive(StructOpt, Clone, Debug)] -enum Command { - /// Adds a mod to the current instance - #[structopt(setting = structopt::clap::AppSettings::ColoredHelp)] - Add(SearchArgs), - /// Removes a mod - #[structopt(setting = structopt::clap::AppSettings::ColoredHelp)] - Remove { package_name: String }, - #[structopt(setting = structopt::clap::AppSettings::ColoredHelp)] - Get(SearchArgs), - #[structopt(setting = structopt::clap::AppSettings::ColoredHelp)] - Update, - #[structopt(setting = structopt::clap::AppSettings::ColoredHelp)] - Clean, -} - -// TODO move main body argument fields to substruct for ease of moving? -#[derive(StructOpt, Clone, Debug)] -#[structopt(name = "hopper", setting = structopt::clap::AppSettings::ColoredHelp)] -struct Args { - /// Path to configuration file - #[structopt(short, long, parse(from_os_str))] - config: Option, - - /// Path to mod lockfile - #[structopt(short, long, parse(from_os_str))] - lockfile: Option, - - /// Auto-accept confirmation dialogues - #[structopt(short = "y", long = "yes")] - auto_accept: bool, - - #[structopt(subcommand)] - command: Command, -} - -impl Args { - fn load_config(&self) -> Result { - if let Some(config_path) = &self.config { - confy::load_path(config_path) - } else { - confy::load("hopper") - } - } -} - -#[derive(Deserialize, Serialize, Debug)] -struct Upstream { - /// Modrinth main server address - server_address: String, -} - -impl Default for Upstream { - fn default() -> Self { - Self { - server_address: "api.modrinth.com".into(), - } - } -} - -#[derive(Deserialize, Serialize, Debug)] -struct Options { - /// Whether to reverse search results - reverse_search: bool, -} - -impl Default for Options { - fn default() -> Self { - Self { - reverse_search: true, - } - } -} - -#[derive(Deserialize, Serialize, Debug, Default)] -struct Config { - /// General settings - options: Options, - - /// Configuration for the upstream Modrinth server - upstream: Upstream, -} - -struct AppContext { - pub args: Args, - pub config: Config, -} - -#[derive(Deserialize, Debug)] -struct SearchResponse { - hits: Vec, - offset: isize, - limit: isize, - total_hits: isize, -} - -#[derive(Deserialize, Debug)] -struct ModResult { - mod_id: String, // TODO parse to `local-xxxxx` with regex - project_type: Option, // NOTE this isn't in all search results? - author: String, - title: String, - description: String, - categories: Vec, - versions: Vec, - downloads: isize, - page_url: String, - icon_url: String, - author_url: String, - date_created: String, - date_modified: String, - latest_version: String, - license: String, - client_side: String, - server_side: String, - host: String, -} - -impl ModResult { - fn format_info(&self) -> String { - let title = style(self.title.clone()).bold(); - let downloads = style(self.downloads.clone()).bold().green(); - if let Some(latest_release) = self.versions.last() { - // TODO fetch version numbers to display - let latest_release = style(latest_release).bold().blue(); - format!("{} [{}] ({} downloads)", title, latest_release, downloads) - } else { - format!("{} [no releases]", title) - } - } - - fn format_description(&self) -> String { - self.description.to_owned() - } - - fn display(&self, index: usize) { - let index = style(index).magenta(); - let info = self.format_info(); - let description = self.format_description(); - println!("{:>2} {}\n {}", index, info, description); - } -} - -#[derive(Deserialize, Debug)] -struct ModInfo { - id: String, // TODO serialize mod id? - slug: String, - team: String, // TODO serialize team id? - title: String, - description: String, - body: String, - published: String, // TODO serialize datetime - updated: String, // TODO serialize datetime - status: String, - license: License, - client_side: String, // TODO serialize as enum - server_side: String, // TODO serialize as enum - downloads: isize, - followers: isize, - categories: Vec, - versions: Vec, - icon_url: Option, - issues_url: Option, - source_url: Option, - wiki_url: Option, - discord_url: Option, - donation_urls: Vec, -} - -#[derive(Deserialize, Debug)] -struct License { - id: String, - name: String, - url: String, -} - -#[derive(Deserialize, Debug)] -struct ModVersion { - id: String, // version id - mod_id: String, // mod id - author_id: String, // user id - // NOTE modrinth docs list this as a String, but is actually a bool? - // featured: String, // user id - name: String, - version_number: String, - changelog: Option, - changelog_url: Option, - date_published: String, // TODO serialize datetime - downloads: isize, - version_type: String, // TODO {alpha | beta | release} - files: Vec, - dependencies: Vec, // TODO dependency wrangling, thank you modrinth, very cool - game_versions: Vec, - loaders: Vec, -} - -#[derive(Deserialize, Debug)] -struct ModVersionFile { - hashes: HashMap, - url: String, - filename: String, -} +use api::*; +use config::*; async fn search_mods(ctx: &AppContext, search_args: &SearchArgs) -> anyhow::Result { let client = reqwest::Client::new();