user1244932
user1244932

Reputation: 8092

How can I automate adding context to an Err?

I'm parsing a text file line by line so I have a line number as context:

#[derive(Debug, Clone)]
pub struct Position {
    pub line: usize,
    pub column: usize,
}

#[derive(Debug)]
pub enum ParseError {
    IoError(io::Error),
    InvalidRecord(Position),
    EncodingError(Position),
}

I have a loop like this:

let mut pos = Position { line: 0, column: 0 };
const LF: u8 = 0xa;
let mut record_buf = Vec::new();
while let Ok(nbytes) = reader.read_until(LF, &mut record_buf) {
    // if record_buf contains some special bytes, then
    // we have several numbers in ASCII
    let x = str::from_utf8(&record_buf[42..(42 + 2)])?.parse::<u32>()?;
    let y = str::from_utf8(&record_buf[46..(46 + 4)])?.parse::<u32>()?;

    //at the end
    record_buf.clear();
    pos.line += 1;
}

I want to automate mapping Utf8Error to ParseError::EncodingError and ParseIntError to ParseError::EncodingError.

I can not just implement impl From<Utf8Error> for ParseError, because the context in the form of line number is not available in the trait implementation.

How can I simplify my coding and not write verbose error handling like this for every number that I want extract from Vec<u8>?

str::from_utf8(&record_buf[42..(42 + 2)])
    .map_err(|_| ParseError::EncodingError(pos.clone()))?
    .parse::<u32>()
    .map_err(|_| ParseError::InvalidRecord(pos.clone()))? 

Upvotes: 1

Views: 571

Answers (1)

Shepmaster
Shepmaster

Reputation: 430861

TL;DR: Use a crate like quick_error, error-chain, or failure.


I can not just implement impl From<Utf8Error> for ParseError, because the context in the form of line number is not available in the trait implementation.

That's true, but that doesn't mean you can't produce a type that carries the context.

You can simplify your call site down to something like this:

let val = str::from_utf8(&record_buf[4..][..2])
    .context(pos)?
    .parse()
    .context(pos)?;

To do so, we create a new type to hold our combination context and original error, then implement an extension trait for Result to add the context to an error:

struct Context<V, E>(V, E);

trait ContextExt<T, E> {
    fn context<V>(self, v: V) -> Result<T, Context<V, E>>;
}

impl<T, E> ContextExt<T, E> for Result<T, E> {
    fn context<V>(self, v: V) -> Result<T, Context<V, E>> {
        self.map_err(|e| Context(v, e))
    }
}

We then implement From<Context<...>> for Error for each interesting thing:

impl From<Context<Position, str::Utf8Error>> for ParseError {
    fn from(other: Context<Position, str::Utf8Error>) -> ParseError {
        ParseError::EncodingError(other.0, other.1)
    }
}

impl From<Context<Position, num::ParseIntError>> for ParseError {
    fn from(other: Context<Position, num::ParseIntError>) -> ParseError {
        ParseError::InvalidRecord(other.0, other.1)
    }
}

The last ergonomic change is to implement Copy for your Postion type, which makes it much easier to use — no more calls to .clone().

Playground


The aforementioned crates make this way easier.

Here's all the code with quick-error (my favorite):

#[macro_use]
extern crate quick_error;

use quick_error::ResultExt;
use std::{num, str};

#[derive(Debug, Copy, Clone)]
pub struct Position {
    pub line: usize,
    pub column: usize,
}

quick_error! {
    #[derive(Debug)]
    pub enum ParseError {
        EncodingError(pos: Position, err: str::Utf8Error) {
            context(pos: Position, err: str::Utf8Error) -> (pos, err)
        }
        InvalidRecord(pos: Position, err: num::ParseIntError) {
            context(pos: Position, err: num::ParseIntError) -> (pos, err)
        }
    }
}

fn inner_main() -> Result<u32, ParseError> {
    let record_buf = b"kode12abc";

    let pos = Position { line: 1, column: 2 };

    let val = str::from_utf8(&record_buf[4..][..2])
        .context(pos)?
        .parse()
        .context(pos)?;

    Ok(val)
}

fn main() {
    let v = inner_main().expect("boom");
    println!("{}", v)
}

Upvotes: 2

Related Questions