Skip to content

Commit b5d1515

Browse files
authored
Merge pull request #107 from ryanoneill/feature/structured-errors
Replace string-based error variants with structured fields
2 parents 41832db + ff008f3 commit b5d1515

File tree

1 file changed

+161
-22
lines changed

1 file changed

+161
-22
lines changed

src/error.rs

Lines changed: 161 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -28,38 +28,132 @@ pub type BoxedError = Box<dyn std::error::Error + Send + Sync + 'static>;
2828
/// Structured error type for the Envision framework.
2929
///
3030
/// Represents the different categories of errors that can occur when
31-
/// using Envision. Each variant provides context about the failure mode.
31+
/// using Envision. Each variant provides structured context about the
32+
/// failure mode, enabling callers to match on specific fields.
3233
///
3334
/// # Example
3435
///
3536
/// ```rust
3637
/// use envision::error::EnvisionError;
3738
///
38-
/// let err = EnvisionError::Config("invalid theme name".into());
39-
/// assert_eq!(err.to_string(), "configuration error: invalid theme name");
39+
/// let err = EnvisionError::config("theme", "invalid theme name");
40+
/// assert_eq!(
41+
/// err.to_string(),
42+
/// "configuration error: field `theme`: invalid theme name"
43+
/// );
4044
/// ```
4145
#[derive(Debug)]
4246
pub enum EnvisionError {
4347
/// An I/O error occurred (terminal, file system, etc.).
4448
Io(std::io::Error),
4549

4650
/// A rendering error occurred.
47-
Render(String),
51+
Render {
52+
/// The component that failed to render.
53+
component: &'static str,
54+
/// Details about the rendering failure.
55+
detail: String,
56+
},
4857

4958
/// A configuration error occurred.
50-
Config(String),
59+
Config {
60+
/// The configuration field that caused the error.
61+
field: String,
62+
/// The reason the configuration is invalid.
63+
reason: String,
64+
},
5165

5266
/// A subscription error occurred.
53-
Subscription(String),
67+
Subscription {
68+
/// The type of subscription that failed.
69+
subscription_type: &'static str,
70+
/// Details about the subscription failure.
71+
detail: String,
72+
},
73+
}
74+
75+
impl EnvisionError {
76+
/// Creates a rendering error.
77+
///
78+
/// # Example
79+
///
80+
/// ```rust
81+
/// use envision::error::EnvisionError;
82+
///
83+
/// let err = EnvisionError::render("ProgressBar", "width must be positive");
84+
/// assert_eq!(
85+
/// err.to_string(),
86+
/// "render error: component `ProgressBar`: width must be positive"
87+
/// );
88+
/// ```
89+
pub fn render(component: &'static str, detail: impl Into<String>) -> Self {
90+
EnvisionError::Render {
91+
component,
92+
detail: detail.into(),
93+
}
94+
}
95+
96+
/// Creates a configuration error.
97+
///
98+
/// # Example
99+
///
100+
/// ```rust
101+
/// use envision::error::EnvisionError;
102+
///
103+
/// let err = EnvisionError::config("theme", "unknown theme name");
104+
/// assert_eq!(
105+
/// err.to_string(),
106+
/// "configuration error: field `theme`: unknown theme name"
107+
/// );
108+
/// ```
109+
pub fn config(field: impl Into<String>, reason: impl Into<String>) -> Self {
110+
EnvisionError::Config {
111+
field: field.into(),
112+
reason: reason.into(),
113+
}
114+
}
115+
116+
/// Creates a subscription error.
117+
///
118+
/// # Example
119+
///
120+
/// ```rust
121+
/// use envision::error::EnvisionError;
122+
///
123+
/// let err = EnvisionError::subscription("tick", "interval too small");
124+
/// assert_eq!(
125+
/// err.to_string(),
126+
/// "subscription error: type `tick`: interval too small"
127+
/// );
128+
/// ```
129+
pub fn subscription(subscription_type: &'static str, detail: impl Into<String>) -> Self {
130+
EnvisionError::Subscription {
131+
subscription_type,
132+
detail: detail.into(),
133+
}
134+
}
54135
}
55136

56137
impl fmt::Display for EnvisionError {
57138
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58139
match self {
59140
EnvisionError::Io(err) => write!(f, "I/O error: {}", err),
60-
EnvisionError::Render(msg) => write!(f, "render error: {}", msg),
61-
EnvisionError::Config(msg) => write!(f, "configuration error: {}", msg),
62-
EnvisionError::Subscription(msg) => write!(f, "subscription error: {}", msg),
141+
EnvisionError::Render { component, detail } => {
142+
write!(f, "render error: component `{}`: {}", component, detail)
143+
}
144+
EnvisionError::Config { field, reason } => {
145+
write!(f, "configuration error: field `{}`: {}", field, reason)
146+
}
147+
EnvisionError::Subscription {
148+
subscription_type,
149+
detail,
150+
} => {
151+
write!(
152+
f,
153+
"subscription error: type `{}`: {}",
154+
subscription_type, detail
155+
)
156+
}
63157
}
64158
}
65159
}
@@ -68,9 +162,9 @@ impl std::error::Error for EnvisionError {
68162
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
69163
match self {
70164
EnvisionError::Io(err) => Some(err),
71-
EnvisionError::Render(_) | EnvisionError::Config(_) | EnvisionError::Subscription(_) => {
72-
None
73-
}
165+
EnvisionError::Render { .. }
166+
| EnvisionError::Config { .. }
167+
| EnvisionError::Subscription { .. } => None,
74168
}
75169
}
76170
}
@@ -94,22 +188,28 @@ mod tests {
94188

95189
#[test]
96190
fn render_error_display() {
97-
let err = EnvisionError::Render("failed to draw widget".into());
98-
assert_eq!(err.to_string(), "render error: failed to draw widget");
191+
let err = EnvisionError::render("ProgressBar", "failed to draw widget");
192+
assert_eq!(
193+
err.to_string(),
194+
"render error: component `ProgressBar`: failed to draw widget"
195+
);
99196
}
100197

101198
#[test]
102199
fn config_error_display() {
103-
let err = EnvisionError::Config("invalid theme name".into());
104-
assert_eq!(err.to_string(), "configuration error: invalid theme name");
200+
let err = EnvisionError::config("theme", "invalid theme name");
201+
assert_eq!(
202+
err.to_string(),
203+
"configuration error: field `theme`: invalid theme name"
204+
);
105205
}
106206

107207
#[test]
108208
fn subscription_error_display() {
109-
let err = EnvisionError::Subscription("tick interval too small".into());
209+
let err = EnvisionError::subscription("tick", "interval too small");
110210
assert_eq!(
111211
err.to_string(),
112-
"subscription error: tick interval too small"
212+
"subscription error: type `tick`: interval too small"
113213
);
114214
}
115215

@@ -129,25 +229,25 @@ mod tests {
129229

130230
#[test]
131231
fn render_error_no_source() {
132-
let err = EnvisionError::Render("bad render".into());
232+
let err = EnvisionError::render("Widget", "bad render");
133233
assert!(std::error::Error::source(&err).is_none());
134234
}
135235

136236
#[test]
137237
fn config_error_no_source() {
138-
let err = EnvisionError::Config("bad config".into());
238+
let err = EnvisionError::config("key", "bad config");
139239
assert!(std::error::Error::source(&err).is_none());
140240
}
141241

142242
#[test]
143243
fn subscription_error_no_source() {
144-
let err = EnvisionError::Subscription("bad sub".into());
244+
let err = EnvisionError::subscription("tick", "bad sub");
145245
assert!(std::error::Error::source(&err).is_none());
146246
}
147247

148248
#[test]
149249
fn debug_format() {
150-
let err = EnvisionError::Config("test".into());
250+
let err = EnvisionError::config("key", "test");
151251
let debug = format!("{:?}", err);
152252
assert!(debug.contains("Config"));
153253
assert!(debug.contains("test"));
@@ -160,4 +260,43 @@ mod tests {
160260
}
161261
assert!(returns_boxed().is_err());
162262
}
263+
264+
#[test]
265+
fn render_error_fields_accessible() {
266+
let err = EnvisionError::render("Table", "column overflow");
267+
match err {
268+
EnvisionError::Render { component, detail } => {
269+
assert_eq!(component, "Table");
270+
assert_eq!(detail, "column overflow");
271+
}
272+
_ => panic!("expected Render variant"),
273+
}
274+
}
275+
276+
#[test]
277+
fn config_error_fields_accessible() {
278+
let err = EnvisionError::config("tick_rate", "must be positive");
279+
match err {
280+
EnvisionError::Config { field, reason } => {
281+
assert_eq!(field, "tick_rate");
282+
assert_eq!(reason, "must be positive");
283+
}
284+
_ => panic!("expected Config variant"),
285+
}
286+
}
287+
288+
#[test]
289+
fn subscription_error_fields_accessible() {
290+
let err = EnvisionError::subscription("interval", "already running");
291+
match err {
292+
EnvisionError::Subscription {
293+
subscription_type,
294+
detail,
295+
} => {
296+
assert_eq!(subscription_type, "interval");
297+
assert_eq!(detail, "already running");
298+
}
299+
_ => panic!("expected Subscription variant"),
300+
}
301+
}
163302
}

0 commit comments

Comments
 (0)