diff --git a/Cargo.lock b/Cargo.lock index 400f09d4a..be1a1fc89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 2aaecd91f..d3b6fe796 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/src/scripting/serde_lua/mod.rs b/src/scripting/serde_lua/mod.rs index 5617a0bfe..d2552920e 100644 --- a/src/scripting/serde_lua/mod.rs +++ b/src/scripting/serde_lua/mod.rs @@ -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 +fn visit_table<'de, 'lua, V>( + table: Table<'lua>, + visitor: V, + struct_name: Option<&'static str>, + allowed_fields: Option<&'static [&'static str]>, +) -> Result where V: Visitor<'de>, { @@ -112,7 +117,93 @@ where let mut pairs = vec![]; for pair in table.pairs::() { 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(""), + 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( self, - name: &'static str, + struct_name: &'static str, fields: &'static [&'static str], v: V, ) -> Result @@ -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::>() - .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),