intelli_shell/service/
variable.rs

1use std::{
2    collections::{BTreeMap, HashMap},
3    env,
4};
5
6use futures_util::{Stream, StreamExt};
7use heck::ToShoutySnakeCase;
8use tracing::instrument;
9
10use super::IntelliShellService;
11use crate::{
12    errors::Result,
13    model::{CommandTemplate, TemplatePart, Variable, VariableSuggestion, VariableValue},
14    utils::{format_env_var, get_working_dir, resolve_completions},
15};
16
17impl IntelliShellService {
18    /// Replaces the variables found in a command with their values.
19    ///
20    /// If one or more values are not found, they will be returned as an error.
21    #[instrument(skip_all)]
22    pub fn replace_command_variables(
23        &self,
24        command: String,
25        values: Vec<(String, Option<String>)>,
26        use_env: bool,
27    ) -> Result<String, Vec<String>> {
28        // Collect the values into a map for fast access
29        let values: HashMap<String, Option<String>> =
30            values.into_iter().map(|(n, v)| (n.to_shouty_snake_case(), v)).collect();
31
32        let mut output = String::new();
33        let mut missing = Vec::new();
34
35        // Parse the command into a template
36        let template = CommandTemplate::parse(command, true);
37        // For each one of the parts
38        for part in template.parts {
39            match part {
40                // Just parsed commands doesn't contain variable values
41                TemplatePart::VariableValue(_, _) => unreachable!(),
42                // Text parts are not variables so we can push them directly to the output
43                TemplatePart::Text(t) => output.push_str(&t),
44                // Variables must be replaced
45                TemplatePart::Variable(v) => {
46                    let env_var_names = v.env_var_names(false);
47                    let variable_value = env_var_names.iter().find_map(|env_var_name| values.get(env_var_name));
48                    match (variable_value, use_env) {
49                        // If the variable is present on the values map and it has a value
50                        (Some(Some(value)), _) => {
51                            // Push it to the output after applying the functions
52                            output.push_str(&v.apply_functions_to(value));
53                        }
54                        // If the variable is present on the map without a value, or the env can be read
55                        (Some(None), _) | (None, true) => {
56                            // Check the env
57                            let variable_value_env = env_var_names
58                                .iter()
59                                .find_map(|env_var_name| env::var(env_var_name).ok().map(|v| (env_var_name, v)));
60                            match (variable_value_env, v.secret) {
61                                // If there's no env var available, the value is missing
62                                (None, _) => missing.push(v.display),
63                                // If there' a value for a non-secret variable, push it
64                                (Some((_, env_value)), false) => {
65                                    output.push_str(&v.apply_functions_to(env_value));
66                                }
67                                // If there's a value but the variable is secret
68                                (Some((env_var_name, _)), true) => {
69                                    // Use the env var itself instead of the value, to avoid exposing the secret
70                                    output.push_str(&format_env_var(env_var_name));
71                                }
72                            }
73                        }
74                        // Otherwise, the variable value is missing
75                        _ => {
76                            missing.push(v.display);
77                        }
78                    }
79                }
80            }
81        }
82
83        if !missing.is_empty() { Err(missing) } else { Ok(output) }
84    }
85
86    /// Searches suggestions for the given variable
87    #[instrument(skip_all)]
88    #[allow(clippy::type_complexity)]
89    pub async fn search_variable_suggestions(
90        &self,
91        flat_root_cmd: &str,
92        variable: &Variable,
93        context: BTreeMap<String, String>,
94    ) -> Result<(
95        Vec<(u8, VariableSuggestion, f64)>,
96        Option<impl Stream<Item = (f64, Result<Vec<String>, String>)> + use<>>,
97    )> {
98        tracing::info!(
99            "Searching for variable suggestions: [{flat_root_cmd}] {}",
100            variable.flat_name
101        );
102
103        let mut suggestions = Vec::new();
104        let mut completion_stream = None;
105
106        if variable.secret {
107            // If the variable is a secret, suggest a new secret value
108            suggestions.push((0, VariableSuggestion::Secret, 0.0));
109            // And check if there's any env var that matches the variable to include it as a suggestion
110            let env_var_names = variable.env_var_names(true);
111            let env_var_len = env_var_names.len();
112            for (rev_ix, env_var_name) in env_var_names
113                .into_iter()
114                .enumerate()
115                .map(|(ix, item)| (env_var_len - 1 - ix, item))
116            {
117                if env::var(&env_var_name).is_ok() {
118                    suggestions.push((
119                        2,
120                        VariableSuggestion::Environment {
121                            env_var_name,
122                            value: None,
123                        },
124                        rev_ix as f64,
125                    ));
126                }
127            }
128        } else {
129            // Otherwise it's a regular variable, suggest a new value
130            suggestions.push((0, VariableSuggestion::New, 0.0));
131
132            // Find existing stored values for the variable
133            let mut existing_values = self
134                .storage
135                .find_variable_values(
136                    flat_root_cmd,
137                    &variable.flat_name,
138                    variable.flat_names.clone(),
139                    get_working_dir(),
140                    &context,
141                    &self.tuning.variables,
142                )
143                .await?;
144
145            // If there's a suggestion for a value previously selected for the same variable
146            let previous_value = context.get(&variable.flat_name).cloned();
147            if let Some(previous_value) = previous_value
148                && let Some(index) = existing_values.iter().position(|(s, _)| s.value == previous_value)
149            {
150                // Include it first, ignoring its relevance ordering
151                let (existing, _) = existing_values.remove(index);
152                suggestions.push((1, VariableSuggestion::Existing(existing), 0.0));
153            }
154
155            // Check if there's any env var that matches the variable to include it as a suggestion
156            let env_var_names = variable.env_var_names(true);
157            let env_var_len = env_var_names.len();
158            for (rev_ix, env_var_name) in env_var_names
159                .into_iter()
160                .enumerate()
161                .map(|(ix, item)| (env_var_len - 1 - ix, item))
162            {
163                if let Ok(value) = env::var(&env_var_name)
164                    && !value.trim().is_empty()
165                {
166                    let value = variable.apply_functions_to(value);
167                    // Check if there's already an existing value for that suggestion
168                    if let Some(existing_index) = existing_values.iter().position(|(s, _)| s.value == value) {
169                        // Include it now, ignoring its ordering
170                        let (existing, _) = existing_values.remove(existing_index);
171                        suggestions.push((2, VariableSuggestion::Existing(existing), rev_ix as f64));
172                    } else {
173                        // Otherwise, include the environment suggestion
174                        suggestions.push((
175                            2,
176                            VariableSuggestion::Environment {
177                                env_var_name,
178                                value: Some(value),
179                            },
180                            rev_ix as f64,
181                        ));
182                    };
183                }
184            }
185
186            // Include Remaining existing suggestions
187            suggestions.extend(
188                existing_values
189                    .into_iter()
190                    .map(|(s, score)| (3, VariableSuggestion::Existing(s), score)),
191            );
192
193            // And suggestions from the variable options (not already present)
194            let options = variable
195                .options
196                .iter()
197                .filter(|o| {
198                    !suggestions.iter().any(|(_, s, _)| match s {
199                        VariableSuggestion::Environment { value: Some(value), .. } => value == *o,
200                        VariableSuggestion::Existing(sv) => &sv.value == *o,
201                        VariableSuggestion::Completion(value) => value == *o,
202                        _ => false,
203                    })
204                })
205                .map(|o| (4, VariableSuggestion::Derived(o.to_owned()), 0.0))
206                .collect::<Vec<_>>();
207            suggestions.extend(options);
208
209            // Find and stream completions for the variable, which might be slow for network related completions
210            let completions = self
211                .storage
212                .get_completions_for(flat_root_cmd, variable.flat_names.clone())
213                .await?;
214            if !completions.is_empty() {
215                let completion_points = self.tuning.variables.completion.points as f64;
216                let stream = resolve_completions(completions, context.clone()).await;
217                completion_stream =
218                    Some(stream.map(move |(score_boost, result)| (completion_points + score_boost, result)));
219            }
220        }
221
222        Ok((suggestions, completion_stream))
223    }
224
225    /// Inserts a new variable value
226    #[instrument(skip_all)]
227    pub async fn insert_variable_value(&self, value: VariableValue) -> Result<VariableValue> {
228        tracing::info!(
229            "Inserting a variable value for '{}' '{}': {}",
230            value.flat_root_cmd,
231            value.flat_variable,
232            value.value
233        );
234        self.storage.insert_variable_value(value).await
235    }
236
237    /// Updates an existing variable value
238    #[instrument(skip_all)]
239    pub async fn update_variable_value(&self, value: VariableValue) -> Result<VariableValue> {
240        tracing::info!(
241            "Updating variable value '{}': {}",
242            value.id.unwrap_or_default(),
243            value.value
244        );
245        self.storage.update_variable_value(value).await
246    }
247
248    /// Increases the usage of a variable value, returning the new usage count
249    #[instrument(skip_all)]
250    pub async fn increment_variable_value_usage(
251        &self,
252        value_id: i32,
253        context: BTreeMap<String, String>,
254    ) -> Result<i32> {
255        tracing::info!("Increasing usage for variable value '{value_id}'");
256        self.storage
257            .increment_variable_value_usage(value_id, get_working_dir(), &context)
258            .await
259    }
260
261    /// Deletes an existing variable value
262    #[instrument(skip_all)]
263    pub async fn delete_variable_value(&self, id: i32) -> Result<()> {
264        tracing::info!("Deleting variable value: {}", id);
265        self.storage.delete_variable_value(id).await
266    }
267}