intelli_shell/service/
completion.rs

1use std::sync::LazyLock;
2
3use regex::Regex;
4use tracing::instrument;
5use uuid::Uuid;
6
7use super::IntelliShellService;
8use crate::{
9    errors::{Result, UserFacingError},
10    model::VariableCompletion,
11    utils::{flatten_str, flatten_variable_name},
12};
13
14pub static FORBIDDEN_COMPLETION_ROOT_CMD_CHARS: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[^\w-]").unwrap());
15pub static FORBIDDEN_COMPLETION_VARIABLE_CHARS: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[:|{}]").unwrap());
16
17impl IntelliShellService {
18    /// Lists all unique root commands for variable completions
19    #[instrument(skip_all)]
20    pub async fn list_variable_completion_root_cmds(&self) -> Result<Vec<String>> {
21        tracing::debug!("Listing variable completion root commands");
22        self.storage.list_variable_completion_root_cmds().await
23    }
24
25    /// Lists variable completions, optionally filtering by root command and variable name
26    #[instrument(skip_all)]
27    pub async fn list_variable_completions(&self, root_cmd: Option<&str>) -> Result<Vec<VariableCompletion>> {
28        tracing::debug!("Listing variable completions for '{:?}'", root_cmd);
29
30        let flat_root_cmd = root_cmd.map(flatten_str);
31
32        self.storage.list_variable_completions(flat_root_cmd, None, false).await
33    }
34
35    /// Creates a new variable completion
36    #[instrument(skip_all)]
37    pub async fn create_variable_completion(&self, var: VariableCompletion) -> Result<VariableCompletion> {
38        if var.flat_variable.is_empty() {
39            return Err(UserFacingError::CompletionEmptyVariable.into());
40        }
41        if var.suggestions_provider.is_empty() {
42            return Err(UserFacingError::CompletionEmptySuggestionsProvider.into());
43        }
44        if FORBIDDEN_COMPLETION_ROOT_CMD_CHARS.is_match(&var.root_cmd) {
45            return Err(UserFacingError::CompletionInvalidCommand.into());
46        }
47        if FORBIDDEN_COMPLETION_VARIABLE_CHARS.is_match(&var.variable) {
48            return Err(UserFacingError::CompletionInvalidVariable.into());
49        }
50
51        if var.is_global() {
52            tracing::info!(
53                "Creating a global variable completion for '{}': {}",
54                var.flat_variable,
55                var.suggestions_provider
56            );
57        } else {
58            tracing::info!(
59                "Creating a variable completion for '{}' '{}': {}",
60                var.flat_root_cmd,
61                var.flat_variable,
62                var.suggestions_provider
63            );
64        }
65
66        self.storage.insert_variable_completion(var).await
67    }
68
69    /// Updates a variable completion
70    #[instrument(skip_all)]
71    pub async fn update_variable_completion(&self, var: VariableCompletion) -> Result<VariableCompletion> {
72        if var.flat_variable.is_empty() {
73            return Err(UserFacingError::CompletionEmptyVariable.into());
74        }
75        if var.suggestions_provider.is_empty() {
76            return Err(UserFacingError::CompletionEmptySuggestionsProvider.into());
77        }
78        if FORBIDDEN_COMPLETION_ROOT_CMD_CHARS.is_match(&var.root_cmd) {
79            return Err(UserFacingError::CompletionInvalidCommand.into());
80        }
81        if FORBIDDEN_COMPLETION_VARIABLE_CHARS.is_match(&var.variable) {
82            return Err(UserFacingError::CompletionInvalidVariable.into());
83        }
84
85        tracing::info!(
86            "Updating variable completion '{}': {}",
87            var.id,
88            var.suggestions_provider
89        );
90
91        self.storage.update_variable_completion(var).await
92    }
93
94    /// Deletes a variable completion
95    #[instrument(skip_all)]
96    pub async fn delete_variable_completion(&self, id: Uuid) -> Result<()> {
97        tracing::info!("Deleting variable completion: {id}");
98        self.storage.delete_variable_completion(id).await
99    }
100
101    /// Deletes a variable completion by its unique key, returning it
102    #[instrument(skip_all)]
103    pub async fn delete_variable_completion_by_key(
104        &self,
105        root_cmd: impl AsRef<str>,
106        variable_name: impl AsRef<str>,
107    ) -> Result<Option<VariableCompletion>> {
108        let flat_root_cmd = flatten_str(root_cmd);
109        let flat_variable_name = flatten_variable_name(variable_name);
110
111        if flat_root_cmd.is_empty() {
112            tracing::info!("Deleting global variable completion for '{flat_variable_name}'");
113        } else {
114            tracing::info!("Deleting variable completion for '{flat_root_cmd}' '{flat_variable_name}'");
115        }
116
117        self.storage
118            .delete_variable_completion_by_key(flat_root_cmd, flat_variable_name)
119            .await
120    }
121}