From 2e14614bdfd93d72f2a65120ea4e34b433beee2c Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Sat, 29 Nov 2025 14:12:07 +0300 Subject: [PATCH 1/3] feat: full_quests_path - Added Config::full_quests_path - Fixed quests saving in parent folder --- src/config/mod.rs | 21 +++++++++++++++++++-- src/quest/mod.rs | 3 ++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 45fccac..407f8dc 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -88,6 +88,24 @@ impl Config { } } + /// Returns full path to quests folder + /// This path will be relative to $PWD, not to config. + /// + /// # Examples + /// ```rust + /// use squad_quest::config::Config; + /// + /// let path = "cfg/config.toml".into(); + /// let config = Config::load(path); + /// + /// let quests_path = config.full_quests_path(); + /// ``` + pub fn full_quests_path(&self) -> PathBuf { + let mut path = self.path.clone(); + path.push(self.quests_path.clone()); + path + } + /// Load [Vec]<[Quest]> from quests folder. /// Also logs errors and counts successfully loaded quests. /// @@ -107,8 +125,7 @@ impl Config { pub fn load_quests(&self) -> Vec { let mut out_vec = Vec::new(); - let mut path = self.path.clone(); - path.push(self.quests_path.clone()); + let path = self.full_quests_path(); match fs::read_dir(path) { Ok(iter) => { diff --git a/src/quest/mod.rs b/src/quest/mod.rs index d675cc1..e3a0d1b 100644 --- a/src/quest/mod.rs +++ b/src/quest/mod.rs @@ -154,7 +154,8 @@ impl Quest { /// ``` pub fn save(&self, path: PathBuf) -> Result<(), QuestError> { let filename = format!("{}.toml", self.id); - let full_path = path.with_file_name(filename); + let mut full_path = path; + full_path.push(filename); let str = match toml::to_string_pretty(&self) { Ok(string) => string, From d61011f5ea9e6d56b08775d877adb1f3b252d473 Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Sat, 29 Nov 2025 14:40:23 +0300 Subject: [PATCH 2/3] feat: CLI quest CRUD - Quest creation - Quest list retrieving - Quest update - Quest deletion --- src/bin/cli.rs | 131 ++++++++++++++++++++++++++++++++++++++++++++--- src/quest/mod.rs | 2 +- 2 files changed, 124 insertions(+), 9 deletions(-) diff --git a/src/bin/cli.rs b/src/bin/cli.rs index 545b656..fef2fc1 100644 --- a/src/bin/cli.rs +++ b/src/bin/cli.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; use clap::{Parser,Subcommand,Args,ValueEnum}; +use squad_quest::{config::Config,quest::{Quest,QuestDifficulty as LibQuestDifficulty}}; #[derive(Parser)] #[command(version, about, long_about = None)] @@ -33,6 +34,17 @@ enum QuestDifficulty { 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 @@ -72,16 +84,20 @@ struct QuestUpdateArgs { /// Id of the quest to update id: u16, /// Difficulty of the quest - #[arg(value_enum)] - difficulty: QuestDifficulty, + #[arg(value_enum,long)] + difficulty: Option, /// Reward for the quest - reward: u32, + #[arg(long)] + reward: Option, /// Name of the quest - name: String, + #[arg(long)] + name: Option, /// Visible description of the quest - description: String, + #[arg(long)] + description: Option, /// Answer for the quest for admins - answer: String + #[arg(long)] + answer: Option } #[derive(Args)] @@ -90,6 +106,105 @@ struct QuestDeleteArgs { id: u16 } -fn main() { - let _cli = Cli::parse(); +fn print_quest_short(quest: &Quest) { + println!("Quest #{}: {}", quest.id, quest.name); +} + +fn print_quest_long(quest: &Quest) { + print_quest_short(quest); + println!("Difficulty: {:?}", quest.difficulty); + println!("Description:\n{}", quest.description); + println!("Answer:\n{}", quest.answer); +} + +fn main() { + let cli = Cli::parse(); + + let config = Config::load(cli.config.clone()); + + match &cli.command { + Objects::Quest(commands) => { + match commands { + QuestCommands::List(args) => { + let quests = config.load_quests(); + for quest in quests { + if args.short { + print_quest_short(&quest); + } else { + print_quest_long(&quest); + } + } + }, + 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) if quest.id == u16::MAX => { + panic!("Error: quest list contains quest with u16::MAX id."); + } + 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) { + Ok(exists) => { + if exists { + panic!("Error: {:?} is not empty.", quest_path); + } + }, + Err(error) => { + panic!("Error while retrieving {:?}: {}.", quest_path, error); + } + } + + let quest = Quest { + id: next_id, + difficulty: args.difficulty.into(), + reward: args.reward, + name: args.name.clone(), + description: args.description.clone(), + answer: args.answer.clone(), + }; + if let Err(error) = quest.save(path) { + eprintln!("Error while saving quest: {error}."); + } else { + println!("Successfully saved quest #{}.", quest.id); + } + }, + 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); + }; + let quest = Quest { + id: args.id, + difficulty: match args.difficulty { + Some(diff) => diff.into(), + None => quest.difficulty + }, + reward: args.reward.unwrap_or(quest.reward), + name: args.name.clone().unwrap_or(quest.name.clone()), + description: args.description.clone().unwrap_or(quest.description.clone()), + answer: args.answer.clone().unwrap_or(quest.answer.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}") + } + }, + 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), + } + }, + } + } + } } diff --git a/src/quest/mod.rs b/src/quest/mod.rs index e3a0d1b..6c95f69 100644 --- a/src/quest/mod.rs +++ b/src/quest/mod.rs @@ -8,7 +8,7 @@ use serde::{ Serialize, Deserialize }; use error::QuestError; /// Difficulty of the quest -#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)] pub enum QuestDifficulty { /// Easy quest Easy, From 96235086d777ee1b1f3017df1bebb61b4305c8d5 Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Sat, 29 Nov 2025 14:46:42 +0300 Subject: [PATCH 3/3] test: fixed crud test --- tests/io.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/io.rs b/tests/io.rs index c186ad0..fc3def8 100644 --- a/tests/io.rs +++ b/tests/io.rs @@ -21,9 +21,9 @@ fn quest_crud() -> Result<(), QuestError> { let filename = format!("{}.toml", quest.id); - let delete_path = quests_path.with_file_name(filename); + quests_path.push(filename); - Quest::delete(delete_path)?; + Quest::delete(quests_path)?; Ok(()) }