Reputation: 739
I've started to learn Rust last week, by reading books and articles, and trying to convert some code from other languages at the same time.
I came across a situation which I'm trying to exemplify through the code below (which is a simplified version of what I was trying to convert from another language):
#[derive(Debug)]
struct InvalidStringSize;
impl std::fmt::Display for InvalidStringSize {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "string is too short")
}
}
impl std::error::Error for InvalidStringSize {}
pub fn extract_codes_as_ints(
message: String,
) -> Result<(i32, i32, i32), Box<dyn std::error::Error>> {
if message.len() < 20 {
return Err(Box::new(InvalidStringSize {}));
}
let code1: i32 = message[0..3].trim().parse()?;
let code2: i32 = message[9..14].trim().parse()?;
let code3: i32 = message[17..20].trim().parse()?;
Ok((code1, code2, code3))
}
So basically I want to extract 3 integers from specific positions of the given string (I could also try to check the other characters for some patterns, but I've left that part out).
I was wondering, is there a way to "catch" or verify all three results of the parse calls at the same time? I don't want to add a match block for each, I'd just like to check if anyone resulted in an error, and return another error in that case. Makes sense?
The only solution I could think of so far would be to create another function with all parses, and match its result. Is there any other way to do this?
Also, any feedback/suggestions on other parts of the code is very welcome, I'm struggling to find out the "right way" to do things in Rust.
Upvotes: 9
Views: 5564
Reputation: 169018
The idiomatic way to accomplish this is to define your own error type and return it, with a From<T>
implementation for each error type T
that can occur in your function. The ?
operator will use From
conversions to match the error type your function is declared to return.
A boxed error is overkill here; just declare an enum listing all of the ways the function can fail. The variant for an integer parse error can even capture the caught error.
use std::error::Error;
use std::fmt::{Display, Formatter, Error as FmtError};
use std::num::ParseIntError;
#[derive(Debug, Clone)]
pub enum ExtractCodeError {
InvalidStringSize,
InvalidInteger(ParseIntError),
}
impl From<ParseIntError> for ExtractCodeError {
fn from(e: ParseIntError) -> Self {
Self::InvalidInteger(e)
}
}
impl Error for ExtractCodeError {}
impl Display for ExtractCodeError {
fn fmt(&self, f: &mut Formatter) -> Result<(), FmtError> {
match self {
Self::InvalidStringSize => write!(f, "string is too short"),
Self::InvalidInteger(e) => write!(f, "invalid integer: {}", e)
}
}
}
Aside: if you use the thiserror
crate to derive Error
, this code becomes way simpler:
use std::num::ParseIntError;
#[derive(Debug, Clone, thiserror::Error)]
pub enum ExtractCodeError {
#[error("string is too short")]
InvalidStringSize,
#[error("invalid integer: {0}")]
InvalidInteger(#[from] ParseIntError),
}
Now we just need to change your function's return type and have it return ExtractCodeError::InvalidStringSize
when the length is too short. Nothing else needs to change as a ParseIntError
is automatically converted into an ExtractCodeError
:
pub fn extract_codes_as_ints(
message: String,
) -> Result<(i32, i32, i32), ExtractCodeError> {
if message.len() < 20 {
return Err(ExtractCodeError::InvalidStringSize);
}
let code1: i32 = message[0..3].trim().parse()?;
let code2: i32 = message[9..14].trim().parse()?;
let code3: i32 = message[17..20].trim().parse()?;
Ok((code1, code2, code3))
}
As an added bonus, callers of this function will be able to inspect errors more easily than with a boxed dyn Error
.
In more complex cases, such as where you'd want to tweak the error slightly for each possible occurrence of a ParseIntError
, you can use .map_err()
on results to transform the error. For example:
something_that_can_fail.map_err(|e| SomeOtherError::Foo(e))?;
Upvotes: 5