squad-quest/src/bin/cli.rs
2ndbeam 78da6dde05 feat!: Added several fields to Quest struct
- Added field public
- Added optional field available_on
- Added optional field deadline
- Updated tests and CLI to use these fields
2025-12-01 13:26:38 +03:00

247 lines
8.2 KiB
Rust

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<Date,toml::de::Error> {
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<QuestDifficulty> 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<Date>,
/// Quest expiration date (format = YYYY-MM-DD, ex. 2025-12-24)
#[arg(short,long,value_parser = parse_date)]
deadline: Option<Date>,
}
#[derive(Args)]
struct QuestUpdateArgs {
/// Id of the quest to update
id: u16,
/// Difficulty of the quest
#[arg(value_enum,long)]
difficulty: Option<QuestDifficulty>,
/// Reward for the quest
#[arg(long)]
reward: Option<u32>,
/// Name of the quest
#[arg(long)]
name: Option<String>,
/// Visible description of the quest
#[arg(long)]
description: Option<String>,
/// Answer for the quest for admins
#[arg(long)]
answer: Option<String>,
/// Create quest and make it public immediately
#[arg(long)]
public: Option<bool>,
/// Make quest available on date (format = YYYY-MM-DD, ex. 2025-12-24)
#[arg(long,value_parser = parse_date)]
available: Option<Date>,
/// Quest expiration date (format = YYYY-MM-DD, ex. 2025-12-24)
#[arg(long,value_parser = parse_date)]
deadline: Option<Date>,
}
#[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),
}
},
}
}
}
}