1use crate::types::IdlSpec;
4use strsim::levenshtein;
5
6#[derive(Debug, Clone)]
8pub struct Suggestion {
9 pub candidate: String,
10 pub distance: usize,
11}
12
13#[derive(Debug, Clone)]
15pub enum IdlSection {
16 Instruction,
17 Account,
18 Type,
19 Error,
20 Event,
21 Constant,
22}
23
24#[derive(Debug, Clone)]
26pub enum MatchType {
27 Exact,
28 CaseInsensitive,
29 Contains,
30 Fuzzy(usize),
31}
32
33#[derive(Debug, Clone)]
35pub struct SearchResult {
36 pub name: String,
37 pub section: IdlSection,
38 pub match_type: MatchType,
39}
40
41pub fn suggest_similar(name: &str, candidates: &[&str], max_distance: usize) -> Vec<Suggestion> {
48 let name_lower = name.to_lowercase();
49 let mut suggestions: Vec<Suggestion> = candidates
50 .iter()
51 .filter_map(|&candidate| {
52 if candidate == name {
54 return None;
55 }
56 let candidate_lower = candidate.to_lowercase();
57 if candidate_lower == name_lower {
59 return Some(Suggestion {
60 candidate: candidate.to_string(),
61 distance: 0,
62 });
63 }
64 if candidate_lower.contains(&name_lower) || name_lower.contains(&candidate_lower) {
66 return Some(Suggestion {
67 candidate: candidate.to_string(),
68 distance: 1,
69 });
70 }
71 let dist = levenshtein(name, candidate);
73 if dist <= max_distance {
74 Some(Suggestion {
75 candidate: candidate.to_string(),
76 distance: dist,
77 })
78 } else {
79 None
80 }
81 })
82 .collect();
83 suggestions.sort_by_key(|s| s.distance);
84 suggestions
85}
86
87pub fn search_idl(idl: &IdlSpec, query: &str) -> Vec<SearchResult> {
92 let mut results = Vec::new();
93 let q = query.to_lowercase();
94
95 for ix in &idl.instructions {
96 if ix.name.to_lowercase().contains(&q) {
97 results.push(SearchResult {
98 name: ix.name.clone(),
99 section: IdlSection::Instruction,
100 match_type: MatchType::Contains,
101 });
102 }
103 }
104 for acc in &idl.accounts {
105 if acc.name.to_lowercase().contains(&q) {
106 results.push(SearchResult {
107 name: acc.name.clone(),
108 section: IdlSection::Account,
109 match_type: MatchType::Contains,
110 });
111 }
112 }
113 for ty in &idl.types {
114 if ty.name.to_lowercase().contains(&q) {
115 results.push(SearchResult {
116 name: ty.name.clone(),
117 section: IdlSection::Type,
118 match_type: MatchType::Contains,
119 });
120 }
121 }
122 for err in &idl.errors {
123 if err.name.to_lowercase().contains(&q) {
124 results.push(SearchResult {
125 name: err.name.clone(),
126 section: IdlSection::Error,
127 match_type: MatchType::Contains,
128 });
129 }
130 }
131 for ev in &idl.events {
132 if ev.name.to_lowercase().contains(&q) {
133 results.push(SearchResult {
134 name: ev.name.clone(),
135 section: IdlSection::Event,
136 match_type: MatchType::Contains,
137 });
138 }
139 }
140 for c in &idl.constants {
141 if c.name.to_lowercase().contains(&q) {
142 results.push(SearchResult {
143 name: c.name.clone(),
144 section: IdlSection::Constant,
145 match_type: MatchType::Contains,
146 });
147 }
148 }
149 results
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn test_fuzzy_suggestions() {
158 let candidates = ["initialize", "close", "deposit"];
159 let suggestions = suggest_similar("initlize", &candidates, 3);
160 assert!(!suggestions.is_empty());
161 assert_eq!(suggestions[0].candidate, "initialize");
162 }
163
164 #[test]
165 fn test_fuzzy_case_insensitive() {
166 let candidates = ["Initialize", "close"];
167 let suggestions = suggest_similar("initialize", &candidates, 3);
168 assert!(!suggestions.is_empty());
169 assert_eq!(suggestions[0].candidate, "Initialize");
170 assert_eq!(suggestions[0].distance, 0);
171 }
172
173 #[test]
174 fn test_fuzzy_no_exact_match() {
175 let candidates = ["initialize"];
176 let suggestions = suggest_similar("initialize", &candidates, 3);
177 assert!(suggestions.is_empty(), "exact matches should be excluded");
178 }
179
180 #[test]
181 fn test_fuzzy_substring() {
182 let candidates = ["swap_exact_in", "close"];
183 let suggestions = suggest_similar("swap", &candidates, 3);
184 assert!(!suggestions.is_empty());
185 assert_eq!(suggestions[0].candidate, "swap_exact_in");
186 }
187
188 #[test]
189 fn test_search_idl() {
190 use crate::parse::parse_idl_file;
191 use std::path::PathBuf;
192 let path =
193 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/meteora_dlmm.json");
194 let idl = parse_idl_file(&path).expect("should parse");
195 let results = search_idl(&idl, "swap");
196 assert!(!results.is_empty(), "should find results for 'swap'");
197 }
198}