1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-23 05:12:40 +03:00

dynamic: improve error messaging

This commit is contained in:
Wez Furlong 2022-05-18 13:39:51 -07:00
parent c7387490a6
commit 42a7c1d481
4 changed files with 154 additions and 69 deletions

View File

@ -106,11 +106,10 @@ impl<'a> FieldInfo<'a> {
quote!( quote!(
#ident: #ident:
<#ty>::from_dynamic(value, options) <#ty>::from_dynamic(value, options)
.map_err(|source| wezterm_dynamic::Error::ErrorInField { .map_err(|source| source.field_context(
type_name: #struct_name, #struct_name,
field_name: #name, #name,
error: source.to_string(), obj))?,
})?,
) )
} else if let Some(try_from) = &self.try_from { } else if let Some(try_from) = &self.try_from {
match &self.allow_default { match &self.allow_default {
@ -120,16 +119,16 @@ impl<'a> FieldInfo<'a> {
Some(v) => { Some(v) => {
use std::convert::TryFrom; use std::convert::TryFrom;
let target = <#try_from>::from_dynamic(v, options) let target = <#try_from>::from_dynamic(v, options)
.map_err(|source| wezterm_dynamic::Error::ErrorInField { .map_err(|source| source.field_context(
type_name: #struct_name, #struct_name,
field_name: #name, #name,
error: source.to_string(), obj,
})?; ))?;
<#ty>::try_from(target) <#ty>::try_from(target)
.map_err(|source| wezterm_dynamic::Error::ErrorInField { .map_err(|source| wezterm_dynamic::Error::ErrorInField{
type_name: #struct_name, type_name:#struct_name,
field_name: #name, field_name:#name,
error: source.to_string(), error: format!("{:#}", source)
})? })?
} }
None => { None => {
@ -144,16 +143,16 @@ impl<'a> FieldInfo<'a> {
Some(v) => { Some(v) => {
use std::convert::TryFrom; use std::convert::TryFrom;
let target = <#try_from>::from_dynamic(v, options) let target = <#try_from>::from_dynamic(v, options)
.map_err(|source| wezterm_dynamic::Error::ErrorInField { .map_err(|source| source.field_context(
type_name: #struct_name, #struct_name,
field_name: #name, #name,
error: source.to_string(), obj,
})?; ))?;
<#ty>::try_from(target) <#ty>::try_from(target)
.map_err(|source| wezterm_dynamic::Error::ErrorInField { .map_err(|source| wezterm_dynamic::Error::ErrorInField{
type_name: #struct_name, type_name:#struct_name,
field_name: #name, field_name:#name,
error: source.to_string(), error: format!("{:#}", source),
})? })?
} }
None => { None => {
@ -167,16 +166,16 @@ impl<'a> FieldInfo<'a> {
#ident: { #ident: {
use std::convert::TryFrom; use std::convert::TryFrom;
let target = <#try_from>::from_dynamic(obj.get_by_str(#name).unwrap_or(&Value::Null), options) let target = <#try_from>::from_dynamic(obj.get_by_str(#name).unwrap_or(&Value::Null), options)
.map_err(|source| wezterm_dynamic::Error::ErrorInField { .map_err(|source| source.field_context(
type_name: #struct_name, #struct_name,
field_name: #name, #name,
error: source.to_string(), obj,
})?; ))?;
<#ty>::try_from(target) <#ty>::try_from(target)
.map_err(|source| wezterm_dynamic::Error::ErrorInField { .map_err(|source| wezterm_dynamic::Error::ErrorInField{
type_name: #struct_name, type_name:#struct_name,
field_name: #name, field_name:#name,
error: source.to_string(), error: format!("{:#}", source),
})? })?
}, },
) )
@ -189,11 +188,11 @@ impl<'a> FieldInfo<'a> {
#ident: match obj.get_by_str(#name) { #ident: match obj.get_by_str(#name) {
Some(v) => { Some(v) => {
<#ty>::from_dynamic(v, options) <#ty>::from_dynamic(v, options)
.map_err(|source| wezterm_dynamic::Error::ErrorInField { .map_err(|source| source.field_context(
type_name: #struct_name, #struct_name,
field_name: #name, #name,
error: source.to_string(), obj,
})? ))?
} }
None => { None => {
<#ty>::default() <#ty>::default()
@ -206,11 +205,11 @@ impl<'a> FieldInfo<'a> {
#ident: match obj.get_by_str(#name) { #ident: match obj.get_by_str(#name) {
Some(v) => { Some(v) => {
<#ty>::from_dynamic(v, options) <#ty>::from_dynamic(v, options)
.map_err(|source| wezterm_dynamic::Error::ErrorInField { .map_err(|source| source.field_context(
type_name: #struct_name, #struct_name,
field_name: #name, #name,
error: source.to_string(), obj,
})? ))?
} }
None => { None => {
#default() #default()
@ -225,11 +224,7 @@ impl<'a> FieldInfo<'a> {
obj.get_by_str(#name).unwrap_or(&Value::Null), obj.get_by_str(#name).unwrap_or(&Value::Null),
options options
) )
.map_err(|source| wezterm_dynamic::Error::ErrorInField { .map_err(|source| source.field_context(#struct_name, #name, obj))?,
type_name: #struct_name,
field_name: #name,
error: source.to_string(),
})?,
) )
} }
} }

View File

@ -1,4 +1,5 @@
use crate::fromdynamic::{FromDynamicOptions, UnknownFieldAction}; use crate::fromdynamic::{FromDynamicOptions, UnknownFieldAction};
use crate::object::Object;
use crate::value::Value; use crate::value::Value;
use thiserror::Error; use thiserror::Error;
@ -33,12 +34,18 @@ pub enum Error {
type_name: &'static str, type_name: &'static str,
num_keys: usize, num_keys: usize,
}, },
#[error("Error in {}::{}: {:#}", .type_name, .field_name, .error)] #[error("Error processing {}::{}: {:#}", .type_name, .field_name, .error)]
ErrorInField { ErrorInField {
type_name: &'static str, type_name: &'static str,
field_name: &'static str, field_name: &'static str,
error: String, error: String,
}, },
#[error("Error processing {} (types: {}) {:#}", .field_name.join("."), .type_name.join(", "), .error)]
ErrorInNestedField {
type_name: Vec<&'static str>,
field_name: Vec<&'static str>,
error: String,
},
#[error("`{}` is not a valid type to use as a field name in `{}`", .key_type, .type_name)] #[error("`{}` is not a valid type to use as a field name in `{}`", .key_type, .type_name)]
InvalidFieldType { InvalidFieldType {
type_name: &'static str, type_name: &'static str,
@ -147,32 +154,94 @@ impl Error {
} }
} }
if !fields.is_empty() { if !fields.is_empty() {
if suggestions.is_empty() {
message.push_str("Possible items are ");
} else {
message.push_str(" Other possible items are ");
}
let limit = 5; let limit = 5;
for (idx, candidate) in fields.iter().enumerate() {
if idx > 0 {
message.push_str(", ");
}
message.push('`');
message.push_str(candidate);
message.push('`');
if idx > limit {
break;
}
}
if fields.len() > limit { if fields.len() > limit {
message.push_str(&format!(" and {} others", fields.len() - limit)); message.push_str(
" There are too many alternatives to list here; consult the documentation!",
);
} else {
if suggestions.is_empty() {
message.push_str("Possible alternatives are ");
} else if suggestions.len() == 1 {
message.push_str(" The other option is ");
} else {
message.push_str(" Other alternatives are ");
}
for (idx, candidate) in fields.iter().enumerate() {
if idx > 0 {
message.push_str(", ");
}
message.push('`');
message.push_str(candidate);
message.push('`');
}
} }
message.push('.');
} }
message message
} }
pub fn field_context(
self,
type_name: &'static str,
field_name: &'static str,
obj: &Object,
) -> Self {
let is_leaf = !matches!(self, Self::ErrorInField { .. });
fn add_obj_context(is_leaf: bool, obj: &Object, message: String) -> String {
if is_leaf {
// Show the object as context.
// However, some objects, like the main config, are very large and
// it isn't helpful to show that, so only include it when the context
// is more reasonable.
let obj_str = format!("{:#?}", obj);
if obj_str.len() > 128 || obj_str.lines().count() > 10 {
message
} else {
format!("{}.\n{}", message, obj_str)
}
} else {
message
}
}
match self {
Self::NoConversion { source_type, .. } if source_type == "Null" => Self::ErrorInField {
type_name,
field_name,
error: add_obj_context(is_leaf, obj, format!("missing field `{}`", field_name)),
},
Self::ErrorInField {
type_name: child_type,
field_name: child_field,
error,
} => Self::ErrorInNestedField {
type_name: vec![type_name, child_type],
field_name: vec![field_name, child_field],
error,
},
Self::ErrorInNestedField {
type_name: mut child_type,
field_name: mut child_field,
error,
} => Self::ErrorInNestedField {
type_name: {
child_type.insert(0, type_name);
child_type
},
field_name: {
child_field.insert(0, field_name);
child_field
},
error,
},
_ => Self::ErrorInField {
type_name,
field_name,
error: add_obj_context(is_leaf, obj, format!("{:#}", self)),
},
}
}
} }
impl From<String> for Error { impl From<String> for Error {

View File

@ -65,7 +65,7 @@ impl<'a> std::hash::Hash for (dyn ObjectKeyTrait + 'a) {
} }
} }
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] #[derive(Clone, Default, PartialEq, Eq, Hash)]
pub struct Object { pub struct Object {
inner: BTreeMap<Value, Value>, inner: BTreeMap<Value, Value>,
} }
@ -77,6 +77,12 @@ impl Object {
} }
} }
impl std::fmt::Debug for Object {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
self.inner.fmt(fmt)
}
}
impl Ord for Object { impl Ord for Object {
fn cmp(&self, other: &Self) -> Ordering { fn cmp(&self, other: &Self) -> Ordering {
let self_ptr = self as *const Self; let self_ptr = self as *const Self;

View File

@ -6,7 +6,7 @@ use ordered_float::OrderedFloat;
/// Value is intended to be convertible to the same set /// Value is intended to be convertible to the same set
/// of types as Lua and is a superset of the types possible /// of types as Lua and is a superset of the types possible
/// in TOML and JSON. /// in TOML and JSON.
#[derive(Clone, Debug, PartialEq, Hash, Eq, Ord, PartialOrd)] #[derive(Clone, PartialEq, Hash, Eq, Ord, PartialOrd)]
pub enum Value { pub enum Value {
Null, Null,
Bool(bool), Bool(bool),
@ -24,6 +24,21 @@ impl Default for Value {
} }
} }
impl std::fmt::Debug for Value {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::String(s) => fmt.write_fmt(format_args!("{:?}", s)),
Self::Null => fmt.write_str("nil"),
Self::Bool(i) => i.fmt(fmt),
Self::I64(i) => i.fmt(fmt),
Self::U64(i) => i.fmt(fmt),
Self::F64(i) => i.fmt(fmt),
Self::Array(a) => a.fmt(fmt),
Self::Object(o) => o.fmt(fmt),
}
}
}
impl Value { impl Value {
pub fn variant_name(&self) -> &str { pub fn variant_name(&self) -> &str {
match self { match self {