diff --git a/Cargo.lock b/Cargo.lock index 46c9cc061..b3299927a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -800,6 +800,7 @@ dependencies = [ "serde_json", "sp-core 18.0.0", "sp-keyring", + "strsim", "thiserror", "tracing", ] diff --git a/crates/transcode/Cargo.toml b/crates/transcode/Cargo.toml index 50ce6d8a6..d1fa587a1 100644 --- a/crates/transcode/Cargo.toml +++ b/crates/transcode/Cargo.toml @@ -36,6 +36,7 @@ scale-info = { version = "2.4.0", default-features = false, features = ["derive" serde = { version = "1.0.159", default-features = false, features = ["derive"] } serde_json = "1.0.95" thiserror = "1.0.40" +strsim = "0.10.0" [dev-dependencies] assert_matches = "1.5.0" diff --git a/crates/transcode/src/lib.rs b/crates/transcode/src/lib.rs index e803ec81e..defeae145 100644 --- a/crates/transcode/src/lib.rs +++ b/crates/transcode/src/lib.rs @@ -127,6 +127,7 @@ use ink_metadata::{ InkProject, MessageSpec, }; +use itertools::Itertools; use scale::{ Compact, Decode, @@ -140,6 +141,7 @@ use scale_info::{ Field, }; use std::{ + cmp::Ordering, fmt::Debug, path::Path, }; @@ -151,6 +153,24 @@ pub struct ContractMessageTranscoder { transcoder: Transcoder, } +/// Find strings from an iterable of `possible_values` similar to a given value `v` +/// Returns a Vec of all possible values that exceed a similarity threshold +/// sorted by ascending similarity, most similar comes last +/// Extracted from https://github.com/clap-rs/clap/blob/v4.3.4/clap_builder/src/parser/features/suggestions.rs#L11-L26 +fn did_you_mean(v: &str, possible_values: I) -> Vec +where + T: AsRef, + I: IntoIterator, +{ + let mut candidates: Vec<(f64, String)> = possible_values + .into_iter() + .map(|pv| (strsim::jaro(v, pv.as_ref()), pv.as_ref().to_owned())) + .filter(|(confidence, _)| *confidence > 0.7) + .collect(); + candidates.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal)); + candidates.into_iter().map(|(_, pv)| pv).collect() +} + impl ContractMessageTranscoder { pub fn new(metadata: InkProject) -> Self { let transcoder = TranscoderBuilder::new(metadata.registry()) @@ -200,9 +220,18 @@ impl ContractMessageTranscoder { )) } (None, None) => { + let constructors = self.constructors().map(|c| c.label()); + let messages = self.messages().map(|c| c.label()); + let possible_values: Vec<_> = constructors.chain(messages).collect(); + let help_txt = did_you_mean(name, possible_values.clone()) + .first() + .map(|suggestion| format!("Did you mean '{}'?", suggestion)) + .unwrap_or_else(|| { + format!("Should be one of: {}", possible_values.iter().join(", ")) + }); + return Err(anyhow::anyhow!( - "No constructor or message with the name '{}' found", - name + "No constructor or message with the name '{name}' found.\n{help_txt}", )) } }; @@ -538,6 +567,16 @@ mod tests { Ok(()) } + #[test] + fn encode_misspelled_arg() { + let metadata = generate_metadata(); + let transcoder = ContractMessageTranscoder::new(metadata); + assert_eq!( + transcoder.encode("fip", ["true"]).unwrap_err().to_string(), + "No constructor or message with the name 'fip' found.\nDid you mean 'flip'?" + ); + } + #[test] fn encode_mismatching_args_length() { let metadata = generate_metadata();