From 790fa88fe3b034c7823ce62f71768bfccc2a3491 Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Thu, 4 Dec 2025 17:37:01 +0300 Subject: [PATCH] refactor(cli)!: Moved CLI stuff to crate::cli - Bump version to 0.4.0 - Added Config::try_load - Added Config.verbose field - Made Config.path public - Added -q/--quiet flag to CLI BREAKING CHANGE: Moved CLI-related objects to squad-quest-cli::cli --- Cargo.lock | 4 +- Cargo.toml | 2 +- cli/Cargo.toml | 2 +- cli/src/cli/account.rs | 57 ++++ cli/src/cli/map.rs | 57 ++++ cli/src/cli/mod.rs | 36 +++ cli/src/cli/quest.rs | 130 +++++++++ cli/src/lib.rs | 1 + cli/src/main.rs | 582 ++++++++++++----------------------------- src/config/mod.rs | 111 ++++++-- 10 files changed, 529 insertions(+), 453 deletions(-) create mode 100644 cli/src/cli/account.rs create mode 100644 cli/src/cli/map.rs create mode 100644 cli/src/cli/mod.rs create mode 100644 cli/src/cli/quest.rs create mode 100644 cli/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index d263382..3d80605 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -332,7 +332,7 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "squad-quest" -version = "0.3.0" +version = "0.4.0" dependencies = [ "serde", "toml", @@ -340,7 +340,7 @@ dependencies = [ [[package]] name = "squad-quest-cli" -version = "0.3.0" +version = "0.4.0" dependencies = [ "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index 669dcea..e66a59f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ members = ["cli"] [workspace.package] -version = "0.3.0" +version = "0.4.0" edition = "2024" repository = "https://2ndbeam.ru/git/2ndbeam/squad-quest" license = "MIT" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 2612f50..ca88ec5 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -9,5 +9,5 @@ license.workspace = true chrono = "0.4.42" clap = { version = "4.5.53", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] } -squad-quest = { version = "0.3.0", path = ".." } +squad-quest = { version = "0.4.0", path = ".." } toml = "0.9.8" diff --git a/cli/src/cli/account.rs b/cli/src/cli/account.rs new file mode 100644 index 0000000..c6da1ac --- /dev/null +++ b/cli/src/cli/account.rs @@ -0,0 +1,57 @@ +use clap::{Args,Subcommand,ValueEnum}; + +#[derive(Subcommand)] +pub enum AccountCommands { + /// List accounts + List, + /// Create empty account + Create(AccountCreateArgs), + /// Update balance value + Balance(AccountBalanceArgs), + /// Approve account answer for quest + Complete(AccountCompleteArgs), + /// Delete account + Delete(AccountDeleteArgs), +} + +#[derive(Args)] +pub struct AccountCreateArgs { + /// Account will be created with this id + pub id: String, +} + +#[derive(Clone, Copy, PartialEq, Eq, ValueEnum)] +pub enum AccountBalanceActions { + Set, + Add, + Remove, +} + +#[derive(Args)] +pub struct AccountBalanceArgs { + /// Account id + pub id: String, + /// What to do with the balance + #[arg(value_enum)] + pub action: AccountBalanceActions, + /// Amount of doing + pub value: u32, + /// If action is remove, set balance to 0 if the result is negative instead of returning error + #[arg(short,long)] + pub negative_ok: bool, +} + +#[derive(Args)] +pub struct AccountCompleteArgs { + /// Id of the account + pub account: String, + /// Id of the quest + pub quest: u16, +} + +#[derive(Args)] +pub struct AccountDeleteArgs { + /// Id of the account to delete + pub id: String, +} + diff --git a/cli/src/cli/map.rs b/cli/src/cli/map.rs new file mode 100644 index 0000000..c98a158 --- /dev/null +++ b/cli/src/cli/map.rs @@ -0,0 +1,57 @@ +use clap::{Args,Subcommand}; + +#[derive(Subcommand)] +pub enum MapCommands { + /// List all rooms with connections + List, + /// Add new room to map + Add(MapAddArgs), + /// Connect two rooms + Connect(MapConnectArgs), + /// Disconnect two rooms if they're connected + Disconnect(MapConnectArgs), + /// Remove all connections with the room + Delete(MapDeleteArgs), + /// Update room data + Update(MapUpdateArgs), +} + +#[derive(Args)] +pub struct MapAddArgs { + /// Name of the room + pub name: String, + /// Price of the room + pub value: u32, + /// Optional description for the room + #[arg(long,short)] + pub description: Option, +} + +#[derive(Args)] +pub struct MapConnectArgs { + /// First room ID + pub first: u16, + /// Second room ID + pub second: u16, +} + +#[derive(Args)] +pub struct MapDeleteArgs { + /// ID of the room to delete + pub id: u16, +} + +#[derive(Args)] +pub struct MapUpdateArgs { + /// ID of the room to update + pub id: u16, + /// Room name + #[arg(short,long)] + pub name: Option, + /// Room description + #[arg(short,long)] + pub description: Option, + /// Room price + #[arg(short,long)] + pub value: Option, +} diff --git a/cli/src/cli/mod.rs b/cli/src/cli/mod.rs new file mode 100644 index 0000000..e44c361 --- /dev/null +++ b/cli/src/cli/mod.rs @@ -0,0 +1,36 @@ +use std::path::PathBuf; + +use clap::{Parser,Subcommand}; + +pub mod account; +pub mod map; +pub mod quest; + +#[derive(Parser)] +#[command(version, about, long_about = None)] +#[command(propagate_version = true)] +pub struct Cli { + /// Path to config + #[arg(short, long)] + pub config: PathBuf, + /// Object to make operation on + #[command(subcommand)] + pub command: Objects, + /// Suppress most output + #[arg(short, long)] + pub quiet: bool, +} + +#[derive(Subcommand)] +pub enum Objects { + /// Operations on the quests + #[command(subcommand)] + Quest(quest::QuestCommands), + /// Operations on the accounts + #[command(subcommand)] + Account(account::AccountCommands), + /// Operations on the map rooms + #[command(subcommand)] + Map(map::MapCommands), +} + diff --git a/cli/src/cli/quest.rs b/cli/src/cli/quest.rs new file mode 100644 index 0000000..41a7820 --- /dev/null +++ b/cli/src/cli/quest.rs @@ -0,0 +1,130 @@ +use squad_quest::quest::QuestDifficulty as LibQuestDifficulty; +use toml::value::Date; +use serde::Deserialize; +use clap::{Args,Subcommand,ValueEnum}; + +#[derive(Deserialize)] +struct DateWrapper { + date: Date, +} + +fn parse_date(arg: &str) -> Result { + let toml_str = format!("date = {arg}"); + let wrapper: DateWrapper = toml::from_str(&toml_str)?; + Ok(wrapper.date) +} + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +pub enum QuestDifficulty { + /// Easy quest + Easy, + /// Normal quest + Normal, + /// Hard quest + Hard, + /// Special case of hard quests. + Secret, +} + +impl From for LibQuestDifficulty { + fn from(value: QuestDifficulty) -> Self { + match value { + QuestDifficulty::Easy => LibQuestDifficulty::Easy, + QuestDifficulty::Normal => LibQuestDifficulty::Normal, + QuestDifficulty::Hard => LibQuestDifficulty::Hard, + QuestDifficulty::Secret => LibQuestDifficulty::Secret, + } + } +} + +#[derive(Subcommand)] +pub enum QuestCommands { + /// List available quests + List(QuestListArgs), + /// Create new quest and automatically assign it id + Create(QuestCreateArgs), + /// Update existing quest + Update(QuestUpdateArgs), + /// Delete quest + Delete(QuestDeleteArgs), + /// Make certain quests public + Daily, + /// Publish quest with specified id + Publish(QuestPublishArgs), +} + + +#[derive(Args)] +pub struct QuestListArgs { + /// Only list id and name of the quest + #[arg(short, long)] + pub short: bool, +} + +#[derive(Args)] +pub struct QuestCreateArgs { + /// Difficulty of the quest #[arg(value_enum)] + pub difficulty: QuestDifficulty, + /// Reward for the quest + pub reward: u32, + /// Name of the quest + pub name: String, + /// Visible description of the quest + pub description: String, + /// Answer for the quest for admins + pub answer: String, + /// Create quest and make it public immediately + #[arg(short,long)] + pub public: bool, + /// Make quest available on date (format = YYYY-MM-DD, ex. 2025-12-24) + #[arg(short,long,value_parser = parse_date)] + pub available: Option, + /// Quest expiration date (format = YYYY-MM-DD, ex. 2025-12-24) + #[arg(short,long,value_parser = parse_date)] + pub deadline: Option, +} + +#[derive(Args)] +pub struct QuestUpdateArgs { + /// Id of the quest to update + pub id: u16, + /// Difficulty of the quest + #[arg(value_enum,long)] + pub difficulty: Option, + /// Reward for the quest + #[arg(long)] + pub reward: Option, + /// Name of the quest + #[arg(long)] + pub name: Option, + /// Visible description of the quest + #[arg(long)] + pub description: Option, + /// Answer for the quest for admins + #[arg(long)] + pub answer: Option, + /// Create quest and make it public immediately + #[arg(long)] + pub public: Option, + /// Make quest available on date (format = YYYY-MM-DD, ex. 2025-12-24) + #[arg(long,value_parser = parse_date)] + pub available: Option, + /// Quest expiration date (format = YYYY-MM-DD, ex. 2025-12-24) + #[arg(long,value_parser = parse_date)] + pub deadline: Option, +} + +#[derive(Args)] +pub struct QuestDeleteArgs { + /// Id of the quest to delete + pub id: u16, +} + +#[derive(Args)] +pub struct QuestPublishArgs { + /// Id of the quest to publish + pub id: u16, + /// Make it non-public instead + #[arg(long,short)] + pub reverse: bool, +} diff --git a/cli/src/lib.rs b/cli/src/lib.rs new file mode 100644 index 0000000..4f77372 --- /dev/null +++ b/cli/src/lib.rs @@ -0,0 +1 @@ +pub mod cli; diff --git a/cli/src/main.rs b/cli/src/main.rs index ff2bac7..cd2e5f6 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,272 +1,11 @@ -use std::path::{Path, PathBuf}; +use std::path::Path; -use clap::{Parser,Subcommand,Args,ValueEnum}; -use serde::Deserialize; -use squad_quest::{SquadObject, account::Account, config::Config, error::Error, map::{Map, Room}, quest::{Quest,QuestDifficulty as LibQuestDifficulty}}; +use clap::Parser; +use squad_quest_cli::cli::{Cli,Objects,account::*,map::*,quest::*}; +use squad_quest::{SquadObject, account::Account, config::Config, map::{Map, Room}, quest::Quest}; use toml::value::Date; use chrono::{Datelike, NaiveDate, Utc}; -#[derive(Deserialize)] -struct DateWrapper { - date: Date, -} - -fn parse_date(arg: &str) -> Result { - let toml_str = format!("date = {arg}"); - let wrapper: DateWrapper = toml::from_str(&toml_str)?; - Ok(wrapper.date) -} - -#[derive(Parser)] -#[command(version, about, long_about = None)] -#[command(propagate_version = true)] -struct Cli { - /// Path to config - #[arg(short, long)] - config: PathBuf, - /// Object to make operation on - #[command(subcommand)] - command: Objects, -} - -#[derive(Subcommand)] -enum Objects { - /// Operations on the quests - #[command(subcommand)] - Quest(QuestCommands), - /// Operations on the accounts - #[command(subcommand)] - Account(AccountCommands), - /// Operations on the map rooms - #[command(subcommand)] - Map(MapCommands), -} - -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] -enum QuestDifficulty { - /// Easy quest - Easy, - /// Normal quest - Normal, - /// Hard quest - Hard, - /// Special case of hard quests. - Secret, -} - -impl From for LibQuestDifficulty { - fn from(value: QuestDifficulty) -> Self { - match value { - QuestDifficulty::Easy => LibQuestDifficulty::Easy, - QuestDifficulty::Normal => LibQuestDifficulty::Normal, - QuestDifficulty::Hard => LibQuestDifficulty::Hard, - QuestDifficulty::Secret => LibQuestDifficulty::Secret, - } - } -} - -#[derive(Subcommand)] -enum QuestCommands { - /// List available quests - List(QuestListArgs), - /// Create new quest and automatically assign it id - Create(QuestCreateArgs), - /// Update existing quest - Update(QuestUpdateArgs), - /// Delete quest - Delete(QuestDeleteArgs), - /// Make certain quests public - Daily, - /// Publish quest with specified id - Publish(QuestPublishArgs), -} - - -#[derive(Args)] -struct QuestListArgs { - /// Only list id and name of the quest - #[arg(short, long)] - short: bool, -} - -#[derive(Args)] -struct QuestCreateArgs { /// Difficulty of the quest #[arg(value_enum)] - difficulty: QuestDifficulty, - /// Reward for the quest - reward: u32, - /// Name of the quest - name: String, - /// Visible description of the quest - description: String, - /// Answer for the quest for admins - answer: String, - /// Create quest and make it public immediately - #[arg(short,long)] - public: bool, - /// Make quest available on date (format = YYYY-MM-DD, ex. 2025-12-24) - #[arg(short,long,value_parser = parse_date)] - available: Option, - /// Quest expiration date (format = YYYY-MM-DD, ex. 2025-12-24) - #[arg(short,long,value_parser = parse_date)] - deadline: Option, -} - -#[derive(Args)] -struct QuestUpdateArgs { - /// Id of the quest to update - id: u16, - /// Difficulty of the quest - #[arg(value_enum,long)] - difficulty: Option, - /// Reward for the quest - #[arg(long)] - reward: Option, - /// Name of the quest - #[arg(long)] - name: Option, - /// Visible description of the quest - #[arg(long)] - description: Option, - /// Answer for the quest for admins - #[arg(long)] - answer: Option, - /// Create quest and make it public immediately - #[arg(long)] - public: Option, - /// Make quest available on date (format = YYYY-MM-DD, ex. 2025-12-24) - #[arg(long,value_parser = parse_date)] - available: Option, - /// Quest expiration date (format = YYYY-MM-DD, ex. 2025-12-24) - #[arg(long,value_parser = parse_date)] - deadline: Option, -} - -#[derive(Args)] -struct QuestDeleteArgs { - /// Id of the quest to delete - id: u16, -} - -#[derive(Args)] -struct QuestPublishArgs { - /// Id of the quest to publish - id: u16, - /// Make it non-public instead - #[arg(long,short)] - reverse: bool, -} - -#[derive(Subcommand)] -enum AccountCommands { - /// List accounts - List, - /// Create empty account - Create(AccountCreateArgs), - /// Update balance value - Balance(AccountBalanceArgs), - /// Approve account answer for quest - Complete(AccountCompleteArgs), - /// Delete account - Delete(AccountDeleteArgs), -} - -#[derive(Args)] -struct AccountCreateArgs { - /// Account will be created with this id - id: String, -} - -#[derive(Clone, Copy, PartialEq, Eq, ValueEnum)] -enum AccountBalanceActions { - Set, - Add, - Remove, -} - -#[derive(Args)] -struct AccountBalanceArgs { - /// Account id - id: String, - /// What to do with the balance - #[arg(value_enum)] - action: AccountBalanceActions, - /// Amount of doing - value: u32, - /// If action is remove, set balance to 0 if the result is negative instead of returning error - #[arg(short,long)] - negative_ok: bool, -} - -#[derive(Args)] -struct AccountCompleteArgs { - /// Id of the account - account: String, - /// Id of the quest - quest: u16, -} - -#[derive(Args)] -struct AccountDeleteArgs { - /// Id of the account to delete - id: String, -} - -#[derive(Subcommand)] -enum MapCommands { - /// List all rooms with connections - List, - /// Add new room to map - Add(MapAddArgs), - /// Connect two rooms - Connect(MapConnectArgs), - /// Disconnect two rooms if they're connected - Disconnect(MapConnectArgs), - /// Remove all connections with the room - Delete(MapDeleteArgs), - /// Update room data - Update(MapUpdateArgs), -} - -#[derive(Args)] -struct MapAddArgs { - /// Name of the room - name: String, - /// Price of the room - value: u32, - /// Optional description for the room - #[arg(long,short)] - description: Option, -} - -#[derive(Args)] -struct MapConnectArgs { - /// First room ID - first: u16, - /// Second room ID - second: u16, -} - -#[derive(Args)] -struct MapDeleteArgs { - /// ID of the room to delete - id: u16, -} - -#[derive(Args)] -struct MapUpdateArgs { - /// ID of the room to update - id: u16, - /// Room name - #[arg(short,long)] - name: Option, - /// Room description - #[arg(short,long)] - description: Option, - /// Room price - #[arg(short,long)] - value: Option, -} - fn print_quest_short(quest: &Quest) { println!("Quest #{}: {}", quest.id, quest.name); } @@ -278,16 +17,36 @@ fn print_quest_long(quest: &Quest) { println!("Answer:\n{}", quest.answer); } -fn main() -> Result<(), Error> { +fn main() { let cli = Cli::parse(); - let config = Config::load(cli.config.clone()); + let config = match cli.quiet { + false => Config::load(cli.config.clone()), + true => { + match Config::try_load(cli.config.clone()) { + Ok(mut config) => { + config.verbose = false; + config + }, + Err(_) => { + let path = cli.config.clone().parent().unwrap_or(&Path::new(".")).to_owned(); + Config { + verbose: false, + path, + ..Default::default() + } + } + } + }, + }; match &cli.command { Objects::Quest(commands) => { + let mut quests = config.load_quests(); + let mut path = config.full_quests_path(); + match commands { QuestCommands::List(args) => { - let quests = config.load_quests(); for quest in quests { if args.short { print_quest_short(&quest); @@ -297,24 +56,27 @@ fn main() -> Result<(), Error> { } }, QuestCommands::Create(args) => { - let mut quests = config.load_quests(); quests.sort_by(|a,b| a.id.cmp(&b.id)); let next_id = match quests.last() { Some(quest) => quest.id + 1u16, None => 0u16 }; - let path = config.full_quests_path(); - let mut quest_path = path.clone(); - quest_path.push(format!("{next_id}.toml")); - match std::fs::exists(&quest_path) { + path.push(format!("{next_id}.toml")); + match std::fs::exists(&path) { Ok(exists) => { if exists { - panic!("Error: {:?} is not empty.", quest_path); + if !cli.quiet { + eprintln!("Error: {:?} is not empty.", path); + } + return; } }, Err(error) => { - panic!("Error while retrieving {:?}: {}.", quest_path, error); + if !cli.quiet { + eprintln!("Error while retrieving {:?}: {}.", path, error); + } + return; } } @@ -329,16 +91,19 @@ fn main() -> Result<(), Error> { available_on: args.available.clone(), deadline: args.deadline.clone() }; - if let Err(error) = quest.save(path) { - eprintln!("Error while saving quest: {error}."); - } else { - println!("Successfully saved quest #{}.", quest.id); + + match quest.save(path) { + Ok(_) if !cli.quiet => println!("Successfully saved quest #{}", quest.id), + Err(error) if !cli.quiet => eprintln!("Error while saving quest: {error}"), + _ => {}, } }, QuestCommands::Update(args) => { - let quests = config.load_quests(); let Some(quest) = quests.iter().find(|q| q.id == args.id) else { - panic!("Error: Quest #{} not found.", args.id); + if !cli.quiet { + eprintln!("Error: Quest #{} not found.", args.id); + } + return; }; let quest = Quest { id: args.id, @@ -354,22 +119,20 @@ fn main() -> Result<(), Error> { available_on: args.available.clone().or(quest.available_on.clone()), deadline: args.deadline.clone().or(quest.deadline.clone()) }; - let path = config.full_quests_path(); match quest.save(path) { - Ok(_) => println!("Updated quest #{}", quest.id), - Err(error) => eprintln!("Error while updating quest: {error}") + Ok(_) if !cli.quiet => println!("Updated quest #{}", quest.id), + Err(error) if !cli.quiet => eprintln!("Error while updating quest: {error}"), + _ => {}, } }, QuestCommands::Delete(args) => { - let mut path = config.full_quests_path(); - path.push(format!("{}.toml", args.id)); match Quest::delete(path) { - Ok(_) => println!("Successfully deleted quest #{}", args.id), - Err(error) => eprintln!("Error deleting quest #{}: {}", args.id, error), + Ok(_) if !cli.quiet => println!("Successfully deleted quest #{}", args.id), + Err(error) if !cli.quiet => eprintln!("Error deleting quest #{}: {}", args.id, error), + _ => {}, } }, QuestCommands::Daily => { - let mut quests = config.load_quests(); let today: NaiveDate = Utc::now().date_naive(); let toml_today = Date { year: today.year() as u16, @@ -377,46 +140,52 @@ fn main() -> Result<(), Error> { day: today.day() as u8 }; - let path = config.full_quests_path(); - for quest in quests.iter_mut().filter(|q| !q.public && q.available_on.is_some_and(|date| date.le(&toml_today))) { - println!("Quest #{} will be published.", quest.id); quest.public = true; - if let Err(error) = quest.save(path.clone()) { - eprintln!("Error while saving quest: {error}."); + + match quest.save(path.clone()) { + Ok(_) if !cli.quiet => println!("Published quest #{}", quest.id), + Err(error) if !cli.quiet => eprintln!("Error while publishing quest: {error}"), + _ => {}, } } }, QuestCommands::Publish(args) => { - let mut quests = config.load_quests(); let quest = quests.iter_mut().find(|q| q.id == args.id); - let path = config.full_quests_path(); - match quest { Some(quest) => { let not_str = if args.reverse {" not "} else {" "}; if quest.public != args.reverse { - panic!("Quest #{} is already{}public", quest.id, not_str); + if !cli.quiet { + eprintln!("Quest #{} is already{}public", quest.id, not_str); + } + return; } quest.public = !args.reverse; - if let Err(error) = quest.save(path) { - eprintln!("Error while saving quest: {error}."); - }; + + match quest.save(path.clone()) { + Ok(_) if !cli.quiet => println!("Published quest #{}", quest.id), + Err(error) if !cli.quiet => eprintln!("Error while publishing quest: {error}"), + _ => {}, + } }, - None => { + None if !cli.quiet => { eprintln!("Error: couldn't find quest with id {}.", args.id); - } + }, + _ => {}, } }, } }, Objects::Account(commands) => { + let mut accounts = config.load_accounts(); + let mut path = config.full_accounts_path(); + match commands { AccountCommands::List => { - let accounts = config.load_accounts(); for account in accounts { println!("\"{}\": Balance {}", account.id, account.balance); @@ -428,30 +197,27 @@ fn main() -> Result<(), Error> { ..Default::default() }; - let accounts = config.load_accounts(); - if let Some(_) = accounts.iter().find(|a| a.id == account.id) { - panic!("Error: account {} exists.", account.id); + if !cli.quiet { + eprintln!("Error: account {} exists.", account.id); + } + return; } - let accounts_path = config.full_accounts_path(); - - match account.save(accounts_path) { - Ok(_) => { - println!("Successfully created account \"{}\"", account.id); - }, - Err(error) => { - eprintln!("Error while saving account: {error}"); - } + match account.save(path) { + Ok(_) if !cli.quiet => println!("Successfully created account \"{}\"", account.id), + Err(error) if !cli.quiet => eprintln!("Error while saving account: {error}"), + _ => {}, } }, AccountCommands::Balance(args) => { - let mut accounts = config.load_accounts(); - let account = match accounts.iter_mut().find(|a| a.id == args.id) { Some(acc) => acc, None => { - panic!("Could not find account \"{}\"", args.id); + if !cli.quiet { + eprintln!("Could not find account \"{}\"", args.id); + } + return; } }; @@ -467,7 +233,10 @@ fn main() -> Result<(), Error> { if args.negative_ok { account.balance = 0u32; } else { - panic!("Error: balance ({}) is less than {}.", account.balance, args.value); + if !cli.quiet { + eprintln!("Error: balance ({}) is less than {}.", account.balance, args.value); + } + return; } } else { account.balance -= args.value; @@ -475,69 +244,68 @@ fn main() -> Result<(), Error> { } } - let accounts_path = config.full_accounts_path(); - - match account.save(accounts_path) { - Ok(_) => { - println!("Successfully updated account \"{}\" balance.", account.id); - }, - Err(error) => { - eprintln!("Error while saving account: {error}"); - } - }; + match account.save(path) { + Ok(_) if !cli.quiet => println!("Successfully updated account \"{}\" balance", account.id), + Err(error) if !cli.quiet => eprintln!("Error while saving account: {error}"), + _ => {}, + } }, AccountCommands::Complete(args) => { - let mut accounts = config.load_accounts(); - let account = match accounts.iter_mut().find(|a| a.id == args.account) { Some(acc) => acc, None => { - panic!("Could not find account \"{}\"", args.account); + if !cli.quiet { + eprintln!("Could not find account \"{}\"", args.account); + } + return; } }; let quests = config.load_quests(); if let None = quests.iter().find(|q| q.id == args.quest) { - panic!("Could not find quest #{}", args.quest); + if !cli.quiet { + eprintln!("Could not find quest #{}", args.quest); + } + return; } match account.quests_completed.iter().find(|qid| **qid == args.quest) { - Some(_) => { + Some(_) if !cli.quiet => { println!("Quest #{} is already completed on account \"{}\"", args.quest, args.account); }, None => { account.quests_completed.push(args.quest); - let accounts_path = config.full_accounts_path(); - match account.save(accounts_path) { - Ok(_) => { - println!("Account \"{}\" completed quest #{}.", args.account, args.quest); - }, - Err(error) => { - eprintln!("Error while saving account: {error}"); - } + match account.save(path) { + Ok(_) if !cli.quiet => println!("Successfully completed quest #{} on account \"{}\".", args.quest, account.id), + Err(error) if !cli.quiet => eprintln!("Error while saving account: {error}"), + _ => {}, } - } + }, + _ => {}, } }, AccountCommands::Delete(args) => { - let mut accounts_path = config.full_accounts_path(); - accounts_path.push(format!("{}.toml", args.id)); - match Account::delete(accounts_path) { - Ok(_) => { - println!("Successfully deleted account \"{}\".", args.id); - }, - Err(error) => { - eprintln!("Error deleting account: {error}"); - } + path.push(format!("{}.toml", args.id)); + match Account::delete(path) { + Ok(_) if !cli.quiet => println!("Successfully deleted account \"{}\".", args.id), + Err(error) if !cli.quiet => eprintln!("Error while deleting account: {error}"), + _ => {}, } }, } }, Objects::Map(commands) => { let map_path = config.full_map_path(); - let mut map = Map::load(map_path.clone())?; - map.room.sort_by(|a,b| a.id.cmp(&b.id)); + let mut map = match Map::load(map_path.clone()) { + Ok(map) => map, + Err(error) => { + if !cli.quiet { + eprintln!("Error while loading map: {error}"); + } + return; + } + }; match commands { MapCommands::List => { for room in map.room { @@ -545,6 +313,7 @@ fn main() -> Result<(), Error> { } }, MapCommands::Add(args) => { + map.room.sort_by(|a,b| a.id.cmp(&b.id)); let last_id = match map.room.last() { Some(r) => r.id + 1u16, None => 0u16 @@ -558,18 +327,17 @@ fn main() -> Result<(), Error> { let r_id = room.id; map.room.push(room); match map.save(map_path.parent().unwrap_or(Path::new("")).to_owned()) { - Ok(_) => { - println!("Created room #{}.", r_id); - println!("Successfully saved map."); - }, - Err(error) => { - eprintln!("Error while saving map: {error}"); - } + Ok(_) if !cli.quiet => println!("Created room #{}.", r_id), + Err(error) if !cli.quiet => eprintln!("Error while saving map: {error}"), + _ => {}, } }, MapCommands::Delete(args) => { let Some(room) = map.room.iter().find(|r| r.id == args.id) else { - panic!("Error: Room #{} not found", args.id); + if !cli.quiet { + eprintln!("Error: Room #{} not found", args.id); + } + return; }; let r_id = room.id; @@ -584,18 +352,17 @@ fn main() -> Result<(), Error> { } match map.save(map_path.parent().unwrap_or(Path::new("")).to_owned()) { - Ok(_) => { - println!("Removed room #{}.", r_id); - println!("Successfully saved map."); - }, - Err(error) => { - eprintln!("Error while saving map: {error}"); - } + Ok(_) if !cli.quiet => println!("Deleted room #{}.", r_id), + Err(error) if !cli.quiet => eprintln!("Error while saving map: {error}"), + _ => {}, } }, MapCommands::Update(args) => { let Some(room) = map.room.iter_mut().find(|r| r.id == args.id) else { - panic!("Error: Room #{} not found", args.id); + if !cli.quiet { + eprintln!("Error: Room #{} not found", args.id); + } + return; }; if let Some(name) = &args.name { @@ -611,71 +378,44 @@ fn main() -> Result<(), Error> { } match map.save(map_path.parent().unwrap_or(Path::new("")).to_owned()) { - Ok(_) => { - println!("Updated room #{}.", args.id); - println!("Successfully saved map."); - }, - Err(error) => { - eprintln!("Error while saving map: {error}"); - } + Ok(_) if !cli.quiet => println!("Updated room #{}.", args.id), + Err(error) if !cli.quiet => eprintln!("Error while saving map: {error}"), + _ => {}, } }, - MapCommands::Connect(args) => { + MapCommands::Connect(args) | MapCommands::Disconnect(args) => { + let connect = match commands { + MapCommands::Connect(_) => true, + _ => false, + }; + // We iterate twice to make references first->second and second->first for (first, second) in [(args.first, args.second),(args.second, args.first)] { let Some(room) = map.room.iter_mut().find(|r| r.id == first) else { - panic!("Error: Room #{} not found", first); - }; - - match room.children.iter().find(|id| **id == second) { - Some(_) => { - println!("Room #{} already has reference to #{}", first, second); - }, - None => { - room.children.push(second); + if !cli.quiet { + eprintln!("Error: Room #{} not found", first); } - } - } - - match map.save(map_path.parent().unwrap_or(Path::new("")).to_owned()) { - Ok(_) => { - println!("Connected rooms #{} <-> #{}.", args.first, args.second); - println!("Successfully saved map."); - }, - Err(error) => { - eprintln!("Error while saving map: {error}"); - } - } - }, - MapCommands::Disconnect(args) => { - // We iterate twice to make references first->second and second->first - for (first, second) in [(args.first, args.second),(args.second, args.first)] { - let Some(room) = map.room.iter_mut().find(|r| r.id == first) else { - panic!("Error: Room #{} not found", first); + return; }; match room.children.iter().position(|id| *id == second) { - Some(id) => { - room.children.remove(id as usize); - }, - None => { - println!("Room #{} has no reference to #{}", first, second); - } + Some(_) if connect && !cli.quiet => println!("Room #{} already has reference to #{}", first, second), + None if connect => room.children.push(second), + Some(id) if !connect => {room.children.remove(id as usize);}, + None if !connect && !cli.quiet => println!("Room #{} has no reference to #{}", first, second), + _ => {}, } } + let connected = if connect { "Connected" } else { "Disconnected" }; + match map.save(map_path.parent().unwrap_or(Path::new("")).to_owned()) { - Ok(_) => { - println!("Disconnected rooms #{} #{}.", args.first, args.second); - println!("Successfully saved map."); - }, - Err(error) => { - eprintln!("Error while saving map: {error}"); - } + Ok(_) if !cli.quiet => println!("{connected} rooms #{} <-> #{}.", args.first, args.second), + Err(error) if !cli.quiet => eprintln!("Error while saving map: {error}"), + _ => {}, } - } + }, } } } - Ok(()) } diff --git a/src/config/mod.rs b/src/config/mod.rs index b47b565..c805002 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -11,7 +11,7 @@ use crate::{SquadObject, account::Account, error::Error, quest::Quest}; pub struct Config { /// Path to config directory #[serde(skip)] - path: PathBuf, + pub path: PathBuf, /// Path to serialized [quests][`crate::quest::Quest`] folder pub quests_path: PathBuf, @@ -20,7 +20,10 @@ pub struct Config { pub accounts_path: PathBuf, /// Path to serialized [map][`crate::map::Map`] file - pub map: PathBuf + pub map: PathBuf, + + /// If true, print to std{out/err} + pub verbose: bool, } impl Default for Config { @@ -29,7 +32,8 @@ impl Default for Config { path: ".".into(), quests_path: "quests".into(), accounts_path: "accounts".into(), - map: "map.toml".into() + map: "map.toml".into(), + verbose: true, } } } @@ -67,8 +71,12 @@ fn handle_account_entry(account_entry: DirEntry) -> Result{ } impl Config { - /// Deserialize config from TOML - /// Logs all errors and returns default config if that happens + /// Deserialize config from TOML. + /// + /// This function wraps [try_load][Config::try_load]. + /// + /// Logs all errors if `config.verbose == true`. + /// Returns default config on error. /// /// # Examples /// ```rust @@ -81,25 +89,62 @@ impl Config { let dir = path.parent() .unwrap_or(Path::new(".")) .to_owned(); + + match Self::try_load(path) { + Ok(conf) => { + if conf.verbose { + println!("Successfully loaded config"); + } + conf + }, + Err(error) => { + let conf = Config { + path: dir, + ..Default::default() + }; + + if conf.verbose { + println!("Error while loading config: {error}"); + } + + conf + } + } + } + + /// Deserialize config from TOML + /// + /// # Examples + /// ```rust + /// use squad_quest::{config::Config,error::Error}; + /// # fn main() { + /// # let _ = wrapper(); + /// # } + /// # fn wrapper() -> Result<(), Error> { + /// let path = "cfg/config.toml".into(); + /// let config = Config::try_load(path)?; + /// # Ok(()) + /// # } + /// ``` + pub fn try_load(path: PathBuf) -> Result { + let dir = path.parent() + .unwrap_or(Path::new(".")) + .to_owned(); + match fs::read_to_string(path) { Ok(string) => { match toml::from_str::(&string) { Ok(mut conf) => { - println!("Successfully loaded config"); conf.path = dir; - conf + Ok(conf) }, Err(error) => { - eprintln!("Error on parsing config: {error}"); - let mut cfg = Config::default(); - cfg.path = dir; - cfg + Err(Error::TomlDeserializeError(error)) } } }, Err(error) => { - eprintln!("Error on reading config path: {error}"); - Config::default() + Err(Error::IoError(error)) } } } @@ -150,23 +195,28 @@ impl Config { Ok(quest_entry) => { match handle_quest_entry(quest_entry) { Ok(quest) => out_vec.push(quest), - Err(error) => { + Err(error) if self.verbose => { eprintln!("Error on loading single quest: {error}"); - } + }, + _ => {}, } }, - Err(error) => { + Err(error) if self.verbose => { eprintln!("Error on loading single quest: {error}"); - } + }, + _ => {}, } } }, - Err(error) => { + Err(error) if self.verbose => { eprintln!("Error on loading quests: {error}"); - } + }, + _ => {}, + } + + if self.verbose { + println!("Loaded {} quests successfully", out_vec.len()); } - - println!("Loaded {} quests successfully", out_vec.len()); out_vec } @@ -217,23 +267,28 @@ impl Config { Ok(acc_entry) => { match handle_account_entry(acc_entry) { Ok(quest) => out_vec.push(quest), - Err(error) => { + Err(error) if self.verbose => { eprintln!("Error on loading single account: {error}"); - } + }, + _ => {}, } }, - Err(error) => { + Err(error) if self.verbose => { eprintln!("Error on loading single account: {error}"); - } + }, + _ => {}, } } }, - Err(error) => { + Err(error) if self.verbose => { eprintln!("Error on loading accounts: {error}"); - } + }, + _ => {}, } - println!("Loaded {} accounts successfully", out_vec.len()); + if self.verbose { + println!("Loaded {} accounts successfully", out_vec.len()); + } out_vec }