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

lua: give a nice error message when using an invalid field name

Make a reasonable suggestion to the user based on similarity with
possible field names, but don't error out.

This facilitates evolving the configuration over time, and makes
the config more forgiving of typos.
This commit is contained in:
Wez Furlong 2020-03-01 09:39:14 -08:00
parent d22a664bdb
commit 5f13e487ce
3 changed files with 111 additions and 19 deletions

9
Cargo.lock generated
View File

@ -365,7 +365,7 @@ dependencies = [
"ansi_term",
"atty",
"bitflags 1.2.1",
"strsim",
"strsim 0.8.0",
"textwrap",
"unicode-width",
"vec_map",
@ -2814,6 +2814,12 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "structopt"
version = "0.3.9"
@ -3517,6 +3523,7 @@ dependencies = [
"serial",
"shared_library",
"ssh2",
"strsim 0.10.0",
"structopt",
"tabout",
"term",

View File

@ -55,6 +55,7 @@ serde = {version="1.0", features = ["rc", "derive"]}
serial = "0.4"
ssh2 = "0.8"
structopt = "0.3"
strsim = "0.10"
tabout = { path = "tabout" }
term = { path = "term" }
termwiz = { path = "termwiz" }

View File

@ -75,7 +75,12 @@ impl<'de, 'lua> IntoDeserializer<'de, Error> for ValueWrapper<'lua> {
}
}
fn visit_table<'de, 'lua, V>(table: Table<'lua>, visitor: V) -> Result<V::Value, Error>
fn visit_table<'de, 'lua, V>(
table: Table<'lua>,
visitor: V,
struct_name: Option<&'static str>,
allowed_fields: Option<&'static [&'static str]>,
) -> Result<V::Value, Error>
where
V: Visitor<'de>,
{
@ -112,7 +117,93 @@ where
let mut pairs = vec![];
for pair in table.pairs::<String, Value>() {
match pair {
Ok(pair) => pairs.push((pair.0, ValueWrapper(pair.1))),
Ok(pair) => {
// When deserializing into a struct with known field names,
// we don't want to hard error if the user gave a bogus field
// name; we'd rather generate a warning somewhere and attempt
// to proceed. This makes the config a bit more forgiving of
// typos and also makes it easier to use a given config in
// a future version of wezterm where the configuration may
// evolve over time.
if let Some(allowed_fields) = allowed_fields {
if !allowed_fields.iter().any(|&name| name == &pair.0) {
// The field wasn't one of the allowed fields in this
// context. Generate an error message that is hopefully
// helpful; we'll suggest the set of most similar field
// names (ordered by similarity) and list out the remaining
// possible field names in alpha order
// Produce similar field name list
let mut candidates: Vec<(f64, &str)> = allowed_fields
.iter()
.map(|&name| (strsim::jaro_winkler(&pair.0, name), name))
.filter(|(confidence, _)| *confidence > 0.8)
.collect();
candidates.sort_by(|a, b| {
b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal)
});
let suggestions: Vec<&str> =
candidates.into_iter().map(|(_, name)| name).collect();
// Filter the suggestions out of the allowed field names
// and sort what remains.
let mut fields: Vec<&str> = allowed_fields
.iter()
.filter(|&name| {
!suggestions.iter().any(|candidate| candidate == name)
})
.map(|&name| name)
.collect();
fields.sort();
let mut message = String::new();
match suggestions.len() {
0 => {}
1 => {
message.push_str(&format!("Did you mean `{}`?", suggestions[0]))
}
_ => {
message.push_str("Did you mean one of ");
for (idx, candidate) in suggestions.iter().enumerate() {
if idx > 0 {
message.push_str(", ");
}
message.push('`');
message.push_str(candidate);
message.push('`');
}
message.push_str("?");
}
}
if !fields.is_empty() {
if suggestions.is_empty() {
message.push_str("Possible fields are ");
} else {
message.push_str(" Other possible fields are ");
}
for (idx, candidate) in fields.iter().enumerate() {
if idx > 0 {
message.push_str(", ");
}
message.push('`');
message.push_str(candidate);
message.push('`');
}
message.push('.');
}
log::error!(
"Ignoring unknown field `{}` in struct of type `{}`. {}",
pair.0,
struct_name.unwrap_or("<unknown>"),
message
);
continue;
}
}
pairs.push((pair.0, ValueWrapper(pair.1)))
}
Err(err) => {
return Err(Error::custom(format!(
"while retrieving map element: {}",
@ -168,7 +259,7 @@ impl<'de, 'lua> Deserializer<'de> for ValueWrapper<'lua> {
Ok(s) => visitor.visit_str(s),
Err(_) => visitor.visit_bytes(s.as_bytes()),
},
Value::Table(t) => visit_table(t, visitor),
Value::Table(t) => visit_table(t, visitor, None, None),
Value::UserData(_) | Value::LightUserData(_) => Err(Error::custom(
"cannot represent userdata in the serde data model",
)),
@ -303,7 +394,7 @@ impl<'de, 'lua> Deserializer<'de> for ValueWrapper<'lua> {
V: Visitor<'de>,
{
match self.0 {
Value::Table(t) => visit_table(t, v),
Value::Table(t) => visit_table(t, v, None, None),
_ => Err(serde::de::Error::invalid_type(
unexpected(&self.0),
&"sequence/array",
@ -433,7 +524,7 @@ impl<'de, 'lua> Deserializer<'de> for ValueWrapper<'lua> {
V: Visitor<'de>,
{
match self.0 {
Value::Table(t) => visit_table(t, v),
Value::Table(t) => visit_table(t, v, None, None),
_ => Err(serde::de::Error::invalid_type(
unexpected(&self.0),
&"a map",
@ -443,7 +534,7 @@ impl<'de, 'lua> Deserializer<'de> for ValueWrapper<'lua> {
fn deserialize_struct<V>(
self,
name: &'static str,
struct_name: &'static str,
fields: &'static [&'static str],
v: V,
) -> Result<V::Value, Self::Error>
@ -451,19 +542,12 @@ impl<'de, 'lua> Deserializer<'de> for ValueWrapper<'lua> {
V: Visitor<'de>,
{
match self.0 {
Value::Table(t) => match visit_table(t, v) {
Value::Table(t) => match visit_table(t, v, Some(struct_name), Some(fields)) {
Ok(v) => Ok(v),
Err(err) => {
let field_names = fields
.iter()
.map(|name| format!("`{}`", name))
.collect::<Vec<String>>()
.join(", ");
Err(Error::custom(format!(
"{} (while processing a struct of type `{}` and fields named {})",
err, name, field_names
)))
}
Err(err) => Err(Error::custom(format!(
"{} (while processing a struct of type `{}`)",
err, struct_name
))),
},
_ => Err(serde::de::Error::invalid_type(
unexpected(&self.0),