I came across a pretty tough design problem and would love to hear the opinions of wider community.
Consider this code:
// These strings may have come from a configuration file or something
let foo = string_from_user1.parse()?;
let bar = string_from_user2.parse()?;
let baz = string_from_user3.parse()?;
// ...
When a user runs this program on invalid input it can create considerable confusion because it's not clear which string is badly-formatted. Especially if, let's say all strings contain x
but only bar
disallows x
. The user would have to either search in documentation, look into the code or try different things. Neither seems appealing.
One could possibly fix this by instead writing:
// These strings may have come from a configuration file or something
let foo = string_from_user1.parse().map_err(|error| WrappedError(string_from_user1.to_owned()))?;
let bar = string_from_user2.parse().map_err(|error| WrappedError(string_from_user2.to_owned()))?;
let baz = string_from_user3.parse().map_err(|error| WrappedError(string_from_user3.to_owned()))?;
// ...
As you can imagine, this is annoying and error-prone. When I first understood this problem I decided to design all errors in libraries I (co-)maintain like this:
pub struct ParseError {
input: String,
invalid_char: char,
}
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(c) = s.find('x') {
return ParseError {
input: s.to_owned(),
invalid_char: c,
};
}
// rest of the code...
}
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "cannot parse {} as foo: the character {} is invalid", self.s, self.c)
}
This looks great at the first sight but has some issues around allocations.
The input string may not be desired in some cases - like in web forms where the string is already visible in the form input, so showing it again is just noise, or it may be processed in different way like conversion to serde::de::Error
using Unexpected::Str
which takes &str
and thus allocation is wasted.
A global feature flag is not great because the same type may be parsed from user and by serde
in the same application.
You might say "users are slow, so one allocation is not a big deal". The problem of library code is that the type can be parsed by non-user too - e.g. when parsing Json over some RPC. It may matter in those cases.
Note that similar situation applies for some non-parse errors such - e.g. storing file path. There's actually a discussion about this in std
where it also seems difficult.
I'm now really not sure what's the best approach for a library. Ideally I'd want to support all use cases but I don't see how. It looks like these requirements are at odds with each-other:
- Write idiomatic code using standard Rust traits (
FromStr
,TryFrom
); don't invent my own stuff - Make writing user-friendly applications easy
- Don't allocate when not needed.
Any ideas how to resolve this would be extremely appreciated. Thanks even for reading this!