Split up source into several files
This commit is contained in:
parent
30e8c782df
commit
b4ab1bf1d4
|
@ -0,0 +1,118 @@
|
||||||
|
use console::style;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct SearchResponse {
|
||||||
|
pub hits: Vec<ModResult>,
|
||||||
|
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<String>, // NOTE this isn't in all search results?
|
||||||
|
pub author: String,
|
||||||
|
pub title: String,
|
||||||
|
pub description: String,
|
||||||
|
pub categories: Vec<String>,
|
||||||
|
pub versions: Vec<String>,
|
||||||
|
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<String>,
|
||||||
|
pub versions: Vec<String>,
|
||||||
|
pub icon_url: Option<String>,
|
||||||
|
pub issues_url: Option<String>,
|
||||||
|
pub source_url: Option<String>,
|
||||||
|
pub wiki_url: Option<String>,
|
||||||
|
pub discord_url: Option<String>,
|
||||||
|
pub donation_urls: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<String>,
|
||||||
|
pub changelog_url: Option<String>,
|
||||||
|
pub date_published: String, // TODO serialize datetime
|
||||||
|
pub downloads: isize,
|
||||||
|
pub version_type: String, // TODO {alpha | beta | release}
|
||||||
|
pub files: Vec<ModVersionFile>,
|
||||||
|
pub dependencies: Vec<String>, // TODO dependency wrangling, thank you modrinth, very cool
|
||||||
|
pub game_versions: Vec<String>,
|
||||||
|
pub loaders: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct ModVersionFile {
|
||||||
|
pub hashes: HashMap<String, String>,
|
||||||
|
pub url: String,
|
||||||
|
pub filename: String,
|
||||||
|
}
|
|
@ -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<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<PathBuf>,
|
||||||
|
|
||||||
|
/// Path to mod lockfile
|
||||||
|
#[structopt(short, long, parse(from_os_str))]
|
||||||
|
pub lockfile: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// 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<Config, confy::ConfyError> {
|
||||||
|
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,
|
||||||
|
}
|
222
src/main.rs
222
src/main.rs
|
@ -1,226 +1,14 @@
|
||||||
use console::style;
|
|
||||||
use log::*;
|
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use serde::{Deserialize, Serialize};
|
use log::*;
|
||||||
use std::cmp::min;
|
use std::cmp::min;
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::PathBuf;
|
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
|
|
||||||
// TODO parameter to restrict target Minecraft version
|
mod api;
|
||||||
#[derive(StructOpt, Clone, Debug)]
|
mod config;
|
||||||
struct SearchArgs {
|
|
||||||
package_name: String,
|
|
||||||
|
|
||||||
/// Restricts the target Minecraft version
|
use api::*;
|
||||||
#[structopt(short, long)]
|
use config::*;
|
||||||
version: Option<Vec<String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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<PathBuf>,
|
|
||||||
|
|
||||||
/// Path to mod lockfile
|
|
||||||
#[structopt(short, long, parse(from_os_str))]
|
|
||||||
lockfile: Option<PathBuf>,
|
|
||||||
|
|
||||||
/// Auto-accept confirmation dialogues
|
|
||||||
#[structopt(short = "y", long = "yes")]
|
|
||||||
auto_accept: bool,
|
|
||||||
|
|
||||||
#[structopt(subcommand)]
|
|
||||||
command: Command,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Args {
|
|
||||||
fn load_config(&self) -> Result<Config, confy::ConfyError> {
|
|
||||||
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<ModResult>,
|
|
||||||
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<String>, // NOTE this isn't in all search results?
|
|
||||||
author: String,
|
|
||||||
title: String,
|
|
||||||
description: String,
|
|
||||||
categories: Vec<String>,
|
|
||||||
versions: Vec<String>,
|
|
||||||
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<String>,
|
|
||||||
versions: Vec<String>,
|
|
||||||
icon_url: Option<String>,
|
|
||||||
issues_url: Option<String>,
|
|
||||||
source_url: Option<String>,
|
|
||||||
wiki_url: Option<String>,
|
|
||||||
discord_url: Option<String>,
|
|
||||||
donation_urls: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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<String>,
|
|
||||||
changelog_url: Option<String>,
|
|
||||||
date_published: String, // TODO serialize datetime
|
|
||||||
downloads: isize,
|
|
||||||
version_type: String, // TODO {alpha | beta | release}
|
|
||||||
files: Vec<ModVersionFile>,
|
|
||||||
dependencies: Vec<String>, // TODO dependency wrangling, thank you modrinth, very cool
|
|
||||||
game_versions: Vec<String>,
|
|
||||||
loaders: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct ModVersionFile {
|
|
||||||
hashes: HashMap<String, String>,
|
|
||||||
url: String,
|
|
||||||
filename: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn search_mods(ctx: &AppContext, search_args: &SearchArgs) -> anyhow::Result<SearchResponse> {
|
async fn search_mods(ctx: &AppContext, search_args: &SearchArgs) -> anyhow::Result<SearchResponse> {
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
|
|
Loading…
Reference in New Issue