Skip to content

Commit 2dc93ca

Browse files
committed
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.
1 parent de6c45a commit 2dc93ca

File tree

3 files changed

+127
-56
lines changed

3 files changed

+127
-56
lines changed

exercises/conversions/from_str.rs

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,31 @@
1-
// This does practically the same thing that TryFrom<&str> does.
1+
// from_str.rs
2+
// This is similar to from_into.rs, but this time we'll implement `FromStr`
3+
// and return errors instead of falling back to a default value.
24
// Additionally, upon implementing FromStr, you can use the `parse` method
35
// on strings to generate an object of the implementor type.
46
// You can read more about it at https://doc.rust-lang.org/std/str/trait.FromStr.html
5-
use std::error;
7+
use std::num::ParseIntError;
68
use std::str::FromStr;
79

8-
#[derive(Debug)]
10+
#[derive(Debug, PartialEq)]
911
struct Person {
1012
name: String,
1113
age: usize,
1214
}
1315

16+
// We will use this error type for the `FromStr` implementation.
17+
#[derive(Debug, PartialEq)]
18+
enum ParsePersonError {
19+
// Empty input string
20+
Empty,
21+
// Incorrect number of fields
22+
BadLen,
23+
// Empty name field
24+
NoName,
25+
// Wrapped error from parse::<usize>()
26+
ParseInt(ParseIntError),
27+
}
28+
1429
// I AM NOT DONE
1530

1631
// Steps:
@@ -24,7 +39,7 @@ struct Person {
2439
// If everything goes well, then return a Result of a Person object
2540

2641
impl FromStr for Person {
27-
type Err = Box<dyn error::Error>;
42+
type Err = ParsePersonError;
2843
fn from_str(s: &str) -> Result<Person, Self::Err> {
2944
}
3045
}
@@ -40,7 +55,7 @@ mod tests {
4055

4156
#[test]
4257
fn empty_input() {
43-
assert!("".parse::<Person>().is_err());
58+
assert_eq!("".parse::<Person>(), Err(ParsePersonError::Empty));
4459
}
4560
#[test]
4661
fn good_input() {
@@ -52,41 +67,56 @@ mod tests {
5267
}
5368
#[test]
5469
fn missing_age() {
55-
assert!("John,".parse::<Person>().is_err());
70+
assert!(matches!(
71+
"John,".parse::<Person>(),
72+
Err(ParsePersonError::ParseInt(_))
73+
));
5674
}
5775

5876
#[test]
5977
fn invalid_age() {
60-
assert!("John,twenty".parse::<Person>().is_err());
78+
assert!(matches!(
79+
"John,twenty".parse::<Person>(),
80+
Err(ParsePersonError::ParseInt(_))
81+
));
6182
}
6283

6384
#[test]
6485
fn missing_comma_and_age() {
65-
assert!("John".parse::<Person>().is_err());
86+
assert_eq!("John".parse::<Person>(), Err(ParsePersonError::BadLen));
6687
}
6788

6889
#[test]
6990
fn missing_name() {
70-
assert!(",1".parse::<Person>().is_err());
91+
assert_eq!(",1".parse::<Person>(), Err(ParsePersonError::NoName));
7192
}
7293

7394
#[test]
7495
fn missing_name_and_age() {
75-
assert!(",".parse::<Person>().is_err());
96+
assert!(matches!(
97+
",".parse::<Person>(),
98+
Err(ParsePersonError::NoName | ParsePersonError::ParseInt(_))
99+
));
76100
}
77101

78102
#[test]
79103
fn missing_name_and_invalid_age() {
80-
assert!(",one".parse::<Person>().is_err());
104+
assert!(matches!(
105+
",one".parse::<Person>(),
106+
Err(ParsePersonError::NoName | ParsePersonError::ParseInt(_))
107+
));
81108
}
82109

83110
#[test]
84111
fn trailing_comma() {
85-
assert!("John,32,".parse::<Person>().is_err());
112+
assert_eq!("John,32,".parse::<Person>(), Err(ParsePersonError::BadLen));
86113
}
87114

88115
#[test]
89116
fn trailing_comma_and_some_string() {
90-
assert!("John,32,man".parse::<Person>().is_err());
117+
assert_eq!(
118+
"John,32,man".parse::<Person>(),
119+
Err(ParsePersonError::BadLen)
120+
);
91121
}
92122
}

exercises/conversions/try_from_into.rs

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1+
// try_from_into.rs
12
// TryFrom is a simple and safe type conversion that may fail in a controlled way under some circumstances.
23
// Basically, this is the same as From. The main difference is that this should return a Result type
34
// instead of the target type itself.
45
// You can read more about it at https://doc.rust-lang.org/std/convert/trait.TryFrom.html
56
use std::convert::{TryFrom, TryInto};
6-
use std::error;
77

88
#[derive(Debug, PartialEq)]
99
struct Color {
@@ -12,49 +12,61 @@ struct Color {
1212
blue: u8,
1313
}
1414

15+
// We will use this error type for these `TryFrom` conversions.
16+
#[derive(Debug, PartialEq)]
17+
enum IntoColorError {
18+
// Incorrect length of slice
19+
BadLen,
20+
// Integer conversion error
21+
IntConversion,
22+
}
23+
1524
// I AM NOT DONE
1625

1726
// Your task is to complete this implementation
1827
// and return an Ok result of inner type Color.
1928
// You need to create an implementation for a tuple of three integers,
20-
// an array of three integers and a slice of integers.
29+
// an array of three integers, and a slice of integers.
2130
//
2231
// Note that the implementation for tuple and array will be checked at compile time,
2332
// but the slice implementation needs to check the slice length!
2433
// Also note that correct RGB color values must be integers in the 0..=255 range.
2534

2635
// Tuple implementation
2736
impl TryFrom<(i16, i16, i16)> for Color {
28-
type Error = Box<dyn error::Error>;
29-
fn try_from(tuple: (i16, i16, i16)) -> Result<Self, Self::Error> {}
37+
type Error = IntoColorError;
38+
fn try_from(tuple: (i16, i16, i16)) -> Result<Self, Self::Error> {
39+
}
3040
}
3141

3242
// Array implementation
3343
impl TryFrom<[i16; 3]> for Color {
34-
type Error = Box<dyn error::Error>;
35-
fn try_from(arr: [i16; 3]) -> Result<Self, Self::Error> {}
44+
type Error = IntoColorError;
45+
fn try_from(arr: [i16; 3]) -> Result<Self, Self::Error> {
46+
}
3647
}
3748

3849
// Slice implementation
3950
impl TryFrom<&[i16]> for Color {
40-
type Error = Box<dyn error::Error>;
41-
fn try_from(slice: &[i16]) -> Result<Self, Self::Error> {}
51+
type Error = IntoColorError;
52+
fn try_from(slice: &[i16]) -> Result<Self, Self::Error> {
53+
}
4254
}
4355

4456
fn main() {
4557
// Use the `from` function
4658
let c1 = Color::try_from((183, 65, 14));
4759
println!("{:?}", c1);
4860

49-
// Since From is implemented for Color, we should be able to use Into
61+
// Since TryFrom is implemented for Color, we should be able to use TryInto
5062
let c2: Result<Color, _> = [183, 65, 14].try_into();
5163
println!("{:?}", c2);
5264

5365
let v = vec![183, 65, 14];
54-
// With slice we should use `from` function
66+
// With slice we should use `try_from` function
5567
let c3 = Color::try_from(&v[..]);
5668
println!("{:?}", c3);
57-
// or take slice within round brackets and use Into
69+
// or take slice within round brackets and use TryInto
5870
let c4: Result<Color, _> = (&v[..]).try_into();
5971
println!("{:?}", c4);
6072
}
@@ -65,15 +77,24 @@ mod tests {
6577

6678
#[test]
6779
fn test_tuple_out_of_range_positive() {
68-
assert!(Color::try_from((256, 1000, 10000)).is_err());
80+
assert_eq!(
81+
Color::try_from((256, 1000, 10000)),
82+
Err(IntoColorError::IntConversion)
83+
);
6984
}
7085
#[test]
7186
fn test_tuple_out_of_range_negative() {
72-
assert!(Color::try_from((-1, -10, -256)).is_err());
87+
assert_eq!(
88+
Color::try_from((-1, -10, -256)),
89+
Err(IntoColorError::IntConversion)
90+
);
7391
}
7492
#[test]
7593
fn test_tuple_sum() {
76-
assert!(Color::try_from((-1, 255, 255)).is_err());
94+
assert_eq!(
95+
Color::try_from((-1, 255, 255)),
96+
Err(IntoColorError::IntConversion)
97+
);
7798
}
7899
#[test]
79100
fn test_tuple_correct() {
@@ -91,17 +112,17 @@ mod tests {
91112
#[test]
92113
fn test_array_out_of_range_positive() {
93114
let c: Result<Color, _> = [1000, 10000, 256].try_into();
94-
assert!(c.is_err());
115+
assert_eq!(c, Err(IntoColorError::IntConversion));
95116
}
96117
#[test]
97118
fn test_array_out_of_range_negative() {
98119
let c: Result<Color, _> = [-10, -256, -1].try_into();
99-
assert!(c.is_err());
120+
assert_eq!(c, Err(IntoColorError::IntConversion));
100121
}
101122
#[test]
102123
fn test_array_sum() {
103124
let c: Result<Color, _> = [-1, 255, 255].try_into();
104-
assert!(c.is_err());
125+
assert_eq!(c, Err(IntoColorError::IntConversion));
105126
}
106127
#[test]
107128
fn test_array_correct() {
@@ -119,17 +140,26 @@ mod tests {
119140
#[test]
120141
fn test_slice_out_of_range_positive() {
121142
let arr = [10000, 256, 1000];
122-
assert!(Color::try_from(&arr[..]).is_err());
143+
assert_eq!(
144+
Color::try_from(&arr[..]),
145+
Err(IntoColorError::IntConversion)
146+
);
123147
}
124148
#[test]
125149
fn test_slice_out_of_range_negative() {
126150
let arr = [-256, -1, -10];
127-
assert!(Color::try_from(&arr[..]).is_err());
151+
assert_eq!(
152+
Color::try_from(&arr[..]),
153+
Err(IntoColorError::IntConversion)
154+
);
128155
}
129156
#[test]
130157
fn test_slice_sum() {
131158
let arr = [-1, 255, 255];
132-
assert!(Color::try_from(&arr[..]).is_err());
159+
assert_eq!(
160+
Color::try_from(&arr[..]),
161+
Err(IntoColorError::IntConversion)
162+
);
133163
}
134164
#[test]
135165
fn test_slice_correct() {
@@ -148,11 +178,11 @@ mod tests {
148178
#[test]
149179
fn test_slice_excess_length() {
150180
let v = vec![0, 0, 0, 0];
151-
assert!(Color::try_from(&v[..]).is_err());
181+
assert_eq!(Color::try_from(&v[..]), Err(IntoColorError::BadLen));
152182
}
153183
#[test]
154184
fn test_slice_insufficient_length() {
155185
let v = vec![0, 0];
156-
assert!(Color::try_from(&v[..]).is_err());
186+
assert_eq!(Color::try_from(&v[..]), Err(IntoColorError::BadLen));
157187
}
158188
}

info.toml

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -925,6 +925,27 @@ mode = "test"
925925
hint = """
926926
Follow the steps provided right before the `From` implementation"""
927927

928+
[[exercises]]
929+
name = "from_str"
930+
path = "exercises/conversions/from_str.rs"
931+
mode = "test"
932+
hint = """
933+
The implementation of FromStr should return an Ok with a Person object,
934+
or an Err with an error if the string is not valid.
935+
936+
This is almost like the `from_into` exercise, but returning errors instead
937+
of falling back to a default value.
938+
939+
Hint: Look at the test cases to see which error variants to return.
940+
941+
Another hint: You can use the `map_err` method of `Result` with a function
942+
or a closure to wrap the error from `parse::<usize>`.
943+
944+
Yet another hint: If you would like to propagate errors by using the `?`
945+
operator in your solution, you might want to look at
946+
https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reenter_question_mark.html
947+
"""
948+
928949
[[exercises]]
929950
name = "try_from_into"
930951
path = "exercises/conversions/try_from_into.rs"
@@ -933,33 +954,23 @@ hint = """
933954
Follow the steps provided right before the `TryFrom` implementation.
934955
You can also use the example at https://doc.rust-lang.org/std/convert/trait.TryFrom.html
935956
936-
You might want to look back at the exercise errors5 (or its hints) to remind
937-
yourself about how `Box<dyn Error>` works.
957+
Hint: Is there an implementation of `TryFrom` in the standard library that
958+
can both do the required integer conversion and check the range of the input?
959+
960+
Another hint: Look at the test cases to see which error variants to return.
938961
939-
If you're trying to return a string as an error, note that neither `str`
940-
nor `String` implements `error::Error`. However, there is an implementation
941-
of `From<&str>` for `Box<dyn Error>`. This means you can use `.into()` or
942-
the `?` operator to convert your string into the correct error type.
962+
Yet another hint: You can use the `map_err` or `or` methods of `Result` to
963+
convert errors.
964+
965+
Yet another hint: If you would like to propagate errors by using the `?`
966+
operator in your solution, you might want to look at
967+
https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reenter_question_mark.html
943968
944-
If you're having trouble with using the `?` operator to convert an error string,
945-
recall that `?` works to convert `Err(something)` into the appropriate error
946-
type for returning from the function."""
969+
Challenge: Can you make the `TryFrom` implementations generic over many integer types?"""
947970

948971
[[exercises]]
949972
name = "as_ref_mut"
950973
path = "exercises/conversions/as_ref_mut.rs"
951974
mode = "test"
952975
hint = """
953976
Add AsRef<str> as a trait bound to the functions."""
954-
955-
[[exercises]]
956-
name = "from_str"
957-
path = "exercises/conversions/from_str.rs"
958-
mode = "test"
959-
hint = """
960-
The implementation of FromStr should return an Ok with a Person object,
961-
or an Err with an error if the string is not valid.
962-
This is almost like the `try_from_into` exercise.
963-
964-
If you're having trouble with returning the correct error type, see the
965-
hints for try_from_into."""

0 commit comments

Comments
 (0)