Charles
Charles

Reputation: 1153

How do I transform io::Result into anyhow::Result?

I'm starting with this working code:

use std::env;
use std::fs::File;
use std::io::{BufRead, BufReader, Result};
use std::path::Path;

fn read_lines<P, I: IntoIterator<Item = P>>(files: I) -> impl Iterator<Item = Result<String>>
where
    P: AsRef<Path>,
{
    let handles = files.into_iter().map(|path| File::open(path).unwrap());
    handles.flat_map(|handle| BufReader::new(handle).lines())
}

fn main() -> Result<()> {
    let lines = read_lines(env::args().skip(1).collect::<Vec<String>>());
    for line in lines {
        println!("{:?}", line?)
    }

    Ok(())
}

I need to integrate this into a codebase that is heavily reliant on the anyhow library, but I have no idea how I can massage the BufReader::lines return value inside the flatmap into a impl Iterator<Item = anyhow::Result<String>>.

As a reproducible example of where I'm stuck, I integrate anyhow into my test bed with this Cargo.toml,

[package]
name = "rust-playground"
version = "0.1.0"
authors = ["Charles"]
edition = "2018"

[dependencies]
anyhow = "1"

And I replace the import of std::io::Result with anyhow::Result. I'm unsure where to place with_context calls, everything I've tried has led to compiler errors.

This attempt fails because I can't use ? inside the closure, but how else can I "unwrap"? I'm not allowed to use unwrap in this context, I'm expected to return an anyhow::Result, somehow...

use anyhow::{Context, Result};
use std::env;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;

fn read_lines<P, I: IntoIterator<Item = P>>(files: I) -> impl Iterator<Item = Result<String>>
where
    P: AsRef<Path>,
{
    let handles = files.into_iter().map(|path| {
        File::open(path).with_context(|| format!("opening path: {}", path.as_ref().display()))?
    });
    handles.flat_map(|handle| BufReader::new(handle).lines())
}

fn main() -> Result<()> {
    let lines = read_lines(env::args().skip(1).collect::<Vec<String>>());
    for line in lines {
        println!("{:?}", line?)
    }

    Ok(())
}

And the error message:

error[E0277]: the `?` operator can only be used in a closure that returns `Result` or `Option` (or another type that implements `Try`)
  --> src/main.rs:13:10
   |
12 |       files.into_iter().map(|path|
   |  ___________________________-
13 | |                   File::open(path).with_context(|| format!("opening path: {}", path.as_ref().display()))?);
   | |                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^-
   | |___________________|_____________________________________________________________________________________|
   |                     |                                                                                     this function should return `Result` or `Option` to accept `?`                                                     
   |                     cannot use the `?` operator in a closure that returns `File`
   |
   = help: the trait `Try` is not implemented for `File`
   = note: required by `from_error`

error[E0271]: type mismatch resolving `<FlatMap<Map<<I as IntoIterator>::IntoIter, [closure@src/main.rs:12:24: 13:97]>, std::io::Lines<BufReader<File>>, [closure@src/main.rs:14:22: 14:61]> as Iterator>::Item == std::result::Result<String, anyhow::Error>`
 --> src/main.rs:7:58
  |
7 | fn read_lines<P, I: IntoIterator<Item = P>>(files: I) -> impl Iterator<Item = Result<String>>
  |                                                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `std::io::Error`, found struct `anyhow::Error`                                                     
  |
  = note: expected enum `std::result::Result<_, std::io::Error>`
             found enum `std::result::Result<_, anyhow::Error>`

I can figure out a way to make this compile if my method handles on a single filename:

fn read_lines<P>(filename: P) -> Result<io::Lines<io::BufReader<File>>>
where
    P: AsRef<Path>,
{
    let file = File::open(&filename)
        .with_context(|| format!("opening filename: {}", filename.as_ref().display()))?;
    Ok(BufReader::new(file).lines())
}

But this doesn't generalize properly to the the case of handling multiple filenames, since File::open(&path).with_context(...)? is not correct inside the iterator.

Upvotes: 5

Views: 3703

Answers (1)

Shadab
Shadab

Reputation: 1347

The last snippet you provided for handling single files doesn't seem to be returning the same thing as the previous examples.

If you want to just extend the single file example for multiple files, then you could just map the single-file function over the list of files.

Something like this

fn read_lines<P, I: IntoIterator<Item = P>>(files: I) -> impl Iterator<Item = Result<std::io::Lines<BufReader<File>>>>
where
    P: AsRef<Path>,
{
    
    files.into_iter().map(|filename: P| {
        let file = File::open(&filename)
            .with_context(|| format!("opening filename: {}", filename.as_ref().display()))?;

        Ok(BufReader::new(file).lines())
    })
}

But if you would like the function to return a Result<String> instead of a Result<std::io::Lines<BufReader<File>>>, you can transform the errors from io::Error to anyhow::Error like so,

fn read_lines<P, I: IntoIterator<Item = P>>(files: I) -> impl Iterator<Item = Result<String>>
where
    P: AsRef<Path>,
{
    files.into_iter().flat_map(|filename: P| {
        let file = File::open(&filename)
            .with_context(|| format!("opening filename: {}", filename.as_ref().display()));

        file.into_iter().flat_map(|file| {
            BufReader::new(file)
                .lines()
                .map(|line| line.map_err(anyhow::Error::from))
        })
    })
}

Upvotes: 4

Related Questions