Documentation
use {
    problemo::{common::*, *},
    std::path::*,
};

// If you are often constructing problems in your code in the same way,
// it can be convenient to define an extension trait for Result

fn read_file(path: &str) -> Result<String, Problem> {
    let path = PathBuf::from(path);
    std::fs::read_to_string(&path).into_file_problem("read_to_string", path)
}

fn main() {
    if let Err(problem) = read_file("non-existing.txt") {
        println!("{}", problem);
        if let Some(path) = problem.attachment_of_type::<PathBuf>() {
            println!("  path: {}", path.display());
        }
    }
}

// Extension trait for Result

pub trait FileResult<OkT> {
    /// Convert [Err] into a [Problem] via [IoError] with a [PathBuf] attachment.
    fn into_file_problem(self, message: &'static str, path: PathBuf) -> Result<OkT, Problem>;
}

// It's convenient to use IntoProblemResult here to handle the initial conversion for us

// See examples/debug_context.rs to understand why we are adding #[track_caller]

impl<ResultT, OkT> FileResult<OkT> for ResultT
where
    ResultT: IntoProblemResult<OkT>,
{
    #[track_caller]
    fn into_file_problem(self, message: &'static str, path: PathBuf) -> Result<OkT, Problem> {
        // Note that we are *not* using map_err() here because a closure cannot be annotated with #[track_caller]
        match self.into_problem() {
            Ok(ok) => Ok(ok),
            Err(problem) => Err(problem.via(IoError::new(message)).with(path)),
        }
    }
}