problem/lib.rs
1/*!
2This crate introduces `Problem` type which can be used on high level APIs (e.g. in command line programs or prototypes) for which error handling boils down to:
3* reporting error message (e.g. log with `error!` macro),
4* aborting program on error other than a bug (e.g. using `panic!` macro),
5* bubbling up errors (e.g. with `?`),
6* ignoring errors (e.g. using `Result::ok`).
7
8# Usage
9The core type of this crate is `Problem`.
10It is used to capture error, backtrace (if enabled) and any additional context information and present it in user friendly way via `Display` implementation.
11
12This library also provides many additional extension traits and some functions that make it easy to construct `Problem` type in different situations
13as well as report or abort programs on error.
14It is recommended to import all the types and traits via perlude module: `use problem::prelude::*`.
15
16`Problem` stores error cause information as `Box<dyn Error>` to dealy construction of error message to when it is actually needed.
17Additionally `Problem` can also store backtrace `String` (if enabled) and a chain of additional context messages as `Vec<String>`.
18
19In order to support conversion from arbitary types implementing `Error` trait, `Problem` does not implement this trait.
20
21# Creating `Problem`
22There are multiple ways to crate `Problem` value.
23
24## Using constructor
25Using `Problem::from_error(error)` if `error` implements `Error` trait (via `Into<Box<dyn Error>>`).
26Note that `String` and `&str` implement `Into<Box<dyn Error>>` so `Problem` can be constructed directly from strings as well.
27
28```rust
29use problem::prelude::*;
30use std::io;
31
32Problem::from_error(io::Error::new(io::ErrorKind::InvalidInput, "boom!"));
33Problem::from_error(Box::new(io::Error::new(io::ErrorKind::InvalidInput, "boom!")));
34Problem::from_error("boom!");
35```
36
37Use `Problem::from_error_message(error)` if you don't want to give up ownership of error or only want to keep final message string in memory.
38
39```rust
40use problem::prelude::*;
41use std::io;
42
43let error = io::Error::new(io::ErrorKind::InvalidInput, "boom!");
44let problem = Problem::from_error_message(&error);
45
46drop(error);
47drop(problem);
48```
49
50## Using macro
51Using `problem!` macro an `Err` variant of `Result` containing `Problem` with formatted message can be constructed.
52
53```rust
54use problem::prelude::*;
55use std::io;
56
57fn foo() -> Result<(), Problem> {
58 problem!("Can't count to {}", 4)
59}
60
61assert_eq!(foo().unwrap_err().to_string(), "Can't count to 4");
62```
63
64## Implicitly
65Types implementing `Into<Box<dyn Error>>` trait can be converted to `Problem` via `From` trait. `?` will automatically convert types implementing `Error` to `Problem`.
66
67```rust
68use problem::prelude::*;
69
70fn foo() -> Result<String, Problem> {
71 let str = String::from_utf8(vec![0, 123, 255])?;
72 Ok(str)
73}
74
75assert_eq!(foo().unwrap_err().to_string(), "invalid utf-8 sequence of 1 bytes from index 2");
76```
77
78If `Error::cause` or `Error::source` is available, the error message from cause chain will be displayed.
79
80```rust
81use problem::prelude::*;
82use std::fmt;
83use std::error::Error;
84
85#[derive(Debug)]
86struct ErrorWithCause(std::string::FromUtf8Error);
87
88impl fmt::Display for ErrorWithCause {
89 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
90 write!(f, "bad things happened")
91 }
92}
93
94impl Error for ErrorWithCause {
95 fn cause(&self) -> Option<&dyn Error> {
96 Some(&self.0)
97 }
98}
99
100fn foo() -> Result<String, Problem> {
101 let str = String::from_utf8(vec![0, 123, 255]).map_err(ErrorWithCause)?;
102 Ok(str)
103}
104
105assert_eq!(foo().unwrap_err().to_string(), "bad things happened; caused by: invalid utf-8 sequence of 1 bytes from index 2");
106```
107
108## By explicitly mapping `Result`
109`Result<T, E>` can be mapped into `Result<T, Problem>` with `.map_problem()` function.
110
111```rust
112use problem::prelude::*;
113
114let res: Result<(), &'static str> = Err("oops");
115let problem: Result<(), Problem> = res.map_problem();
116
117assert_eq!(problem.unwrap_err().to_string(), "oops");
118```
119
120## By conversion of `Option` into `Result`
121`Option<T>` can be converted into `Result<T, Problem>` with `.ok_or_problem(problem)` function where `problem` implements `Into<Problem>`.
122
123```rust
124use problem::prelude::*;
125
126let opt: Option<()> = None;
127let problem: Result<(), Problem> = opt.ok_or_problem("oops");
128
129assert_eq!(problem.unwrap_err().to_string(), "oops");
130```
131
132## From `Result` with `Err` containing `Option`
133`.map_problem_or(problem)` method is implemented for `Result<O, Option<E>>` and will map to `Result<O, Problem>` with provided problem for `Err(None)` variant.
134This may be usefult when working with FFI.
135
136```rust
137use problem::prelude::*;
138
139let unknown: Result<(), Option<&'static str>> = Err(None);
140let known: Result<(), Option<&'static str>> = Err(Some("oops"));
141
142assert_eq!(unknown.map_problem_or("unknown error").unwrap_err().to_string(), "unknown error");
143assert_eq!(known.map_problem_or("unknown error").unwrap_err().to_string(), "oops");
144```
145
146There is also `.map_problem_or_else(problem_function)` variant provided that can be used to defer construction of error to error path.
147
148```rust
149use problem::prelude::*;
150
151let unknown: Result<(), Option<&'static str>> = Err(None);
152let known: Result<(), Option<&'static str>> = Err(Some("oops"));
153
154assert_eq!(unknown.map_problem_or_else(|| "unknown error").unwrap_err().to_string(), "unknown error");
155assert_eq!(known.map_problem_or_else(|| "unknown error").unwrap_err().to_string(), "oops");
156```
157
158# Adding context to `Problem`
159A context information that provides clues on which good path has been taken that led to error can be added to `Problem` object.
160
161Adding context is a good way to convert other error types to `Problem` as well as providing extra information on where that happened.
162
163## On `Result` error
164Method `.problem_while(message)` can be called on any `Result` value that error type can be converted to `Problem` to add context message (via `Dispaly` trait).
165
166```rust
167use problem::prelude::*;
168
169let res = String::from_utf8(vec![0, 123, 255]);
170
171assert_eq!(res.problem_while("creating string").unwrap_err().to_string(), "while creating string got error caused by: invalid utf-8 sequence of 1 bytes from index 2");
172```
173
174There is also `.problem_while_with(message_function)` variant provided that can be used to defer construction of context message to error path.
175
176```rust
177use problem::prelude::*;
178
179let res = String::from_utf8(vec![0, 123, 255]);
180
181assert_eq!(res.problem_while_with(|| "creating string").unwrap_err().to_string(), "while creating string got error caused by: invalid utf-8 sequence of 1 bytes from index 2");
182```
183
184## Using scope and `?`
185Function `in_context_of(message, closure)` can be used to wrap block of code in a closure that returns `Result`.
186
187This is useful when you want to add context to any error that can happen in the block of code and use `?` operator.
188The return type of the closure needs to be `Result<T, Problem>` but `?` operator can convert to `Problem` automatically.
189
190```rust
191use problem::prelude::*;
192
193let res = in_context_of("processing string", || {
194 let _s = String::from_utf8(vec![0, 123, 255])?;
195 // do some processing of _s
196 Ok(())
197});
198
199assert_eq!(res.unwrap_err().to_string(), "while processing string got error caused by: invalid utf-8 sequence of 1 bytes from index 2");
200```
201
202There is also `in_context_of_with(message_function, closure)` variant provided that can be used to defer construction of context message to error path.
203
204```rust
205use problem::prelude::*;
206
207let res = in_context_of_with(|| "processing string", || {
208 let _s = String::from_utf8(vec![0, 123, 255])?;
209 // do some processing of _s
210 Ok(())
211});
212
213assert_eq!(res.unwrap_err().to_string(), "while processing string got error caused by: invalid utf-8 sequence of 1 bytes from index 2");
214```
215
216## Nested context
217Context methods can be used multiple times to add layers of context.
218
219```rust
220use problem::prelude::*;
221
222fn foo() -> Result<String, Problem> {
223 let str = String::from_utf8(vec![0, 123, 255])?;
224 Ok(str)
225}
226
227let res = in_context_of("doing stuff", || {
228 let _s = foo().problem_while("running foo")?;
229 // do some processing of _s
230 Ok(())
231});
232
233assert_eq!(res.unwrap_err().to_string(), "while doing stuff, while running foo got error caused by: invalid utf-8 sequence of 1 bytes from index 2");
234```
235
236# Aborting program on `Problem`
237`panic!(message, problem)` macro can be used directly to abort program execution but error message printed on the screen will be formatted with `Debug` implementation.
238
239This library provides function `format_panic_to_stderr()` to set up hook that will use `eprintln!("{}", message)` to report panics.
240Alternatively if `log` feature is enabled (default), function `format_panic_to_error_log()` will set up hook that will log with `error!("{}", message)` to report panics.
241
242Panic hooks will produce backtrace of panic site if enabled via `RUST_BACKTRACE=1` environment variable along of the `Problem` object backtrace collected
243at object construction site.
244
245```noformat
246ERROR: Panicked in libcore/slice/mod.rs:2334:5: index 18492 out of range for slice of length 512
247```
248
249## Panicking on `Result` with `Problem`
250Similarly to `.expect(message)`, method `.or_failed_to(message)` can be used to abort the program via `panic!()` in case of `Err` variant with `Display` formatted
251message and error converted to `Problem` to format the error message with `Display` trait.
252
253```rust,should_panic
254use problem::prelude::*;
255use problem::format_panic_to_stderr;
256
257format_panic_to_stderr();
258
259// Prints message:
260let _s = String::from_utf8(vec![0, 123, 255]).or_failed_to("convert string"); // Failed to convert string due to: invalid utf-8 sequence of 1 bytes from index 2
261```
262
263## Panicking on `Option`
264Similarly to `.ok_or(error)`, method `.or_failed_to(message)` can be used to abort the program via `panic!()` with `Display` formatted message on `None` variant of `Option` type.
265
266```rust,should_panic
267use problem::prelude::*;
268use problem::format_panic_to_stderr;
269
270format_panic_to_stderr();
271let nothing: Option<&'static str> = None;
272
273let _s = nothing.or_failed_to("get something"); // Failed to get something
274```
275
276## Panicking on iterators of `Result`
277Method `.or_failed_to(message)` can be used to abort the program via `panic!()` with formatted message on iterators with `Result` item when first `Err`
278is encountered otherwise unwrapping the `Ok` value.
279The error type will be converted to `Problem` just before panicing.
280
281```rust,should_panic
282use problem::prelude::*;
283use problem::format_panic_to_stderr;
284
285format_panic_to_stderr();
286
287let results = vec![Ok(1u32), Ok(2u32), Err("oops")];
288
289let _ok: Vec<u32> = results.into_iter()
290 .or_failed_to("collect numbers")
291 .collect(); // Failed to collect numbers due to: oops
292```
293
294# Main function exit with error message and custom status
295`FatalProblem` and `result::FinalResult` types can be used on `main` function signature to allow programs to terminate with `Problem` formatted message and custom exit status.
296
297```rust,should_panic
298use problem::prelude::*;
299
300fn main() -> FinalResult {
301 // Prints "I'm sorry Dave, I'm afraid I can't do that" and exits with status 1
302 Err(Problem::from_error("I'm sorry Dave, I'm afraid I can't do that"))?;
303
304 // Prints "I'm sorry Dave, I'm afraid I can't do that" and exits with status 42
305 Err(Problem::from_error("I'm sorry Dave, I'm afraid I can't do that")).fatal_with_status(42)?;
306
307 Ok(())
308}
309```
310
311# Logging errors
312If `log` feature is enabled (default) function `.ok_or_log_warn()` or `.ok_or_log_error()` can be used on `Result` and iterator of `Result` items to convert
313`Result` into `Option` while logging `Err` wariants as warnings or errors.
314
315When used on iterators `.flatten()` addaptor can be used to filter out all `Err` variant items after they were logged and converted to `None`.
316
317```rust
318use problem::prelude::*;
319
320# #[cfg(feature = "log")]
321# fn test_with_log_feature() {
322let results = vec![Ok(1u32), Ok(2), Err("oops"), Ok(3), Err("oh"), Ok(4)];
323
324// Logs warning message: Continuing with error oops
325// Logs warning message: Continuing with error oh
326let ok: Vec<u32> = results.into_iter()
327 .ok_or_log_warn()
328 .flatten()
329 .collect();
330
331assert_eq!(ok.as_slice(), [1, 2, 3, 4]);
332# }
333#
334# #[cfg(feature = "log")]
335# test_with_log_feature();
336```
337
338# Backtraces
339When compiled with `backtrace` feature (default) formatting of backtraces for `Problem` cause and `panic!` locations can be enabled via
340`RUST_BACKTRACE=1` environment variable.
341
342```noformat
343Fatal error: thread 'tests::test_panic_format_stderr_problem' panicked at src/lib.rs:657:35 with: Failed to complete processing task due to: while processing object, while processing input data, while parsing input got error caused by: boom!
344--- Cause
345 0: backtrace::backtrace::trace_unsynchronized::h936094cb968a67c2
346 at /Users/jpastuszek/.cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.13/src/backtrace/mod.rs:57
347 1: problem::Problem::from_error::hfdbc5afef77017de
348 at /Users/jpastuszek/Documents/problem/src/lib.rs:435
349 2: <problem::Problem as core::convert::From<E>>::from::h3b5fdbec33645197
350 at /Users/jpastuszek/Documents/problem/src/lib.rs:500
351 3: <T as core::convert::Into<U>>::into::h37311b4bc5720d6d
352 at /rustc/9fda7c2237db910e41d6a712e9a2139b352e558b/src/libcore/convert.rs:455
353 4: <core::result::Result<O, E> as problem::ProblemWhile>::problem_while::{{closure}}::h97ad232ce9ba14fb
354 at /Users/jpastuszek/Documents/problem/src/lib.rs:617
355 5: <core::result::Result<T, E>>::map_err::he22546342a0a16ff
356 at /rustc/9fda7c2237db910e41d6a712e9a2139b352e558b/src/libcore/result.rs:530
357 6: <core::result::Result<O, E> as problem::ProblemWhile>::problem_while::he5e05f693d81f439
358 at /Users/jpastuszek/Documents/problem/src/lib.rs:617
359 7: problem::tests::test_panic_format_stderr_problem::he7f271034488edfe
360 at /Users/jpastuszek/Documents/problem/src/lib.rs:1053
361 8: problem::tests::test_panic_format_stderr_problem::{{closure}}::hd06e112465364a39
362 at /Users/jpastuszek/Documents/problem/src/lib.rs:1051
363 9: core::ops::function::FnOnce::call_once::hb512c264f284bc3f
364 at /rustc/9fda7c2237db910e41d6a712e9a2139b352e558b/src/libcore/ops/function.rs:238
365 10: <F as alloc::boxed::FnBox<A>>::call_box::h0b961cc85c049bee
366 at /rustc/9fda7c2237db910e41d6a712e9a2139b352e558b/src/liballoc/boxed.rs:673
367 11: ___rust_maybe_catch_panic
368 at /rustc/9fda7c2237db910e41d6a712e9a2139b352e558b/src/libpanic_unwind/lib.rs:102
369 12: std::sys_common::backtrace::__rust_begin_short_backtrace::h07f538384f587585
370 at /rustc/9fda7c2237db910e41d6a712e9a2139b352e558b/src/libtest/lib.rs:1426
371 13: std::panicking::try::do_call::h4953be8a0738d6ec
372 at /rustc/9fda7c2237db910e41d6a712e9a2139b352e558b/src/libstd/panicking.rs:310
373 14: ___rust_maybe_catch_panic
374 at /rustc/9fda7c2237db910e41d6a712e9a2139b352e558b/src/libpanic_unwind/lib.rs:102
375 15: <F as alloc::boxed::FnBox<A>>::call_box::h5a5d3b35dc8857f3
376 at /rustc/9fda7c2237db910e41d6a712e9a2139b352e558b/src/libstd/thread/mod.rs:476
377 16: std::sys::unix::thread::Thread::new::thread_start::hb66fd5e16b37cfd7
378 at /rustc/9fda7c2237db910e41d6a712e9a2139b352e558b/src/libstd/sys_common/thread.rs:24
379 17: __pthread_body
380 18: __pthread_start
381--- Panicked
382 0: backtrace::backtrace::trace_unsynchronized::h936094cb968a67c2
383 at /Users/jpastuszek/.cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.13/src/backtrace/mod.rs:57
384 1: problem::format_panic_to_stderr::{{closure}}::h24ae835f59658f26
385 at /Users/jpastuszek/Documents/problem/src/lib.rs:868
386 2: std::panicking::rust_panic_with_hook::h3fe6a67edb032589
387 at /rustc/9fda7c2237db910e41d6a712e9a2139b352e558b/src/libstd/panicking.rs:495
388 3: std::panicking::continue_panic_fmt::hf7169aba6b1afe9c
389 at /rustc/9fda7c2237db910e41d6a712e9a2139b352e558b/src/libstd/panicking.rs:398
390 4: std::panicking::begin_panic_fmt::hb5f6d46d54559b8a
391 at /rustc/9fda7c2237db910e41d6a712e9a2139b352e558b/src/libstd/panicking.rs:353
392 5: <core::result::Result<O, E> as problem::FailedTo<O>>::or_failed_to::{{closure}}::h8e0b5d62111b80f4
393 at /Users/jpastuszek/Documents/problem/src/lib.rs:657
394 6: <core::result::Result<T, E>>::unwrap_or_else::h8bcc063ecb00981e
395 at /rustc/9fda7c2237db910e41d6a712e9a2139b352e558b/src/libcore/result.rs:774
396 7: <core::result::Result<O, E> as problem::FailedTo<O>>::or_failed_to::h2e59dbec5efe6366
397 at /Users/jpastuszek/Documents/problem/src/lib.rs:657
398 8: problem::tests::test_panic_format_stderr_problem::he7f271034488edfe
399 at /Users/jpastuszek/Documents/problem/src/lib.rs:1058
400 9: problem::tests::test_panic_format_stderr_problem::{{closure}}::hd06e112465364a39
401 at /Users/jpastuszek/Documents/problem/src/lib.rs:1051
402 10: core::ops::function::FnOnce::call_once::hb512c264f284bc3f
403 at /rustc/9fda7c2237db910e41d6a712e9a2139b352e558b/src/libcore/ops/function.rs:238
404 11: <F as alloc::boxed::FnBox<A>>::call_box::h0b961cc85c049bee
405 at /rustc/9fda7c2237db910e41d6a712e9a2139b352e558b/src/liballoc/boxed.rs:673
406 12: ___rust_maybe_catch_panic
407 at /rustc/9fda7c2237db910e41d6a712e9a2139b352e558b/src/libpanic_unwind/lib.rs:102
408 13: std::sys_common::backtrace::__rust_begin_short_backtrace::h07f538384f587585
409 at /rustc/9fda7c2237db910e41d6a712e9a2139b352e558b/src/libtest/lib.rs:1426
410 14: std::panicking::try::do_call::h4953be8a0738d6ec
411 at /rustc/9fda7c2237db910e41d6a712e9a2139b352e558b/src/libstd/panicking.rs:310
412 15: ___rust_maybe_catch_panic
413 at /rustc/9fda7c2237db910e41d6a712e9a2139b352e558b/src/libpanic_unwind/lib.rs:102
414 16: <F as alloc::boxed::FnBox<A>>::call_box::h5a5d3b35dc8857f3
415 at /rustc/9fda7c2237db910e41d6a712e9a2139b352e558b/src/libstd/thread/mod.rs:476
416 17: std::sys::unix::thread::Thread::new::thread_start::hb66fd5e16b37cfd7
417 at /rustc/9fda7c2237db910e41d6a712e9a2139b352e558b/src/libstd/sys_common/thread.rs:24
418 18: __pthread_body
419 19: __pthread_start
420```
421
422## Access
423Formatted backtrace `&str` can be accessed via `Problem::backtrace` function that will return `Some` if `backtrace` feature is enabled and `RUST_BACKTRACE=1`
424environment variable is set.
425
426```rust
427use problem::prelude::*;
428
429Problem::from_error("foo").backtrace(); // Some(" 0: backtrace...")
430```
431 */
432#[cfg(feature = "log")]
433#[macro_use]
434extern crate log;
435use std::error::Error;
436use std::fmt::{self, Display, Write};
437use std::panic;
438
439const DEFAULT_FATAL_STATUS: i32 = 1;
440
441/// Includes `Problem` type and related conversion traits and `in_context_of*` functions
442pub mod prelude {
443 pub use super::{
444 in_context_of, in_context_of_with, problem, FailedTo, FailedToIter, Fatal, FatalProblem,
445 MapProblem, MapProblemOr, OkOrProblem, Problem, ProblemWhile,
446 };
447
448 pub use super::result::FinalResult;
449 // Note that `result::Result` is not part of prelude as it may conflict with standard library or
450 // custom library result types.
451
452 #[cfg(feature = "log")]
453 pub use super::logged::{OkOrLog, OkOrLogIter};
454}
455
456/// Wraps error, context and backtrace information and formats it for display.
457/// Data is heap allocated to avoid type parameters or lifetimes.
458#[derive(Debug)]
459pub struct Problem {
460 error: Box<dyn Error>,
461 context: Vec<String>,
462 backtrace: Option<String>,
463}
464
465impl Problem {
466 /// Create `Problem` from types implementing `Into<Box<dyn Error>>` (including `String` and `&str`) so that `Error::cause`
467 /// chain is followed through in the `Display` message
468 pub fn from_error(error: impl Into<Box<dyn Error>>) -> Problem {
469 Problem {
470 error: error.into(),
471 context: Vec::new(),
472 backtrace: format_backtrace(),
473 }
474 }
475
476 /// Same as `Problem::from_error` but stores only final message as `String` and does not take ownership of the error
477 pub fn from_error_message(error: &impl Error) -> Problem {
478 let mut message = String::new();
479 write_error_message(error, &mut message).unwrap();
480
481 Problem {
482 error: message.into(),
483 context: Vec::new(),
484 backtrace: format_backtrace(),
485 }
486 }
487
488 /// Get backtrace associated with this `Problem` instance if available
489 pub fn backtrace(&self) -> Option<&str> {
490 self.backtrace.as_ref().map(String::as_str)
491 }
492}
493
494#[allow(deprecated)]
495fn write_error_message(error: &dyn Error, w: &mut impl Write) -> fmt::Result {
496 write!(w, "{}", error)?;
497
498 let mut error_cause = error;
499 loop {
500 // Note: using Error::cause() here to be backward compatible with older errors
501 if let Some(cause) = error_cause.cause() {
502 write!(w, "; caused by: {}", cause)?;
503 error_cause = cause;
504 } else {
505 break;
506 }
507 }
508 Ok(())
509}
510
511impl Display for Problem {
512 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
513 if let Some(context) = self.context.last() {
514 write!(f, "while {}", context)?;
515 }
516 for context in self.context.iter().rev().skip(1) {
517 write!(f, ", while {}", context)?;
518 }
519 if !self.context.is_empty() {
520 write!(f, " got error caused by: ")?;
521 }
522
523 write_error_message(self.error.as_ref(), f)?;
524
525 if let Some(backtrace) = self.backtrace.as_ref() {
526 write!(f, "\n--- Cause\n{}", backtrace)?;
527 }
528
529 Ok(())
530 }
531}
532
533/// Every type implementing `Into<Box<dyn Error>>` trait (including `String` and `&str` types) can be converted to `Problem` via `?` operator
534impl<E> From<E> for Problem
535where
536 E: Into<Box<dyn Error>>,
537{
538 fn from(error: E) -> Problem {
539 Problem::from_error(error)
540 }
541}
542
543/// Construct `Err` variant of `Result` containing `Problem` with given message formatted with
544/// `format!` macro.
545#[macro_export]
546macro_rules! problem {
547 ($ ($ arg : tt) *) => { Err(Problem::from_error(format!($($arg)*))) };
548}
549
550/// This error type is meant to be used as `main()` result error. It implements `Debug` display so
551/// that the program can terminate with nice message formatted with `Problem` and custom exit
552/// status.
553pub struct FatalProblem {
554 status: i32,
555 problem: Problem,
556}
557
558impl From<Problem> for FatalProblem {
559 fn from(problem: Problem) -> FatalProblem {
560 FatalProblem {
561 status: DEFAULT_FATAL_STATUS,
562 problem,
563 }
564 }
565}
566
567impl<E> From<E> for FatalProblem
568where
569 E: Into<Box<dyn std::error::Error>>,
570{
571 fn from(error: E) -> FatalProblem {
572 FatalProblem {
573 status: DEFAULT_FATAL_STATUS,
574 problem: Problem::from_error(error),
575 }
576 }
577}
578
579impl fmt::Debug for FatalProblem {
580 fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
581 eprintln!("{}", self.problem);
582 std::process::exit(self.status)
583 }
584}
585
586/// Extension trait to map `Option` to `Result` with `Problem`
587pub trait Fatal<O> {
588 fn fatal(self) -> Result<O, FatalProblem>;
589 fn fatal_with_status(self, status: i32) -> Result<O, FatalProblem>;
590}
591
592/// Extension trait that allow to convert to `Result` with `FatalProblem`
593impl<O> Fatal<O> for Result<O, Problem> {
594 /// Converts to `Result` with `FatalProblem` and default exit status
595 fn fatal(self) -> Result<O, FatalProblem> {
596 self.fatal_with_status(DEFAULT_FATAL_STATUS)
597 }
598
599 /// Converts to `Result` with `FatalProblem` and given exit status
600 fn fatal_with_status(self, status: i32) -> Result<O, FatalProblem> {
601 self.map_err(|problem| FatalProblem { status, problem })
602 }
603}
604
605pub mod result {
606 //! Handy Result types using `Problem` as error.
607 use super::{FatalProblem, Problem};
608
609 /// `Result` with `Problem`
610 pub type Result<T> = std::result::Result<T, Problem>;
611
612 /// `Result` with `FatalProblem` to be used as `main()` return value
613 pub type FinalResult = std::result::Result<(), FatalProblem>;
614}
615
616/// Map type containing error to type containing `Problem`
617pub trait MapProblem {
618 type ProblemCarrier;
619 fn map_problem(self) -> Self::ProblemCarrier;
620}
621
622/// Mapping `Result` with error to `Result` with `Problem`
623impl<O, E> MapProblem for Result<O, E>
624where
625 E: Into<Problem>,
626{
627 type ProblemCarrier = Result<O, Problem>;
628 fn map_problem(self) -> Result<O, Problem> {
629 self.map_err(|e| e.into())
630 }
631}
632
633/// Map type not containing any error to type containing given `Problem`
634pub trait MapProblemOr {
635 type ProblemCarrier;
636 fn map_problem_or(self, problem: impl Into<Problem>) -> Self::ProblemCarrier;
637 fn map_problem_or_else<F, P>(self, problem: F) -> Self::ProblemCarrier
638 where
639 F: FnOnce() -> P,
640 P: Into<Problem>;
641}
642
643/// Mapping `Result` with `Option<E>` to `Result` with `Problem` where `E` implements `Into<Problem>`
644impl<O, E> MapProblemOr for Result<O, Option<E>>
645where
646 E: Into<Problem>,
647{
648 type ProblemCarrier = Result<O, Problem>;
649
650 fn map_problem_or(self, problem: impl Into<Problem>) -> Result<O, Problem> {
651 self.map_err(|e| e.map(Into::into).unwrap_or_else(|| problem.into()))
652 }
653
654 fn map_problem_or_else<F, P>(self, problem: F) -> Self::ProblemCarrier
655 where
656 F: FnOnce() -> P,
657 P: Into<Problem>,
658 {
659 self.map_err(|e| e.map(Into::into).unwrap_or_else(|| problem().into()))
660 }
661}
662
663/// Extension trait to map `Option` to `Result` with `Problem`
664pub trait OkOrProblem<O> {
665 fn ok_or_problem<P>(self, problem: P) -> Result<O, Problem>
666 where
667 P: Into<Problem>;
668 fn ok_or_problem_with<F, P>(self, problem: F) -> Result<O, Problem>
669 where
670 F: FnOnce() -> P,
671 P: Into<Problem>;
672}
673
674impl<O> OkOrProblem<O> for Option<O> {
675 fn ok_or_problem<P>(self, problem: P) -> Result<O, Problem>
676 where
677 P: Into<Problem>,
678 {
679 self.ok_or_else(|| problem.into())
680 }
681
682 fn ok_or_problem_with<F, P>(self, problem: F) -> Result<O, Problem>
683 where
684 F: FnOnce() -> P,
685 P: Into<Problem>,
686 {
687 self.ok_or_else(|| problem().into())
688 }
689}
690
691/// Convert to `Problem` if needed and add context to it
692pub trait ProblemWhile {
693 type WithContext;
694
695 /// Add context information
696 fn problem_while(self, message: impl ToString) -> Self::WithContext;
697
698 /// Add context information from function call
699 fn problem_while_with<F, M>(self, message: F) -> Self::WithContext
700 where
701 F: FnOnce() -> M,
702 M: ToString;
703}
704
705impl ProblemWhile for Problem {
706 type WithContext = Problem;
707
708 fn problem_while(mut self, message: impl ToString) -> Problem {
709 self.context.push(message.to_string());
710 self
711 }
712
713 fn problem_while_with<F, M>(self, message: F) -> Problem
714 where
715 F: FnOnce() -> M,
716 M: ToString,
717 {
718 self.problem_while(message())
719 }
720}
721
722impl<O, E> ProblemWhile for Result<O, E>
723where
724 E: Into<Problem>,
725{
726 type WithContext = Result<O, Problem>;
727
728 fn problem_while(self, message: impl ToString) -> Result<O, Problem> {
729 self.map_err(|err| err.into().problem_while(message))
730 }
731
732 fn problem_while_with<F, M>(self, message: F) -> Result<O, Problem>
733 where
734 F: FnOnce() -> M,
735 M: ToString,
736 {
737 self.map_err(|err| err.into().problem_while_with(message))
738 }
739}
740
741/// Executes closure with `problem_while` context
742pub fn in_context_of<O, B>(message: &str, body: B) -> Result<O, Problem>
743where
744 B: FnOnce() -> Result<O, Problem>,
745{
746 body().problem_while(message)
747}
748
749/// Executes closure with `problem_while_with` context
750pub fn in_context_of_with<O, F, M, B>(message: F, body: B) -> Result<O, Problem>
751where
752 F: FnOnce() -> M,
753 M: ToString,
754 B: FnOnce() -> Result<O, Problem>,
755{
756 body().problem_while_with(message)
757}
758
759/// Extension of `Result` that allows program to panic with `Display` message on `Err` for fatal application errors that are not bugs
760pub trait FailedTo<O> {
761 fn or_failed_to(self, message: impl Display) -> O;
762}
763
764impl<O, E> FailedTo<O> for Result<O, E>
765where
766 E: Into<Problem>,
767{
768 fn or_failed_to(self, message: impl Display) -> O {
769 self.unwrap_or_else(|err| panic!("Failed to {} due to: {}", message, err.into()))
770 }
771}
772
773impl<O> FailedTo<O> for Option<O> {
774 fn or_failed_to(self, message: impl Display) -> O {
775 self.unwrap_or_else(|| panic!("Failed to {}", message))
776 }
777}
778
779/// Iterator that will panic on first error with message displaying `Display` formatted message
780pub struct ProblemIter<I, M> {
781 inner: I,
782 message: M,
783}
784
785impl<I, O, E, M> Iterator for ProblemIter<I, M>
786where
787 I: Iterator<Item = Result<O, E>>,
788 E: Into<Problem>,
789 M: Display,
790{
791 type Item = O;
792
793 fn next(&mut self) -> Option<Self::Item> {
794 self.inner.next().map(|res| res.or_failed_to(&self.message))
795 }
796}
797
798/// Convert `Iterator` of `Result<O, E>` to iterator of `O` and panic on first `E` with problem message
799pub trait FailedToIter<O, E, M>: Sized {
800 fn or_failed_to(self, message: M) -> ProblemIter<Self, M>;
801}
802
803impl<I, O, E, M> FailedToIter<O, E, M> for I
804where
805 I: Iterator<Item = Result<O, E>>,
806 E: Into<Problem>,
807 M: Display,
808{
809 fn or_failed_to(self, message: M) -> ProblemIter<Self, M> {
810 ProblemIter {
811 inner: self,
812 message: message,
813 }
814 }
815}
816
817#[cfg(feature = "log")]
818pub mod logged {
819 use super::*;
820 use log::{error, warn};
821
822 /// Extension of `Result` that allows program to log on `Err` with `Display` message for application errors that are not critical
823 pub trait OkOrLog<O> {
824 fn ok_or_log_warn(self) -> Option<O>;
825 fn ok_or_log_error(self) -> Option<O>;
826 }
827
828 impl<O, E> OkOrLog<O> for Result<O, E>
829 where
830 E: Into<Problem>,
831 {
832 fn ok_or_log_warn(self) -> Option<O> {
833 self.map_err(|err| warn!("Continuing with error: {}", err.into()))
834 .ok()
835 }
836
837 fn ok_or_log_error(self) -> Option<O> {
838 self.map_err(|err| error!("Continuing with error: {}", err.into()))
839 .ok()
840 }
841 }
842
843 /// Iterator that will log as warn `Display` formatted message on `Err` and skip to next item; it can be flattened to skip failed items
844 pub struct ProblemWarnLoggingIter<I> {
845 inner: I,
846 }
847
848 impl<I, O, E> Iterator for ProblemWarnLoggingIter<I>
849 where
850 I: Iterator<Item = Result<O, E>>,
851 E: Into<Problem>,
852 {
853 type Item = Option<O>;
854
855 fn next(&mut self) -> Option<Self::Item> {
856 self.inner.next().map(|res| res.ok_or_log_warn())
857 }
858 }
859
860 /// Iterator that will log as error `Display` formatted message on `Err` and skip to next item; it can be flattened to skip failed items
861 pub struct ProblemErrorLoggingIter<I> {
862 inner: I,
863 }
864
865 impl<I, O, E> Iterator for ProblemErrorLoggingIter<I>
866 where
867 I: Iterator<Item = Result<O, E>>,
868 E: Into<Problem>,
869 {
870 type Item = Option<O>;
871
872 fn next(&mut self) -> Option<Self::Item> {
873 self.inner.next().map(|res| res.ok_or_log_error())
874 }
875 }
876
877 /// Convert `Iterator` of `Result<O, E>` to iterator of `Option<O>` and log any `Err` variants
878 pub trait OkOrLogIter<O, E>: Sized {
879 fn ok_or_log_warn(self) -> ProblemWarnLoggingIter<Self>;
880 fn ok_or_log_error(self) -> ProblemErrorLoggingIter<Self>;
881 }
882
883 impl<I, O, E> OkOrLogIter<O, E> for I
884 where
885 I: Iterator<Item = Result<O, E>>,
886 E: Into<Problem>,
887 {
888 fn ok_or_log_warn(self) -> ProblemWarnLoggingIter<Self> {
889 ProblemWarnLoggingIter { inner: self }
890 }
891
892 fn ok_or_log_error(self) -> ProblemErrorLoggingIter<Self> {
893 ProblemErrorLoggingIter { inner: self }
894 }
895 }
896}
897
898#[cfg(not(feature = "backtrace"))]
899fn format_backtrace() -> Option<String> {
900 None
901}
902
903/* Use default Rust format like:
904 0: std::sys_common::backtrace::_print
905 at C:\projects\rust\src\libstd\sys_common\backtrace.rs:94
906 1: std::sys_common::backtrace::_print
907 at C:\projects\rust\src\libstd\sys_common\backtrace.rs:71
908*/
909#[cfg(feature = "backtrace")]
910#[inline(always)]
911fn format_backtrace() -> Option<String> {
912 if let Ok("1") = std::env::var("RUST_BACKTRACE").as_ref().map(String::as_str) {
913 let mut backtrace = String::new();
914 let mut frame_no: u32 = 0;
915
916 backtrace::trace(|frame| {
917 let ip = frame.ip();
918
919 if frame_no > 0 {
920 backtrace.push_str("\n");
921 }
922
923 backtrace::resolve(ip, |symbol| {
924 if let Some(name) = symbol.name() {
925 write!(backtrace, "{:4}: {}", frame_no, name).unwrap();
926 }
927 if let (Some(filename), Some(lineno)) = (symbol.filename(), symbol.lineno()) {
928 write!(
929 backtrace,
930 "\n at {}:{}",
931 filename.display(),
932 lineno
933 )
934 .unwrap();
935 }
936 });
937
938 frame_no += 1;
939 true // keep going to the next frame
940 });
941
942 Some(backtrace)
943 } else {
944 None
945 }
946}
947
948fn format_panic(panic: &std::panic::PanicInfo, backtrace: Option<String>) -> String {
949 let mut message = String::new();
950
951 let thread = std::thread::current();
952 let name = thread.name().unwrap_or("<unnamed>");
953
954 // taken from libstd
955 let msg = match panic.payload().downcast_ref::<&'static str>() {
956 Some(s) => *s,
957 None => match panic.payload().downcast_ref::<String>() {
958 Some(s) => &s[..],
959 None => "Box<Any>",
960 },
961 };
962
963 match (backtrace.is_some(), panic.location()) {
964 (true, Some(location)) => write!(
965 message,
966 "thread '{}' panicked at {} with: {}",
967 name, location, msg
968 )
969 .ok(),
970 (true, None) => write!(message, "thread '{}' panicked with: {}", name, msg).ok(),
971 (false, _) => write!(message, "{}", msg).ok(),
972 };
973
974 if let Some(backtrace) = backtrace {
975 message.push_str("\n--- Panicked\n");
976 message.push_str(&backtrace);
977 }
978
979 message
980}
981
982/// Set panic hook so that formats error message to `stderr` with more `Problem` friendly way
983pub fn format_panic_to_stderr() {
984 panic::set_hook(Box::new(|panic_info| {
985 let backtrace = format_backtrace();
986 eprintln!("Fatal error: {}", format_panic(panic_info, backtrace));
987 }));
988}
989
990/// Set panic hook so that when program panics it will log error massage with `error!` macro
991#[cfg(feature = "log")]
992pub fn format_panic_to_error_log() {
993 panic::set_hook(Box::new(|panic_info| {
994 let backtrace = format_backtrace();
995 error!("{}", format_panic(panic_info, backtrace));
996 }));
997}
998
999#[cfg(test)]
1000mod tests {
1001 use super::prelude::*;
1002 use super::{format_panic_to_stderr, in_context_of};
1003 use std::error::Error;
1004 use std::fmt::{self, Display};
1005 use std::io;
1006
1007 #[derive(Debug)]
1008 struct Foo;
1009
1010 impl Display for Foo {
1011 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1012 write!(f, "Foo error")
1013 }
1014 }
1015
1016 impl Error for Foo {}
1017
1018 #[derive(Debug)]
1019 struct Bar(Foo);
1020
1021 impl Display for Bar {
1022 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1023 write!(f, "Bar error")
1024 }
1025 }
1026
1027 impl Error for Bar {
1028 fn source(&self) -> Option<&(dyn Error + 'static)> {
1029 Some(&self.0)
1030 }
1031 }
1032
1033 #[derive(Debug)]
1034 struct Baz(Bar);
1035
1036 impl Display for Baz {
1037 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1038 write!(f, "Baz error")
1039 }
1040 }
1041
1042 impl Error for Baz {
1043 fn source(&self) -> Option<&(dyn Error + 'static)> {
1044 Some(&self.0)
1045 }
1046 }
1047
1048 #[test]
1049 #[should_panic(expected = "Failed to test due to: foo: 1")]
1050 fn test_problem_macro() {
1051 let err: Result<(), Problem> = problem!("foo: {}", 1);
1052 err.or_failed_to("test");
1053 }
1054
1055 #[test]
1056 fn test_convertion() {
1057 let _: Problem = io::Error::new(io::ErrorKind::InvalidInput, "boom!").into();
1058 let _: Problem = "boom!".into(); // via impl<'a> From<&'a str> for Box<dyn Error>
1059 }
1060
1061 #[test]
1062 #[should_panic(
1063 expected = "Failed to complete processing task due to: while processing object, while processing input data, while parsing input got error caused by: boom!"
1064 )]
1065 fn test_integration() {
1066 Err(io::Error::new(io::ErrorKind::InvalidInput, "boom!"))
1067 .problem_while("parsing input")
1068 .problem_while("processing input data")
1069 .problem_while("processing object")
1070 .or_failed_to("complete processing task")
1071 }
1072
1073 #[test]
1074 #[should_panic(
1075 expected = "Failed to complete processing task due to: while processing object, while processing input data, while parsing input got error caused by: boom!"
1076 )]
1077 fn test_integration_message() {
1078 Err("boom!")
1079 .problem_while("parsing input")
1080 .problem_while("processing input data")
1081 .problem_while("processing object")
1082 .or_failed_to("complete processing task")
1083 }
1084
1085 #[test]
1086 #[should_panic(
1087 expected = "Failed to complete processing task due to: while processing object, while processing input data, while parsing input got error caused by: Baz error; caused by: Bar error; caused by: Foo error"
1088 )]
1089 fn test_integration_cause_chain() {
1090 Err(Baz(Bar(Foo)))
1091 .problem_while("parsing input")
1092 .problem_while("processing input data")
1093 .problem_while("processing object")
1094 .or_failed_to("complete processing task")
1095 }
1096
1097 #[test]
1098 #[should_panic(
1099 expected = "Failed to complete processing task due to: while doing stuff got error caused by: boom!"
1100 )]
1101 fn test_in_context_of() {
1102 in_context_of("doing stuff", || {
1103 Err(io::Error::new(io::ErrorKind::InvalidInput, "boom!"))?
1104 })
1105 .or_failed_to("complete processing task")
1106 }
1107
1108 #[test]
1109 #[should_panic(expected = "Failed to foo due to: boom!")]
1110 fn test_result() {
1111 Err(io::Error::new(io::ErrorKind::InvalidInput, "boom!")).or_failed_to("foo")
1112 }
1113
1114 #[test]
1115 #[should_panic(
1116 expected = "Failed to quix due to: Baz error; caused by: Bar error; caused by: Foo error"
1117 )]
1118 fn test_result_cause_chain() {
1119 Err(Baz(Bar(Foo))).or_failed_to("quix")
1120 }
1121
1122 #[test]
1123 #[should_panic(
1124 expected = "Failed to quix due to: Baz error; caused by: Bar error; caused by: Foo error"
1125 )]
1126 fn test_result_cause_chain_message() {
1127 let error = Baz(Bar(Foo));
1128 Err(Problem::from_error_message(&error)).or_failed_to("quix")
1129 }
1130
1131 #[test]
1132 #[should_panic(expected = "Failed to foo")]
1133 fn test_option() {
1134 None.or_failed_to("foo")
1135 }
1136
1137 #[test]
1138 #[should_panic(expected = "Failed to foo due to: boom!")]
1139 fn test_option_errors() {
1140 Err(Some(io::Error::new(io::ErrorKind::InvalidInput, "boom!")))
1141 .map_problem_or("<unknown error>")
1142 .or_failed_to("foo")
1143 }
1144
1145 #[test]
1146 #[should_panic(expected = "Failed to foo due to: <unknown error>")]
1147 fn test_result_option_errors_unknown() {
1148 let err: Result<(), Option<io::Error>> = Err(None);
1149 err.map_problem_or("<unknown error>").or_failed_to("foo")
1150 }
1151
1152 #[test]
1153 #[should_panic(expected = "Failed to foo due to: nothing here")]
1154 fn test_result_ok_or_problem() {
1155 None.ok_or_problem("nothing here").or_failed_to("foo")
1156 }
1157
1158 #[test]
1159 #[should_panic(expected = "Failed to foo due to: omg!")]
1160 fn test_result_iter_or_failed_to() {
1161 let results = vec![Ok(1u32), Ok(2u32), Err("omg!")];
1162 let _ok = results.into_iter().or_failed_to("foo").collect::<Vec<_>>();
1163 }
1164
1165 #[test]
1166 #[should_panic]
1167 fn test_panic_format_stderr() {
1168 format_panic_to_stderr();
1169 panic!("foo bar!");
1170 }
1171
1172 #[test]
1173 #[should_panic]
1174 fn test_panic_format_stderr_problem() {
1175 format_panic_to_stderr();
1176 let result: Result<(), Problem> = Err(io::Error::new(io::ErrorKind::InvalidInput, "boom!"))
1177 .problem_while("parsing input")
1178 .problem_while("processing input data")
1179 .problem_while("processing object");
1180
1181 result.or_failed_to("complete processing task");
1182 }
1183
1184 #[test]
1185 #[should_panic]
1186 fn test_panic_format_stderr_unwrap() {
1187 format_panic_to_stderr();
1188 let result: Result<(), io::Error> =
1189 Err(io::Error::new(io::ErrorKind::InvalidInput, "boom!"));
1190 result.unwrap();
1191 }
1192
1193 #[test]
1194 #[should_panic]
1195 fn test_panic_format_stderr_expect() {
1196 format_panic_to_stderr();
1197 let result: Result<(), io::Error> =
1198 Err(io::Error::new(io::ErrorKind::InvalidInput, "boom!"));
1199 result.expect("foo");
1200 }
1201
1202 #[test]
1203 #[cfg(feature = "backtrace")]
1204 fn test_problem_backtrace() {
1205 let p = Problem::from_error("foo")
1206 .problem_while("bar")
1207 .problem_while("baz");
1208
1209 if let Ok("1") = std::env::var("RUST_BACKTRACE").as_ref().map(String::as_str) {
1210 assert!(p.backtrace().is_some());
1211 println!("{}", p.backtrace().unwrap());
1212 } else {
1213 assert!(p.backtrace().is_none());
1214 }
1215 }
1216
1217 #[test]
1218 #[cfg(feature = "log")]
1219 fn test_problem_log_error() {
1220 loggerv::init_quiet().ok();
1221 let error: Result<(), _> = Err(Baz(Bar(Foo)));
1222 error.ok_or_log_error();
1223 }
1224
1225 #[test]
1226 #[cfg(feature = "log")]
1227 fn test_problem_log_warn() {
1228 loggerv::init_quiet().ok();
1229 let error: Result<(), _> = Err(Baz(Bar(Foo)));
1230 error.ok_or_log_warn();
1231 }
1232
1233 #[test]
1234 #[cfg(feature = "log")]
1235 fn test_problem_log_iter_error() {
1236 loggerv::init_quiet().ok();
1237 assert_eq!(
1238 vec![Ok(1), Err(Foo), Err(Foo), Ok(2), Err(Foo), Ok(3)]
1239 .into_iter()
1240 .ok_or_log_error()
1241 .flatten()
1242 .collect::<Vec<_>>(),
1243 vec![1, 2, 3]
1244 );
1245 }
1246
1247 #[test]
1248 #[cfg(feature = "log")]
1249 fn test_problem_log_iter_warn() {
1250 loggerv::init_quiet().ok();
1251 assert_eq!(
1252 vec![Ok(1), Err(Foo), Err(Foo), Ok(2), Err(Foo), Ok(3)]
1253 .into_iter()
1254 .ok_or_log_warn()
1255 .flatten()
1256 .collect::<Vec<_>>(),
1257 vec![1, 2, 3]
1258 );
1259 }
1260}