use std::path::PathBuf; use clap::{Parser,Subcommand,Args,ValueEnum}; use serde::Deserialize; use squad_quest::{config::Config,quest::{Quest,QuestDifficulty as LibQuestDifficulty}}; use toml::value::Date; #[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) } #[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), } #[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 } 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(), public: args.public, 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); } }, 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()), public: args.public.unwrap_or(quest.public), 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}") } }, 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), } }, } } } }