fix(from_str, try_from_into): custom error types

Remove the use of trait objects as errors from `from_str` and
`try_from_into`; they seem to have caused a lot of confusion in
practice. (Also, it's considered best practice to use custom error
types instead of boxed errors in library code.) Instead, use custom
error enums, and update hints accordingly. Hints also provide
some guidance about converting errors, which could be covered
more completely in a future advanced errors section.

Also move from_str to directly after the similar exercise `from_into`,
for the sake of familiarity when solving.
This commit is contained in:
Taylor Yu 2021-06-24 21:33:41 -05:00
parent de6c45ad24
commit 2dc93cadda
3 changed files with 127 additions and 56 deletions

View File

@ -1,16 +1,31 @@
// This does practically the same thing that TryFrom<&str> does.
// from_str.rs
// This is similar to from_into.rs, but this time we'll implement `FromStr`
// and return errors instead of falling back to a default value.
// Additionally, upon implementing FromStr, you can use the `parse` method
// on strings to generate an object of the implementor type.
// You can read more about it at https://doc.rust-lang.org/std/str/trait.FromStr.html
use std::error;
use std::num::ParseIntError;
use std::str::FromStr;
#[derive(Debug)]
#[derive(Debug, PartialEq)]
struct Person {
name: String,
age: usize,
}
// We will use this error type for the `FromStr` implementation.
#[derive(Debug, PartialEq)]
enum ParsePersonError {
// Empty input string
Empty,
// Incorrect number of fields
BadLen,
// Empty name field
NoName,
// Wrapped error from parse::<usize>()
ParseInt(ParseIntError),
}
// I AM NOT DONE
// Steps:
@ -24,7 +39,7 @@ struct Person {
// If everything goes well, then return a Result of a Person object
impl FromStr for Person {
type Err = Box<dyn error::Error>;
type Err = ParsePersonError;
fn from_str(s: &str) -> Result<Person, Self::Err> {
}
}
@ -40,7 +55,7 @@ mod tests {
#[test]
fn empty_input() {
assert!("".parse::<Person>().is_err());
assert_eq!("".parse::<Person>(), Err(ParsePersonError::Empty));
}
#[test]
fn good_input() {
@ -52,41 +67,56 @@ mod tests {
}
#[test]
fn missing_age() {
assert!("John,".parse::<Person>().is_err());
assert!(matches!(
"John,".parse::<Person>(),
Err(ParsePersonError::ParseInt(_))
));
}
#[test]
fn invalid_age() {
assert!("John,twenty".parse::<Person>().is_err());
assert!(matches!(
"John,twenty".parse::<Person>(),
Err(ParsePersonError::ParseInt(_))
));
}
#[test]
fn missing_comma_and_age() {
assert!("John".parse::<Person>().is_err());
assert_eq!("John".parse::<Person>(), Err(ParsePersonError::BadLen));
}
#[test]
fn missing_name() {
assert!(",1".parse::<Person>().is_err());
assert_eq!(",1".parse::<Person>(), Err(ParsePersonError::NoName));
}
#[test]
fn missing_name_and_age() {
assert!(",".parse::<Person>().is_err());
assert!(matches!(
",".parse::<Person>(),
Err(ParsePersonError::NoName | ParsePersonError::ParseInt(_))
));
}
#[test]
fn missing_name_and_invalid_age() {
assert!(",one".parse::<Person>().is_err());
assert!(matches!(
",one".parse::<Person>(),
Err(ParsePersonError::NoName | ParsePersonError::ParseInt(_))
));
}
#[test]
fn trailing_comma() {
assert!("John,32,".parse::<Person>().is_err());
assert_eq!("John,32,".parse::<Person>(), Err(ParsePersonError::BadLen));
}
#[test]
fn trailing_comma_and_some_string() {
assert!("John,32,man".parse::<Person>().is_err());
assert_eq!(
"John,32,man".parse::<Person>(),
Err(ParsePersonError::BadLen)
);
}
}

View File

@ -1,9 +1,9 @@
// try_from_into.rs
// TryFrom is a simple and safe type conversion that may fail in a controlled way under some circumstances.
// Basically, this is the same as From. The main difference is that this should return a Result type
// instead of the target type itself.
// You can read more about it at https://doc.rust-lang.org/std/convert/trait.TryFrom.html
use std::convert::{TryFrom, TryInto};
use std::error;
#[derive(Debug, PartialEq)]
struct Color {
@ -12,12 +12,21 @@ struct Color {
blue: u8,
}
// We will use this error type for these `TryFrom` conversions.
#[derive(Debug, PartialEq)]
enum IntoColorError {
// Incorrect length of slice
BadLen,
// Integer conversion error
IntConversion,
}
// I AM NOT DONE
// Your task is to complete this implementation
// and return an Ok result of inner type Color.
// You need to create an implementation for a tuple of three integers,
// an array of three integers and a slice of integers.
// an array of three integers, and a slice of integers.
//
// Note that the implementation for tuple and array will be checked at compile time,
// but the slice implementation needs to check the slice length!
@ -25,20 +34,23 @@ struct Color {
// Tuple implementation
impl TryFrom<(i16, i16, i16)> for Color {
type Error = Box<dyn error::Error>;
fn try_from(tuple: (i16, i16, i16)) -> Result<Self, Self::Error> {}
type Error = IntoColorError;
fn try_from(tuple: (i16, i16, i16)) -> Result<Self, Self::Error> {
}
}
// Array implementation
impl TryFrom<[i16; 3]> for Color {
type Error = Box<dyn error::Error>;
fn try_from(arr: [i16; 3]) -> Result<Self, Self::Error> {}
type Error = IntoColorError;
fn try_from(arr: [i16; 3]) -> Result<Self, Self::Error> {
}
}
// Slice implementation
impl TryFrom<&[i16]> for Color {
type Error = Box<dyn error::Error>;
fn try_from(slice: &[i16]) -> Result<Self, Self::Error> {}
type Error = IntoColorError;
fn try_from(slice: &[i16]) -> Result<Self, Self::Error> {
}
}
fn main() {
@ -46,15 +58,15 @@ fn main() {
let c1 = Color::try_from((183, 65, 14));
println!("{:?}", c1);
// Since From is implemented for Color, we should be able to use Into
// Since TryFrom is implemented for Color, we should be able to use TryInto
let c2: Result<Color, _> = [183, 65, 14].try_into();
println!("{:?}", c2);
let v = vec![183, 65, 14];
// With slice we should use `from` function
// With slice we should use `try_from` function
let c3 = Color::try_from(&v[..]);
println!("{:?}", c3);
// or take slice within round brackets and use Into
// or take slice within round brackets and use TryInto
let c4: Result<Color, _> = (&v[..]).try_into();
println!("{:?}", c4);
}
@ -65,15 +77,24 @@ mod tests {
#[test]
fn test_tuple_out_of_range_positive() {
assert!(Color::try_from((256, 1000, 10000)).is_err());
assert_eq!(
Color::try_from((256, 1000, 10000)),
Err(IntoColorError::IntConversion)
);
}
#[test]
fn test_tuple_out_of_range_negative() {
assert!(Color::try_from((-1, -10, -256)).is_err());
assert_eq!(
Color::try_from((-1, -10, -256)),
Err(IntoColorError::IntConversion)
);
}
#[test]
fn test_tuple_sum() {
assert!(Color::try_from((-1, 255, 255)).is_err());
assert_eq!(
Color::try_from((-1, 255, 255)),
Err(IntoColorError::IntConversion)
);
}
#[test]
fn test_tuple_correct() {
@ -91,17 +112,17 @@ mod tests {
#[test]
fn test_array_out_of_range_positive() {
let c: Result<Color, _> = [1000, 10000, 256].try_into();
assert!(c.is_err());
assert_eq!(c, Err(IntoColorError::IntConversion));
}
#[test]
fn test_array_out_of_range_negative() {
let c: Result<Color, _> = [-10, -256, -1].try_into();
assert!(c.is_err());
assert_eq!(c, Err(IntoColorError::IntConversion));
}
#[test]
fn test_array_sum() {
let c: Result<Color, _> = [-1, 255, 255].try_into();
assert!(c.is_err());
assert_eq!(c, Err(IntoColorError::IntConversion));
}
#[test]
fn test_array_correct() {
@ -119,17 +140,26 @@ mod tests {
#[test]
fn test_slice_out_of_range_positive() {
let arr = [10000, 256, 1000];
assert!(Color::try_from(&arr[..]).is_err());
assert_eq!(
Color::try_from(&arr[..]),
Err(IntoColorError::IntConversion)
);
}
#[test]
fn test_slice_out_of_range_negative() {
let arr = [-256, -1, -10];
assert!(Color::try_from(&arr[..]).is_err());
assert_eq!(
Color::try_from(&arr[..]),
Err(IntoColorError::IntConversion)
);
}
#[test]
fn test_slice_sum() {
let arr = [-1, 255, 255];
assert!(Color::try_from(&arr[..]).is_err());
assert_eq!(
Color::try_from(&arr[..]),
Err(IntoColorError::IntConversion)
);
}
#[test]
fn test_slice_correct() {
@ -148,11 +178,11 @@ mod tests {
#[test]
fn test_slice_excess_length() {
let v = vec![0, 0, 0, 0];
assert!(Color::try_from(&v[..]).is_err());
assert_eq!(Color::try_from(&v[..]), Err(IntoColorError::BadLen));
}
#[test]
fn test_slice_insufficient_length() {
let v = vec![0, 0];
assert!(Color::try_from(&v[..]).is_err());
assert_eq!(Color::try_from(&v[..]), Err(IntoColorError::BadLen));
}
}

View File

@ -925,6 +925,27 @@ mode = "test"
hint = """
Follow the steps provided right before the `From` implementation"""
[[exercises]]
name = "from_str"
path = "exercises/conversions/from_str.rs"
mode = "test"
hint = """
The implementation of FromStr should return an Ok with a Person object,
or an Err with an error if the string is not valid.
This is almost like the `from_into` exercise, but returning errors instead
of falling back to a default value.
Hint: Look at the test cases to see which error variants to return.
Another hint: You can use the `map_err` method of `Result` with a function
or a closure to wrap the error from `parse::<usize>`.
Yet another hint: If you would like to propagate errors by using the `?`
operator in your solution, you might want to look at
https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reenter_question_mark.html
"""
[[exercises]]
name = "try_from_into"
path = "exercises/conversions/try_from_into.rs"
@ -933,17 +954,19 @@ hint = """
Follow the steps provided right before the `TryFrom` implementation.
You can also use the example at https://doc.rust-lang.org/std/convert/trait.TryFrom.html
You might want to look back at the exercise errors5 (or its hints) to remind
yourself about how `Box<dyn Error>` works.
Hint: Is there an implementation of `TryFrom` in the standard library that
can both do the required integer conversion and check the range of the input?
If you're trying to return a string as an error, note that neither `str`
nor `String` implements `error::Error`. However, there is an implementation
of `From<&str>` for `Box<dyn Error>`. This means you can use `.into()` or
the `?` operator to convert your string into the correct error type.
Another hint: Look at the test cases to see which error variants to return.
If you're having trouble with using the `?` operator to convert an error string,
recall that `?` works to convert `Err(something)` into the appropriate error
type for returning from the function."""
Yet another hint: You can use the `map_err` or `or` methods of `Result` to
convert errors.
Yet another hint: If you would like to propagate errors by using the `?`
operator in your solution, you might want to look at
https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reenter_question_mark.html
Challenge: Can you make the `TryFrom` implementations generic over many integer types?"""
[[exercises]]
name = "as_ref_mut"
@ -951,15 +974,3 @@ path = "exercises/conversions/as_ref_mut.rs"
mode = "test"
hint = """
Add AsRef<str> as a trait bound to the functions."""
[[exercises]]
name = "from_str"
path = "exercises/conversions/from_str.rs"
mode = "test"
hint = """
The implementation of FromStr should return an Ok with a Person object,
or an Err with an error if the string is not valid.
This is almost like the `try_from_into` exercise.
If you're having trouble with returning the correct error type, see the
hints for try_from_into."""