Skip to content

Commit aaba879

Browse files
authored
Merge pull request #33 from iyanumajekodunmi756/feat/error-handling-module
feat: add error handling module with AppError enum
2 parents 82b48ff + c948165 commit aaba879

8 files changed

Lines changed: 334 additions & 119 deletions

File tree

src/config/mod.rs

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
//! The resulting [`Config`] is a plain typed struct accessible anywhere in the
66
//! application.
77
8+
use crate::errors::AppError;
89
use serde::{Deserialize, Serialize};
910
use std::fs;
1011
use std::path::Path;
@@ -50,14 +51,14 @@ impl Config {
5051
/// - Returns the parsed config on success.
5152
/// - Returns [`Config::default`] if the file does not exist.
5253
/// - Returns an error for IO errors or malformed TOML.
53-
pub fn load(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
54+
pub fn load(path: impl AsRef<Path>) -> Result<Self, AppError> {
5455
let path = path.as_ref();
5556
if !path.exists() {
5657
return Ok(Config::default());
5758
}
58-
let raw = fs::read_to_string(path).map_err(ConfigError::Io)?;
59+
let raw = fs::read_to_string(path).map_err(AppError::Io)?;
5960
let partial: PartialConfig =
60-
toml::from_str(&raw).map_err(|e| ConfigError::Parse(e.to_string()))?;
61+
toml::from_str(&raw).map_err(|e| AppError::ConfigParse(e.to_string()))?;
6162
Ok(partial.into_config())
6263
}
6364
}
@@ -87,33 +88,12 @@ impl PartialConfig {
8788
}
8889
}
8990

90-
// ── Error type ───────────────────────────────────────────────────────────────
91-
92-
/// Errors that can occur while loading configuration.
93-
#[derive(Debug)]
94-
pub enum ConfigError {
95-
/// An underlying IO error (e.g. permission denied).
96-
Io(std::io::Error),
97-
/// The TOML content could not be parsed.
98-
Parse(String),
99-
}
100-
101-
impl std::fmt::Display for ConfigError {
102-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103-
match self {
104-
ConfigError::Io(e) => write!(f, "config IO error: {e}"),
105-
ConfigError::Parse(msg) => write!(f, "config parse error: {msg}"),
106-
}
107-
}
108-
}
109-
110-
impl std::error::Error for ConfigError {}
111-
11291
// ── Tests ────────────────────────────────────────────────────────────────────
11392

11493
#[cfg(test)]
11594
mod tests {
11695
use super::*;
96+
use crate::errors::AppError;
11797
use std::io::Write;
11898
use tempfile::NamedTempFile;
11999

@@ -174,7 +154,7 @@ score_multiplier = 2.5
174154
fn invalid_toml_returns_parse_error() {
175155
let file = write_toml("not = [ valid toml");
176156
let err = Config::load(file.path()).expect_err("should fail");
177-
assert!(matches!(err, ConfigError::Parse(_)));
157+
assert!(matches!(err, AppError::ConfigParse(_)));
178158
}
179159

180160
#[test]

src/errors/mod.rs

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
//! Errors module.
2+
//!
3+
//! Centralises all custom error types and handling logic into a dedicated
4+
//! [`AppError`] enum that every module in the codebase uses instead of
5+
//! ad-hoc error strings or module-specific error types.
6+
7+
use std::fmt;
8+
9+
/// Top-level error type covering all major error categories in the game.
10+
///
11+
/// Every module converts its internal errors into [`AppError`] so that
12+
/// callers can handle failures uniformly.
13+
#[derive(Debug)]
14+
pub enum AppError {
15+
// ── I/O ──────────────────────────────────────────────────────────────────
16+
/// An underlying I/O error (e.g. file not found, permission denied).
17+
Io(std::io::Error),
18+
19+
// ── Serialisation ────────────────────────────────────────────────────────
20+
/// A JSON serialisation or deserialisation error.
21+
Serde(serde_json::Error),
22+
23+
// ── Config ───────────────────────────────────────────────────────────────
24+
/// The TOML configuration could not be parsed.
25+
ConfigParse(String),
26+
27+
// ── Input ────────────────────────────────────────────────────────────────
28+
/// The input string was not recognised as a valid game action.
29+
InputInvalid(String),
30+
/// No input was provided.
31+
InputEmpty,
32+
33+
// ── NFT / Achievements ───────────────────────────────────────────────────
34+
/// The achievement has already been minted for this player.
35+
NftAlreadyMinted {
36+
/// The player ID.
37+
player_id: String,
38+
/// The milestone type that was already minted.
39+
milestone_type: String,
40+
},
41+
42+
// ── Player ───────────────────────────────────────────────────────────────
43+
/// A player with the given identifier could not be found.
44+
PlayerNotFound(String),
45+
46+
// ── Inventory ────────────────────────────────────────────────────────────
47+
/// An item with the given ID does not exist in the inventory.
48+
InventoryItemNotFound(String),
49+
50+
// ── Puzzle ───────────────────────────────────────────────────────────────
51+
/// A general puzzle-domain error.
52+
Puzzle(String),
53+
54+
// ── Leaderboard ──────────────────────────────────────────────────────────
55+
/// A leaderboard-domain error.
56+
Leaderboard(String),
57+
}
58+
59+
// ── Display ──────────────────────────────────────────────────────────────────
60+
61+
impl fmt::Display for AppError {
62+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63+
match self {
64+
AppError::Io(e) => write!(f, "I/O error: {e}"),
65+
AppError::Serde(e) => write!(f, "serialisation error: {e}"),
66+
AppError::ConfigParse(msg) => write!(f, "config parse error: {msg}"),
67+
AppError::InputInvalid(s) => write!(f, "invalid input: '{s}'"),
68+
AppError::InputEmpty => write!(f, "input cannot be empty"),
69+
AppError::NftAlreadyMinted {
70+
player_id,
71+
milestone_type,
72+
} => {
73+
write!(
74+
f,
75+
"achievement '{milestone_type}' already minted for player '{player_id}'"
76+
)
77+
}
78+
AppError::PlayerNotFound(id) => write!(f, "player '{id}' not found"),
79+
AppError::InventoryItemNotFound(id) => {
80+
write!(f, "item '{id}' not found in inventory")
81+
}
82+
AppError::Puzzle(msg) => write!(f, "puzzle error: {msg}"),
83+
AppError::Leaderboard(msg) => write!(f, "leaderboard error: {msg}"),
84+
}
85+
}
86+
}
87+
88+
// ── std::error::Error ────────────────────────────────────────────────────────
89+
90+
impl std::error::Error for AppError {
91+
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
92+
match self {
93+
AppError::Io(e) => Some(e),
94+
AppError::Serde(e) => Some(e),
95+
_ => None,
96+
}
97+
}
98+
}
99+
100+
// ── From implementations ─────────────────────────────────────────────────────
101+
102+
impl From<std::io::Error> for AppError {
103+
fn from(e: std::io::Error) -> Self {
104+
AppError::Io(e)
105+
}
106+
}
107+
108+
impl From<serde_json::Error> for AppError {
109+
fn from(e: serde_json::Error) -> Self {
110+
AppError::Serde(e)
111+
}
112+
}
113+
114+
// ── Tests ────────────────────────────────────────────────────────────────────
115+
116+
#[cfg(test)]
117+
mod tests {
118+
use super::*;
119+
use std::error::Error;
120+
121+
// ── Display tests ────────────────────────────────────────────────────────
122+
123+
#[test]
124+
fn display_io_error() {
125+
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
126+
let err = AppError::Io(io_err);
127+
let msg = err.to_string();
128+
assert!(msg.contains("I/O error"));
129+
assert!(msg.contains("file missing"));
130+
}
131+
132+
#[test]
133+
fn display_serde_error() {
134+
let serde_err = serde_json::from_str::<()>("invalid").unwrap_err();
135+
let err = AppError::Serde(serde_err);
136+
let msg = err.to_string();
137+
assert!(msg.contains("serialisation error"));
138+
}
139+
140+
#[test]
141+
fn display_config_parse() {
142+
let err = AppError::ConfigParse("bad toml".into());
143+
assert_eq!(err.to_string(), "config parse error: bad toml");
144+
}
145+
146+
#[test]
147+
fn display_input_invalid() {
148+
let err = AppError::InputInvalid("xyz".into());
149+
assert_eq!(err.to_string(), "invalid input: 'xyz'");
150+
}
151+
152+
#[test]
153+
fn display_input_empty() {
154+
let err = AppError::InputEmpty;
155+
assert_eq!(err.to_string(), "input cannot be empty");
156+
}
157+
158+
#[test]
159+
fn display_nft_already_minted() {
160+
let err = AppError::NftAlreadyMinted {
161+
player_id: "p1".into(),
162+
milestone_type: "level10".into(),
163+
};
164+
assert_eq!(
165+
err.to_string(),
166+
"achievement 'level10' already minted for player 'p1'"
167+
);
168+
}
169+
170+
#[test]
171+
fn display_player_not_found() {
172+
let err = AppError::PlayerNotFound("hero".into());
173+
assert_eq!(err.to_string(), "player 'hero' not found");
174+
}
175+
176+
#[test]
177+
fn display_inventory_not_found() {
178+
let err = AppError::InventoryItemNotFound("sword".into());
179+
assert_eq!(err.to_string(), "item 'sword' not found in inventory");
180+
}
181+
182+
#[test]
183+
fn display_puzzle_error() {
184+
let err = AppError::Puzzle("invalid move".into());
185+
assert_eq!(err.to_string(), "puzzle error: invalid move");
186+
}
187+
188+
#[test]
189+
fn display_leaderboard_error() {
190+
let err = AppError::Leaderboard("capacity reached".into());
191+
assert_eq!(err.to_string(), "leaderboard error: capacity reached");
192+
}
193+
194+
// ── Error trait tests ────────────────────────────────────────────────────
195+
196+
#[test]
197+
fn io_error_has_source() {
198+
let inner = std::io::Error::new(std::io::ErrorKind::Other, "reason");
199+
let err = AppError::Io(inner);
200+
assert!(err.source().is_some());
201+
}
202+
203+
#[test]
204+
fn serde_error_has_source() {
205+
let inner = serde_json::from_str::<()>("[").unwrap_err();
206+
let err = AppError::Serde(inner);
207+
assert!(err.source().is_some());
208+
}
209+
210+
#[test]
211+
fn non_wrapping_variants_have_no_source() {
212+
assert!(AppError::ConfigParse("x".into()).source().is_none());
213+
assert!(AppError::InputInvalid("x".into()).source().is_none());
214+
assert!(AppError::InputEmpty.source().is_none());
215+
assert!(AppError::PlayerNotFound("x".into()).source().is_none());
216+
assert!(AppError::InventoryItemNotFound("x".into()).source().is_none());
217+
assert!(AppError::Puzzle("x".into()).source().is_none());
218+
assert!(AppError::Leaderboard("x".into()).source().is_none());
219+
assert!(AppError::NftAlreadyMinted {
220+
player_id: "p".into(),
221+
milestone_type: "m".into(),
222+
}
223+
.source()
224+
.is_none());
225+
}
226+
227+
// ── From impl tests ──────────────────────────────────────────────────────
228+
229+
#[test]
230+
fn from_io_error() {
231+
let io = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
232+
let app: AppError = io.into();
233+
assert!(matches!(app, AppError::Io(_)));
234+
}
235+
236+
#[test]
237+
fn from_serde_error() {
238+
let serde = serde_json::from_str::<()>("%%%").unwrap_err();
239+
let app: AppError = serde.into();
240+
assert!(matches!(app, AppError::Serde(_)));
241+
}
242+
}

0 commit comments

Comments
 (0)