feat!: Account features
- Bump version to 0.2.0
- Added trait SquadObject
- Implemented SquadObject for Quest and Account
- Implemented Config::load_accounts
- Removed src/quest/error.rs
- Added account tests in tests/main.rs
BREAKING CHANGE: Quest::{load,delete,save} are now provided by
SquadObject trait
This commit is contained in:
parent
1dc7d94785
commit
0e8cdde697
12 changed files with 285 additions and 98 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -332,7 +332,7 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
|||
|
||||
[[package]]
|
||||
name = "squad-quest"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "squad-quest"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
|
|
@ -1,24 +1,97 @@
|
|||
//! User accounts
|
||||
|
||||
use std::{fs, io::Write, path::PathBuf};
|
||||
|
||||
use serde::{ Serialize, Deserialize };
|
||||
|
||||
use crate::{SquadObject, error::Error};
|
||||
|
||||
fn default_id() -> String {
|
||||
"none".to_string()
|
||||
}
|
||||
|
||||
/// User account struct, which can be (de-)serialized from/into TOML
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||
#[serde(default)]
|
||||
pub struct Account {
|
||||
|
||||
/// User identifier, specific to used service
|
||||
#[serde(default = "default_id")]
|
||||
pub id: String,
|
||||
|
||||
/// User balance
|
||||
#[serde(default)]
|
||||
pub balance: u32,
|
||||
|
||||
/// Id of room node where user is located
|
||||
#[serde(default)]
|
||||
pub location: u16
|
||||
pub location: u16,
|
||||
|
||||
/// Vec of quests completed by this user
|
||||
pub quests_completed: Vec<u16>,
|
||||
|
||||
/// Vec of rooms unlocked by this user
|
||||
pub rooms_unlocked: Vec<u16>,
|
||||
}
|
||||
|
||||
impl Default for Account {
|
||||
fn default() -> Self {
|
||||
Account {
|
||||
id: default_id(),
|
||||
balance: u32::default(),
|
||||
location: u16::default(),
|
||||
quests_completed: Vec::new(),
|
||||
rooms_unlocked: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SquadObject for Account {
|
||||
fn load(path: PathBuf) -> Result<Self, Error> {
|
||||
match std::fs::read_to_string(path) {
|
||||
Ok(string) => {
|
||||
match toml::from_str::<Self>(&string) {
|
||||
Ok(object) => Ok(object),
|
||||
Err(error) => Err(Error::TomlDeserializeError(error))
|
||||
}
|
||||
},
|
||||
Err(error) => Err(Error::IoError(error))
|
||||
}
|
||||
}
|
||||
|
||||
fn delete(path: PathBuf) -> Result<(), Error> {
|
||||
match Self::load(path.clone()) {
|
||||
Ok(_) => {
|
||||
if let Err(error) = fs::remove_file(path) {
|
||||
return Err(Error::IoError(error));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
},
|
||||
Err(error) => Err(error)
|
||||
}
|
||||
}
|
||||
|
||||
fn save(&self, path: PathBuf) -> Result<(), Error> {
|
||||
let filename = format!("{}.toml", self.id);
|
||||
let mut full_path = path;
|
||||
full_path.push(filename);
|
||||
|
||||
let str = match toml::to_string_pretty(&self) {
|
||||
Ok(string) => string,
|
||||
Err(error) => {
|
||||
return Err(Error::TomlSerializeError(error));
|
||||
}
|
||||
};
|
||||
|
||||
let mut file = match fs::File::create(full_path) {
|
||||
Ok(f) => f,
|
||||
Err(error) => {
|
||||
return Err(Error::IoError(error));
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(error) = file.write_all(str.as_bytes()) {
|
||||
return Err(Error::IoError(error));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::path::PathBuf;
|
|||
|
||||
use clap::{Parser,Subcommand,Args,ValueEnum};
|
||||
use serde::Deserialize;
|
||||
use squad_quest::{config::Config,quest::{Quest,QuestDifficulty as LibQuestDifficulty}};
|
||||
use squad_quest::{SquadObject, config::Config, quest::{Quest,QuestDifficulty as LibQuestDifficulty}};
|
||||
use toml::value::Date;
|
||||
use chrono::{Datelike, NaiveDate, Utc};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
use std::{fs::{self, DirEntry},path::{Path, PathBuf}};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{error::Error,quest::Quest};
|
||||
use crate::{SquadObject, account::Account, error::Error, quest::Quest};
|
||||
|
||||
/// Struct for containing paths to other (de-)serializable things
|
||||
#[derive(Deserialize)]
|
||||
|
|
@ -50,6 +50,22 @@ fn handle_quest_entry(quest_entry: DirEntry) -> Result<Quest, Error>{
|
|||
Quest::load(path)
|
||||
}
|
||||
|
||||
fn handle_account_entry(account_entry: DirEntry) -> Result<Account, Error>{
|
||||
let filetype = account_entry.file_type();
|
||||
if let Err(error) = filetype {
|
||||
return Err(Error::IoError(error));
|
||||
}
|
||||
|
||||
let path = account_entry.path();
|
||||
|
||||
let filetype = filetype.unwrap();
|
||||
if !filetype.is_file() {
|
||||
return Err(Error::IsNotAFile(path));
|
||||
}
|
||||
|
||||
Account::load(path)
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Deserialize config from TOML
|
||||
/// Logs all errors and returns default config if that happens
|
||||
|
|
@ -154,4 +170,71 @@ impl Config {
|
|||
|
||||
out_vec
|
||||
}
|
||||
|
||||
/// 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 accounts_path = config.full_accounts_path();
|
||||
/// ```
|
||||
pub fn full_accounts_path(&self) -> PathBuf {
|
||||
let mut path = self.path.clone();
|
||||
path.push(self.accounts_path.clone());
|
||||
path
|
||||
}
|
||||
|
||||
/// Load [Vec]<[Account]> from accounts folder.
|
||||
/// Also logs errors and counts successfully loaded quests.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// use squad_quest::{config::Config, account::Account};
|
||||
///
|
||||
///
|
||||
/// let path = "cfg/config.toml".into();
|
||||
/// let config = Config::load(path);
|
||||
/// let accounts = config.load_accounts();
|
||||
///
|
||||
/// for account in accounts {
|
||||
/// println!("Account {}", account.id);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn load_accounts(&self) -> Vec<Account> {
|
||||
let mut out_vec = Vec::new();
|
||||
|
||||
let path = self.full_accounts_path();
|
||||
|
||||
match fs::read_dir(path) {
|
||||
Ok(iter) => {
|
||||
for entry in iter {
|
||||
match entry {
|
||||
Ok(acc_entry) => {
|
||||
match handle_account_entry(acc_entry) {
|
||||
Ok(quest) => out_vec.push(quest),
|
||||
Err(error) => {
|
||||
eprintln!("Error on loading single account: {error}");
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
eprintln!("Error on loading single account: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
eprintln!("Error on loading accounts: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
println!("Loaded {} accounts successfully", out_vec.len());
|
||||
|
||||
out_vec
|
||||
}
|
||||
}
|
||||
|
|
|
|||
67
src/lib.rs
67
src/lib.rs
|
|
@ -2,8 +2,75 @@
|
|||
|
||||
#![warn(missing_docs)]
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
pub mod account;
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod map;
|
||||
pub mod quest;
|
||||
|
||||
/// Trait for objects that are internally used in squad_quest.
|
||||
/// Contains functions and methods to interact with file system.
|
||||
/// Files are saved in TOML format.
|
||||
pub trait SquadObject {
|
||||
/// Parse SquadObject TOML or return error
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// use squad_quest::{SquadObject,error::Error,quest::Quest};
|
||||
/// # fn main() {
|
||||
/// # let _ = wrapper();
|
||||
/// # }
|
||||
///
|
||||
/// # fn wrapper() -> Result<(), Error> {
|
||||
/// let path = "quests/0.toml".into();
|
||||
///
|
||||
/// let quest = Quest::load(path)?;
|
||||
/// #
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
fn load(path: PathBuf) -> Result<Self, Error> where Self: Sized;
|
||||
|
||||
/// Check if given file is a SquadObject, then delete it or raise an error.
|
||||
/// If file is not a quest, raises [Error::TomlDeserializeError]
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// use squad_quest::{SquadObject,error::Error,quest::Quest};
|
||||
///
|
||||
/// let path = "quests/0.toml".into();
|
||||
///
|
||||
/// if let Err(error) = Quest::delete(path) {
|
||||
/// // handle the error
|
||||
/// }
|
||||
/// ```
|
||||
fn delete(path: PathBuf) -> Result<(), Error>;
|
||||
|
||||
/// Save SquadObject to given folder in TOML format.
|
||||
/// File will be saved as `{id}.toml`.
|
||||
/// If file exists, this method will override it.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # fn main() {
|
||||
/// use squad_quest::{SquadObject,error::Error,quest::Quest};
|
||||
/// use std::path::PathBuf;
|
||||
///
|
||||
/// let quest = Quest::default();
|
||||
///
|
||||
/// let path: PathBuf = "quests".into();
|
||||
/// # let path2 = path.clone();
|
||||
///
|
||||
/// if let Err(error) = quest.save(path) {
|
||||
/// // handle the error
|
||||
/// }
|
||||
/// # let filename = format!("{}.toml", quest.id);
|
||||
/// # let _ = Quest::delete(path2.with_file_name(filename));
|
||||
/// # }
|
||||
/// ```
|
||||
fn save(&self, path: PathBuf) -> Result<(), Error>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
//! Module for handling quest loading errors
|
||||
|
||||
use std::{fmt, path::PathBuf};
|
||||
|
||||
/// Error raised when trying to parse quest file
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum QuestError {
|
||||
/// Given path is not a file
|
||||
IsNotAFile(PathBuf),
|
||||
/// std::io::Error happenned when loading
|
||||
IoError(std::io::Error),
|
||||
/// toml::ser::Error happened when loading
|
||||
TomlSerializeError(toml::ser::Error),
|
||||
/// toml::de::Error happened when loading
|
||||
TomlDeserializeError(toml::de::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for QuestError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
QuestError::IsNotAFile(path) => write!(f, "{:?} is not a file", path),
|
||||
QuestError::IoError(error) => write!(f, "io error: {error}"),
|
||||
QuestError::TomlSerializeError(error) => write!(f, "serialize error: {error}"),
|
||||
QuestError::TomlDeserializeError(error) => write!(f, "parse error: {error}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
use std::{fs, io::Write, path::PathBuf};
|
||||
|
||||
use serde::{ Serialize, Deserialize };
|
||||
use crate::error::Error;
|
||||
use crate::{SquadObject, error::Error};
|
||||
use toml::value::Date;
|
||||
|
||||
/// Difficulty of the quest
|
||||
|
|
@ -85,29 +85,12 @@ impl Default for Quest {
|
|||
}
|
||||
}
|
||||
|
||||
impl Quest {
|
||||
/// Parse quest TOML or return error
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// use squad_quest::{error::Error,quest::Quest};
|
||||
/// # fn main() {
|
||||
/// # let _ = wrapper();
|
||||
/// # }
|
||||
///
|
||||
/// # fn wrapper() -> Result<(), Error> {
|
||||
/// let path = "quests/0.toml".into();
|
||||
///
|
||||
/// let quest = Quest::load(path)?;
|
||||
/// #
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn load(path: PathBuf) -> Result<Self, Error> {
|
||||
impl SquadObject for Quest {
|
||||
fn load(path: PathBuf) -> Result<Self, Error> {
|
||||
match std::fs::read_to_string(path) {
|
||||
Ok(string) => {
|
||||
match toml::from_str::<Quest>(&string) {
|
||||
Ok(quest) => Ok(quest),
|
||||
match toml::from_str::<Self>(&string) {
|
||||
Ok(object) => Ok(object),
|
||||
Err(error) => Err(Error::TomlDeserializeError(error))
|
||||
}
|
||||
},
|
||||
|
|
@ -115,20 +98,7 @@ impl Quest {
|
|||
}
|
||||
}
|
||||
|
||||
/// Check if given file is a quest, then delete it or raise an error.
|
||||
/// If file is not a quest, raises [QuestError::TomlDeserializeError]
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// use squad_quest::{error::Error,quest::Quest};
|
||||
///
|
||||
/// let path = "quests/0.toml".into();
|
||||
///
|
||||
/// if let Err(error) = Quest::delete(path) {
|
||||
/// // handle the error
|
||||
/// }
|
||||
/// ```
|
||||
pub fn delete(path: PathBuf) -> Result<(), Error> {
|
||||
fn delete(path: PathBuf) -> Result<(), Error> {
|
||||
match Quest::load(path.clone()) {
|
||||
Ok(_) => {
|
||||
if let Err(error) = fs::remove_file(path) {
|
||||
|
|
@ -141,29 +111,7 @@ impl Quest {
|
|||
}
|
||||
}
|
||||
|
||||
/// Save quest to given folder in TOML format.
|
||||
/// File will be saved as `{id}.toml`.
|
||||
/// If file exists, this method will override it.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # fn main() {
|
||||
/// use squad_quest::{error::Error,quest::Quest};
|
||||
/// use std::path::PathBuf;
|
||||
///
|
||||
/// let quest = Quest::default();
|
||||
///
|
||||
/// let path: PathBuf = "quests".into();
|
||||
/// # let path2 = path.clone();
|
||||
///
|
||||
/// if let Err(error) = quest.save(path) {
|
||||
/// // handle the error
|
||||
/// }
|
||||
/// # let filename = format!("{}.toml", quest.id);
|
||||
/// # let _ = Quest::delete(path2.with_file_name(filename));
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn save(&self, path: PathBuf) -> Result<(), Error> {
|
||||
fn save(&self, path: PathBuf) -> Result<(), Error> {
|
||||
let filename = format!("{}.toml", self.id);
|
||||
let mut full_path = path;
|
||||
full_path.push(filename);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use squad_quest::{config::Config,error::Error,quest::Quest};
|
||||
use squad_quest::{SquadObject, config::Config, error::Error, quest::Quest};
|
||||
use std::path::PathBuf;
|
||||
|
||||
const CONFIG_PATH: &str = "tests/io/config.toml";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use squad_quest::{config::Config, quest::Quest};
|
||||
use squad_quest::{account::Account, config::Config, quest::Quest};
|
||||
|
||||
static CONFIG_PATH: &str = "./tests/main/config.toml";
|
||||
|
||||
|
|
@ -43,3 +43,41 @@ fn quest_one() {
|
|||
|
||||
assert_eq!(*quest, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_accounts() {
|
||||
let config = Config::load(CONFIG_PATH.into());
|
||||
let accounts = config.load_accounts();
|
||||
|
||||
assert_eq!(accounts.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_account_is_default() {
|
||||
let config = Config::load(CONFIG_PATH.into());
|
||||
let accounts = config.load_accounts();
|
||||
|
||||
let default = Account::default();
|
||||
|
||||
let account = accounts.iter().find(|a| a.id == default.id).unwrap();
|
||||
|
||||
assert_eq!(*account, default);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_test() {
|
||||
let config = Config::load(CONFIG_PATH.into());
|
||||
|
||||
let expected = Account {
|
||||
id: "test".to_string(),
|
||||
balance: 150,
|
||||
location: 0,
|
||||
quests_completed: vec![0],
|
||||
rooms_unlocked: Vec::new()
|
||||
};
|
||||
|
||||
let accounts = config.load_accounts();
|
||||
let account = accounts.iter().find(|a| a.id == expected.id).unwrap();
|
||||
|
||||
assert_eq!(*account, expected);
|
||||
}
|
||||
|
|
|
|||
1
tests/main/accounts/none.toml
Normal file
1
tests/main/accounts/none.toml
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Empty account for testing
|
||||
5
tests/main/accounts/test.toml
Normal file
5
tests/main/accounts/test.toml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
id = "test"
|
||||
balance = 150
|
||||
location = 0
|
||||
quests_completed = [ 0 ]
|
||||
rooms_unlocked = []
|
||||
Loading…
Add table
Add a link
Reference in a new issue