ruststructerror-handling

Rust, do something if a large number of resutls are all ok or return a vector of all errors


I'm trying to parse some user input, the input is stored in a struct where there are a lot of options/results (to deal with the data being uninitialized before the user enters it) and when the user submits it it needs to go into a struct where all the values are known.

I'd like to be able to a vector with any errors so I can give feedback to the user on all the errors with the input (not just the first). However, I can't find a good way to do that.

struct Test {
    x: i32,
    y: String,
}

#[derive(Debug)]
enum Errors {
    OneIsNone,
    TwoIsNone,
}

The my current approach is:

fn main() -> Result<Test, Vec<Errors>> {
    let option1 = Some(5);
    let option2 = None;

    let mut errors = Vec::new();

    let x: i32;
    match option1 {
        Some(value) => x = value,
        None => errors.push(Errors::OneIsNone),
    };

    let y: String;
    match option2 {
        Some(value) => y = value,
        None => errors.push(Errors::TwoIsNone),
    }

    if errors.len() != 0 {
        return Err(errors)
    }

    Ok(
        Test {
            x,
            y
        }
    )
}

Which doesn't work because the compiler can't tell that x and y must be initialised if the code reaches the Ok block.

In this case I could just unwrap match the two results but that approach will become hideously clunky for structs with a large number of fields.

Basically I want a way to take in a large number of results and parse them into a struct if they all give Ok() or return a vector containing all the errors otherwise i.e. how to I implement unwrap_or_errors in

fn main() -> Result<Test, Vec<Errors>> {
    let option1 = Some(5);
    let option2 = None;

    let mut errors = Vec::new();
    

    let x = match option1 {
        Some(val) => Ok(val),
        None => Err(Errors::OneIsNone),
    };

    let y =  match option2 {
        Some(val) => Ok(val),
        None => Err(Errors::TwoIsNone),
    };

    
    if errors.len() != 0 {
        return Err(errors)
    }

    Test::unwrap_or_errors(x, y)?
}

Solution

  • You can use tuple pattern matching for the success case, and iterator flattening for the failure case:

    struct Test {
        x: i32,
        y: String,
    }
    
    impl Test {
        fn unwrap_or_errors<E>(
            x: Result<i32, E>,
            y: Result<String, E>,
        ) -> Result<Self, Vec<E>> {
            match (x, y) {
                (Ok(x), Ok(y)) => Ok(Self { x, y }),
                (x, y) => Err([x.err(), y.err()]
                    .into_iter()
                    .flatten()
                    .collect()),
            }
        }
    }
    
    #[derive(Debug)]
    enum Errors {
        OneIsNone,
        TwoIsNone,
    }
    
    fn new() -> Result<Test, Vec<Errors>> {
        let option1 = Some(5);
        let option2 = None;
    
        let x = match option1 {
            Some(val) => Ok(val),
            None => Err(Errors::OneIsNone),
        };
    
        let y = match option2 {
            Some(val) => Ok(val),
            None => Err(Errors::TwoIsNone),
        };
    
        Test::unwrap_or_errors(x, y)
    }
    

    This is still slightly redundant, but you can switch to a macro to fix that:

    struct Test {
        x: i32,
        y: String,
    }
    
    macro_rules! unwrap_or_errors {
        ($($id: ident),*) => {
            match ($($id),*) {
                ($(Ok($id)),*) => Ok(Test { $($id),* }),
                ($($id),*) => Err([$($id.err()),*]
                    .into_iter()
                    .flatten()
                    .collect()),
            }
        }
    }
    
    #[derive(Debug)]
    enum Errors {
        OneIsNone,
        TwoIsNone,
    }
    
    fn new() -> Result<Test, Vec<Errors>> {
        let option1 = Some(5);
        let option2 = None;
    
        let x = match option1 {
            Some(val) => Ok(val),
            None => Err(Errors::OneIsNone),
        };
    
        let y = match option2 {
            Some(val) => Ok(val),
            None => Err(Errors::TwoIsNone),
        };
    
        unwrap_or_errors!(x, y)
    }