Is there any specific technical reason for why env config crates stop after encountering the first error? Wouldn't it be better if you could see all of the errors in your configuration files with detailed source spans about which file, what type mismatch etc altogether instead of fixing one and rechecking again?
I made a crate that does this, but is there some sort of fundamental issue that I am missing here? Is this related to some sort of practical usage related problem?
heya, I think it's because of the following reasons:
- When we learn programming, we're not taught to accumulate errors, so it may not even cross our minds that "oh, just because an error happens, doesn't mean I need to return control flow immediately".
- Programming languages make it very natural to either crash or pass control flow upward on the first error. and to deliberately write error accumulation code takes both awareness and intention.
I know you know how to; but a younger version of myself wouldn't know what that means until seeing it in code, so here's an illustration
// quasi-code
fn config_load(path: &Path) -> Result<Config, Vec<ConfigLoadError>> {
let mut errors = Vec::new(); // <-- deliberately accumulate errors
let config_raw = serde_yaml::from_file(path)
.map_err(|error| vec![ConfigLoadError::Deserialize(error)])?;
// note: don't just parse, then use `?` to return early.
// save any error in `errors`
let key1 = match parse_key1(config_raw.key1) {
Ok(key1) => Some(key1),
Err(error) => {
errors.push(ConfigLoadError::Key1Invalid(error));
None
}
};
let key2 = match parse_key2(config_raw.key2) {
Ok(key2) => Some(key2),
Err(error) => {
errors.push(ConfigLoadError::Key2Invalid(error));
None
}
};
// either:
// key1.zip(key2).map(Config::new).ok_or(errors)
// or
if errors.is_empty() {
Ok(Config::new(key1, key2))
} else {
Err(errors)
}
}
1 Like
Hahaha, literally what I did in my code
But I guess most people find the accumulation unnecessary. Repeatedly finding errors and fixing your config files is probably slightly annoying at best for most, so people don't consider this to be a worthwhile approach considering the added complexity.
quote! {
let #local_var: std::option::Option<std::option::Option<#inner_ty>> = match __obj.get(#field_name_str) {
std::option::Option::Some(v) if !v.is_null() => {
match ::serde_json::from_value::<#inner_ty>(v.clone()) {
std::result::Result::Ok(parsed) => std::option::Option::Some(std::option::Option::Some(parsed)),
std::result::Result::Err(e) => {
__errors.push(::procenv::Error::extraction(
#field_name_str,
#type_name,
e.to_string()
));
std::option::Option::None
}
}
}
_ => std::option::Option::Some(std::option::Option::None),
};
}
1 Like
