1#![allow(dead_code)]
20#![allow(clippy::module_name_repetitions)]
21
22use std::collections::HashMap;
23
24#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct ErrorContext {
42 component: String,
43 operation: String,
44 message: String,
45 fields: HashMap<String, String>,
46}
47
48impl ErrorContext {
49 #[must_use]
51 pub fn new(component: &str, operation: &str, message: &str) -> Self {
52 Self {
53 component: component.to_owned(),
54 operation: operation.to_owned(),
55 message: message.to_owned(),
56 fields: HashMap::new(),
57 }
58 }
59
60 #[inline]
62 #[must_use]
63 pub fn component(&self) -> &str {
64 &self.component
65 }
66
67 #[inline]
69 #[must_use]
70 pub fn operation(&self) -> &str {
71 &self.operation
72 }
73
74 #[inline]
76 #[must_use]
77 pub fn message(&self) -> &str {
78 &self.message
79 }
80
81 pub fn with_field(&mut self, key: &str, value: &str) -> &mut Self {
86 self.fields.insert(key.to_owned(), value.to_owned());
87 self
88 }
89
90 #[must_use]
92 pub fn field(&self, key: &str) -> Option<&str> {
93 self.fields.get(key).map(String::as_str)
94 }
95
96 pub fn fields(&self) -> impl Iterator<Item = (&str, &str)> {
98 self.fields.iter().map(|(k, v)| (k.as_str(), v.as_str()))
99 }
100}
101
102impl std::fmt::Display for ErrorContext {
103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104 write!(
105 f,
106 "[{}::{}] {}",
107 self.component, self.operation, self.message
108 )
109 }
110}
111
112#[derive(Debug, Clone)]
130pub struct ErrorChain {
131 frames: Vec<ErrorContext>,
132}
133
134impl ErrorChain {
135 #[must_use]
137 pub fn root(ctx: ErrorContext) -> Self {
138 Self { frames: vec![ctx] }
139 }
140
141 #[must_use]
143 pub fn empty() -> Self {
144 Self { frames: Vec::new() }
145 }
146
147 pub fn push(&mut self, ctx: ErrorContext) {
149 self.frames.push(ctx);
150 }
151
152 #[must_use]
154 pub fn depth(&self) -> usize {
155 self.frames.len()
156 }
157
158 #[must_use]
160 pub fn is_empty(&self) -> bool {
161 self.frames.is_empty()
162 }
163
164 #[must_use]
166 pub fn root_cause(&self) -> Option<&ErrorContext> {
167 self.frames.first()
168 }
169
170 #[must_use]
172 pub fn outermost(&self) -> Option<&ErrorContext> {
173 self.frames.last()
174 }
175
176 pub fn iter(&self) -> impl Iterator<Item = &ErrorContext> {
178 self.frames.iter()
179 }
180
181 #[must_use]
183 pub fn involves(&self, component: &str) -> bool {
184 self.frames.iter().any(|f| f.component() == component)
185 }
186}
187
188impl std::fmt::Display for ErrorChain {
189 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190 for (i, frame) in self.frames.iter().enumerate() {
191 if i > 0 {
192 write!(f, " -> ")?;
193 }
194 write!(f, "{frame}")?;
195 }
196 Ok(())
197 }
198}
199
200#[derive(Debug, Default)]
218pub struct ErrorContextBuilder {
219 component: String,
220 operation: String,
221 message: String,
222 fields: HashMap<String, String>,
223}
224
225impl ErrorContextBuilder {
226 #[must_use]
228 pub fn new(component: &str, operation: &str) -> Self {
229 Self {
230 component: component.to_owned(),
231 operation: operation.to_owned(),
232 message: String::new(),
233 fields: HashMap::new(),
234 }
235 }
236
237 #[must_use]
239 pub fn message(mut self, msg: &str) -> Self {
240 msg.clone_into(&mut self.message);
241 self
242 }
243
244 #[must_use]
246 pub fn field(mut self, key: &str, value: &str) -> Self {
247 self.fields.insert(key.to_owned(), value.to_owned());
248 self
249 }
250
251 #[must_use]
253 pub fn build(self) -> ErrorContext {
254 ErrorContext {
255 component: self.component,
256 operation: self.operation,
257 message: self.message,
258 fields: self.fields,
259 }
260 }
261}
262
263#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn error_context_accessors() {
271 let ctx = ErrorContext::new("demuxer", "read_packet", "EOF");
272 assert_eq!(ctx.component(), "demuxer");
273 assert_eq!(ctx.operation(), "read_packet");
274 assert_eq!(ctx.message(), "EOF");
275 }
276
277 #[test]
278 fn error_context_with_field() {
279 let mut ctx = ErrorContext::new("codec", "decode", "error");
280 ctx.with_field("pts", "1000");
281 assert_eq!(ctx.field("pts"), Some("1000"));
282 }
283
284 #[test]
285 fn error_context_missing_field_is_none() {
286 let ctx = ErrorContext::new("x", "y", "z");
287 assert!(ctx.field("nonexistent").is_none());
288 }
289
290 #[test]
291 fn error_context_field_overwrite() {
292 let mut ctx = ErrorContext::new("a", "b", "c");
293 ctx.with_field("k", "v1");
294 ctx.with_field("k", "v2");
295 assert_eq!(ctx.field("k"), Some("v2"));
296 }
297
298 #[test]
299 fn error_context_display() {
300 let ctx = ErrorContext::new("muxer", "write", "disk full");
301 let s = ctx.to_string();
302 assert!(s.contains("muxer"));
303 assert!(s.contains("write"));
304 assert!(s.contains("disk full"));
305 }
306
307 #[test]
308 fn error_chain_root_depth_one() {
309 let ctx = ErrorContext::new("io", "read", "timeout");
310 let chain = ErrorChain::root(ctx);
311 assert_eq!(chain.depth(), 1);
312 }
313
314 #[test]
315 fn error_chain_push_increases_depth() {
316 let mut chain = ErrorChain::root(ErrorContext::new("a", "op", "msg"));
317 chain.push(ErrorContext::new("b", "op2", "msg2"));
318 assert_eq!(chain.depth(), 2);
319 }
320
321 #[test]
322 fn error_chain_root_cause() {
323 let ctx = ErrorContext::new("inner", "op", "root cause");
324 let chain = ErrorChain::root(ctx.clone());
325 assert_eq!(chain.root_cause(), Some(&ctx));
326 }
327
328 #[test]
329 fn error_chain_outermost() {
330 let mut chain = ErrorChain::root(ErrorContext::new("inner", "op", "cause"));
331 let outer = ErrorContext::new("outer", "handle", "context");
332 chain.push(outer.clone());
333 assert_eq!(chain.outermost(), Some(&outer));
334 }
335
336 #[test]
337 fn error_chain_involves() {
338 let mut chain = ErrorChain::root(ErrorContext::new("io", "read", "err"));
339 chain.push(ErrorContext::new("demuxer", "parse", "err2"));
340 assert!(chain.involves("io"));
341 assert!(chain.involves("demuxer"));
342 assert!(!chain.involves("encoder"));
343 }
344
345 #[test]
346 fn error_chain_empty() {
347 let chain = ErrorChain::empty();
348 assert!(chain.is_empty());
349 assert_eq!(chain.depth(), 0);
350 assert!(chain.root_cause().is_none());
351 assert!(chain.outermost().is_none());
352 }
353
354 #[test]
355 fn error_chain_display_multi_frame() {
356 let mut chain = ErrorChain::root(ErrorContext::new("a", "op", "first"));
357 chain.push(ErrorContext::new("b", "op2", "second"));
358 let s = chain.to_string();
359 assert!(s.contains("first"));
360 assert!(s.contains("second"));
361 assert!(s.contains("->"));
362 }
363
364 #[test]
365 fn builder_creates_correct_context() {
366 let ctx = ErrorContextBuilder::new("codec", "decode_frame")
367 .message("bitstream error")
368 .field("pts", "12345")
369 .build();
370 assert_eq!(ctx.component(), "codec");
371 assert_eq!(ctx.operation(), "decode_frame");
372 assert_eq!(ctx.message(), "bitstream error");
373 assert_eq!(ctx.field("pts"), Some("12345"));
374 }
375
376 #[test]
377 fn builder_default_message_is_empty() {
378 let ctx = ErrorContextBuilder::new("c", "op").build();
379 assert_eq!(ctx.message(), "");
380 }
381
382 #[test]
383 fn error_chain_iter_count_matches_depth() {
384 let mut chain = ErrorChain::root(ErrorContext::new("a", "op", "e1"));
385 chain.push(ErrorContext::new("b", "op2", "e2"));
386 chain.push(ErrorContext::new("c", "op3", "e3"));
387 assert_eq!(chain.iter().count(), chain.depth());
388 }
389}