1use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
59#[serde(rename_all = "camelCase")]
60pub struct CompletableArgument {
61 pub name: String,
63
64 #[serde(skip_serializing_if = "Option::is_none")]
66 pub description: Option<String>,
67
68 #[serde(default)]
70 pub required: bool,
71
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub completion: Option<CompletionConfig>,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79#[serde(rename_all = "camelCase")]
80pub struct CompletionConfig {
81 pub provider: CompletionProvider,
83
84 #[serde(flatten)]
86 pub config: HashMap<String, Value>,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91#[serde(rename_all = "camelCase")]
92pub enum CompletionProvider {
93 Static,
95 Resource,
97 Tool,
99 File,
101 Custom(String),
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107#[serde(rename_all = "camelCase")]
108pub struct CompletionRequest {
109 pub argument: String,
111
112 pub partial: String,
114
115 pub context: HashMap<String, String>,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
121#[serde(rename_all = "camelCase")]
122pub struct CompletionResponse {
123 pub completions: Vec<CompletionItem>,
125
126 #[serde(default)]
128 pub has_more: bool,
129
130 #[serde(skip_serializing_if = "Option::is_none")]
132 pub continuation_token: Option<String>,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
137#[serde(rename_all = "camelCase")]
138pub struct CompletionItem {
139 pub value: String,
141
142 #[serde(skip_serializing_if = "Option::is_none")]
144 pub label: Option<String>,
145
146 #[serde(skip_serializing_if = "Option::is_none")]
148 pub description: Option<String>,
149
150 #[serde(skip_serializing_if = "Option::is_none")]
152 pub icon: Option<String>,
153
154 #[serde(flatten)]
156 pub metadata: HashMap<String, Value>,
157}
158
159#[async_trait::async_trait]
161pub trait CompletionProviderTrait: Send + Sync {
162 async fn complete(
164 &self,
165 request: CompletionRequest,
166 ) -> crate::error::Result<CompletionResponse>;
167}
168
169#[derive(Debug)]
216pub struct StaticCompletionProvider {
217 items: Vec<CompletionItem>,
219}
220
221impl StaticCompletionProvider {
222 pub fn new(items: Vec<CompletionItem>) -> Self {
224 Self { items }
225 }
226
227 pub fn from_strings(values: Vec<String>) -> Self {
229 Self {
230 items: values
231 .into_iter()
232 .map(|value| CompletionItem {
233 value,
234 label: None,
235 description: None,
236 icon: None,
237 metadata: HashMap::new(),
238 })
239 .collect(),
240 }
241 }
242}
243
244#[async_trait::async_trait]
245impl CompletionProviderTrait for StaticCompletionProvider {
246 async fn complete(
247 &self,
248 request: CompletionRequest,
249 ) -> crate::error::Result<CompletionResponse> {
250 let completions: Vec<CompletionItem> = self
251 .items
252 .iter()
253 .filter(|item| {
254 item.value.starts_with(&request.partial)
255 || item
256 .label
257 .as_ref()
258 .is_some_and(|l| l.starts_with(&request.partial))
259 })
260 .cloned()
261 .collect();
262
263 Ok(CompletionResponse {
264 completions,
265 has_more: false,
266 continuation_token: None,
267 })
268 }
269}
270
271#[derive(Debug)]
273pub struct FileCompletionProvider {
274 base_dir: Option<std::path::PathBuf>,
276 extensions: Option<Vec<String>>,
278}
279
280impl FileCompletionProvider {
281 pub fn new() -> Self {
283 Self {
284 base_dir: None,
285 extensions: None,
286 }
287 }
288
289 pub fn with_base_dir(mut self, dir: std::path::PathBuf) -> Self {
291 self.base_dir = Some(dir);
292 self
293 }
294
295 pub fn with_extensions(mut self, extensions: Vec<String>) -> Self {
297 self.extensions = Some(extensions);
298 self
299 }
300}
301
302#[async_trait::async_trait]
303impl CompletionProviderTrait for FileCompletionProvider {
304 async fn complete(
305 &self,
306 request: CompletionRequest,
307 ) -> crate::error::Result<CompletionResponse> {
308 use std::path::Path;
309
310 let partial_path = Path::new(&request.partial);
311 let (dir_path, file_prefix) = if request.partial.ends_with(std::path::MAIN_SEPARATOR) {
312 (partial_path, "")
313 } else {
314 (
315 partial_path.parent().unwrap_or_else(|| Path::new(".")),
316 partial_path
317 .file_name()
318 .and_then(|s| s.to_str())
319 .unwrap_or(""),
320 )
321 };
322
323 let search_dir = if dir_path.is_absolute() {
324 dir_path.to_path_buf()
325 } else if let Some(base) = &self.base_dir {
326 base.join(dir_path)
327 } else {
328 dir_path.to_path_buf()
329 };
330
331 let mut completions = Vec::new();
332
333 if let Ok(entries) = std::fs::read_dir(&search_dir) {
334 for entry in entries.flatten() {
335 if let Some(name) = entry.file_name().to_str() {
336 if name.starts_with(file_prefix) {
337 let is_dir = entry.file_type().is_ok_and(|t| t.is_dir());
338
339 if !is_dir {
341 if let Some(exts) = &self.extensions {
342 let has_valid_ext = Path::new(name)
343 .extension()
344 .and_then(|e| e.to_str())
345 .is_some_and(|e| exts.iter().any(|ext| ext == e));
346
347 if !has_valid_ext {
348 continue;
349 }
350 }
351 }
352
353 let value = if request.partial.is_empty() {
354 name.to_string()
355 } else {
356 dir_path.join(name).to_string_lossy().to_string()
357 };
358
359 completions.push(CompletionItem {
360 value,
361 label: Some(name.to_string()),
362 description: if is_dir {
363 Some("Directory".to_string())
364 } else {
365 Some("File".to_string())
366 },
367 icon: if is_dir {
368 Some("📁".to_string())
369 } else {
370 Some("📄".to_string())
371 },
372 metadata: HashMap::new(),
373 });
374 }
375 }
376 }
377 }
378
379 completions.sort_by(|a, b| a.value.cmp(&b.value));
381
382 Ok(CompletionResponse {
383 completions,
384 has_more: false,
385 continuation_token: None,
386 })
387 }
388}
389
390impl Default for FileCompletionProvider {
391 fn default() -> Self {
392 Self::new()
393 }
394}
395
396#[derive(Debug)]
398pub struct CompletableBuilder {
399 name: String,
400 description: Option<String>,
401 required: bool,
402 completion: Option<CompletionConfig>,
403}
404
405impl CompletableBuilder {
406 pub fn new(name: impl Into<String>) -> Self {
408 Self {
409 name: name.into(),
410 description: None,
411 required: false,
412 completion: None,
413 }
414 }
415
416 pub fn description(mut self, desc: impl Into<String>) -> Self {
418 self.description = Some(desc.into());
419 self
420 }
421
422 pub fn required(mut self) -> Self {
424 self.required = true;
425 self
426 }
427
428 pub fn static_completions(mut self, values: Vec<String>) -> Self {
430 self.completion = Some(CompletionConfig {
431 provider: CompletionProvider::Static,
432 config: {
433 let mut config = HashMap::new();
434 config.insert("values".to_string(), serde_json::to_value(values).unwrap());
435 config
436 },
437 });
438 self
439 }
440
441 pub fn file_completions(mut self) -> Self {
443 self.completion = Some(CompletionConfig {
444 provider: CompletionProvider::File,
445 config: HashMap::new(),
446 });
447 self
448 }
449
450 pub fn build(self) -> CompletableArgument {
452 CompletableArgument {
453 name: self.name,
454 description: self.description,
455 required: self.required,
456 completion: self.completion,
457 }
458 }
459}
460
461pub fn completable(name: impl Into<String>) -> CompletableBuilder {
463 CompletableBuilder::new(name)
464}
465
466#[cfg(test)]
467mod tests {
468 use super::*;
469
470 #[tokio::test]
471 async fn test_static_completion() {
472 let provider = StaticCompletionProvider::from_strings(vec![
473 "apple".to_string(),
474 "apricot".to_string(),
475 "banana".to_string(),
476 "cherry".to_string(),
477 ]);
478
479 let request = CompletionRequest {
480 argument: "fruit".to_string(),
481 partial: "ap".to_string(),
482 context: HashMap::new(),
483 };
484
485 let response = provider.complete(request).await.unwrap();
486 assert_eq!(response.completions.len(), 2);
487 assert_eq!(response.completions[0].value, "apple");
488 assert_eq!(response.completions[1].value, "apricot");
489 }
490
491 #[test]
492 fn test_completable_builder() {
493 let arg = completable("environment")
494 .description("Target environment")
495 .required()
496 .static_completions(vec![
497 "development".to_string(),
498 "staging".to_string(),
499 "production".to_string(),
500 ])
501 .build();
502
503 assert_eq!(arg.name, "environment");
504 assert_eq!(arg.description, Some("Target environment".to_string()));
505 assert!(arg.required);
506 assert!(arg.completion.is_some());
507 }
508}