diff --git a/Cargo.lock b/Cargo.lock index a8145e0..1c88b30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -332,7 +332,7 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "squad-quest" -version = "0.1.0" +version = "0.2.0" dependencies = [ "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index b981d44..f912a91 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "squad-quest" -version = "0.1.0" +version = "0.2.0" edition = "2024" [dependencies] diff --git a/src/account/mod.rs b/src/account/mod.rs index 0960db8..5ed0373 100644 --- a/src/account/mod.rs +++ b/src/account/mod.rs @@ -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, + + /// Vec of rooms unlocked by this user + pub rooms_unlocked: Vec, +} + +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 { + match std::fs::read_to_string(path) { + Ok(string) => { + match toml::from_str::(&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(()) + } } diff --git a/src/bin/cli.rs b/src/bin/cli.rs index e5ee968..4d06a2a 100644 --- a/src/bin/cli.rs +++ b/src/bin/cli.rs @@ -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}; diff --git a/src/config/mod.rs b/src/config/mod.rs index 5057e16..d5a86da 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -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::load(path) } +fn handle_account_entry(account_entry: DirEntry) -> Result{ + 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 { + 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 + } } diff --git a/src/lib.rs b/src/lib.rs index 90670d7..db152a5 100644 --- a/src/lib.rs +++ b/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 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>; +} diff --git a/src/quest/error.rs b/src/quest/error.rs deleted file mode 100644 index 55ddbcf..0000000 --- a/src/quest/error.rs +++ /dev/null @@ -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}") - } - } -} diff --git a/src/quest/mod.rs b/src/quest/mod.rs index 6cbaf7d..b8d7886 100644 --- a/src/quest/mod.rs +++ b/src/quest/mod.rs @@ -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 { +impl SquadObject for Quest { + fn load(path: PathBuf) -> Result { match std::fs::read_to_string(path) { Ok(string) => { - match toml::from_str::(&string) { - Ok(quest) => Ok(quest), + match toml::from_str::(&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); diff --git a/tests/io.rs b/tests/io.rs index 90068f5..983e4cd 100644 --- a/tests/io.rs +++ b/tests/io.rs @@ -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"; diff --git a/tests/main.rs b/tests/main.rs index 1210380..b582e5c 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -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); +} diff --git a/tests/main/accounts/none.toml b/tests/main/accounts/none.toml new file mode 100644 index 0000000..ca0a7fb --- /dev/null +++ b/tests/main/accounts/none.toml @@ -0,0 +1 @@ +# Empty account for testing diff --git a/tests/main/accounts/test.toml b/tests/main/accounts/test.toml new file mode 100644 index 0000000..439a946 --- /dev/null +++ b/tests/main/accounts/test.toml @@ -0,0 +1,5 @@ +id = "test" +balance = 150 +location = 0 +quests_completed = [ 0 ] +rooms_unlocked = []