greentic-flow-builder 0.1.0

AI-powered Adaptive Card flow builder with visual graph editor and demo runner
Documentation
//! Fuzzy matching and suggestions using Levenshtein distance.

use strsim::normalized_damerau_levenshtein;

/// Find the closest match from a list of candidates.
/// Returns None if no candidate is at least 40% similar.
pub fn find_closest<'a>(target: &str, candidates: &[&'a str]) -> Option<&'a str> {
    let target_lower = target.to_lowercase();
    let mut best: Option<(&str, f64)> = None;
    for candidate in candidates {
        let score = normalized_damerau_levenshtein(&target_lower, &candidate.to_lowercase());
        if score >= 0.4 {
            match best {
                Some((_, prev_score)) if score > prev_score => best = Some((candidate, score)),
                None => best = Some((candidate, score)),
                _ => {}
            }
        }
    }
    best.map(|(c, _)| c)
}

/// Generate a user-friendly "did you mean" suggestion.
pub fn suggest_preset_name(target: &str, candidates: &[&str]) -> String {
    match find_closest(target, candidates) {
        Some(closest) => {
            format!("preset '{}' not found. Did you mean '{}'?", target, closest)
        }
        None => format!(
            "preset '{}' not found. Available: {}",
            target,
            candidates
                .iter()
                .take(5)
                .copied()
                .collect::<Vec<_>>()
                .join(", ")
        ),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_find_closest_exact_match() {
        let candidates = vec!["menu-card", "leave-request", "weather-current"];
        assert_eq!(find_closest("menu-card", &candidates), Some("menu-card"));
    }

    #[test]
    fn test_find_closest_typo() {
        let candidates = vec!["menu-card", "leave-request"];
        assert_eq!(find_closest("menu-crd", &candidates), Some("menu-card"));
    }

    #[test]
    fn test_find_closest_no_match() {
        let candidates = vec!["menu-card"];
        assert_eq!(find_closest("xyz", &candidates), None);
    }

    #[test]
    fn test_suggest_with_match() {
        let candidates = vec!["menu-card", "weather-current"];
        let msg = suggest_preset_name("menu-crd", &candidates);
        assert!(msg.contains("menu-card"));
        assert!(msg.contains("Did you mean"));
    }

    #[test]
    fn test_suggest_without_match() {
        let candidates = vec!["menu-card", "weather-current"];
        let msg = suggest_preset_name("qqq", &candidates);
        assert!(msg.contains("Available"));
    }
}