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        previous_values: Option<Vec<String>>,
94        context: BTreeMap<String, String>,
95    ) -> Result<(
96        Vec<(u8, VariableSuggestion, f64)>,
97        Option<impl Stream<Item = (f64, Result<Vec<String>, String>)> + use<>>,
98    )> {
99        tracing::info!(
100            "Searching for variable suggestions: [{flat_root_cmd}] {}",
101            variable.flat_name
102        );
103
104        let mut suggestions = Vec::new();
105        if variable.secret {
106            // If the variable is a secret, suggest a new secret value
107            suggestions.push((0, VariableSuggestion::Secret, 0.0));
108            // Check if the user already selected any value for the same variable, to be suggested again
109            if let Some(values) = previous_values {
110                for (ix, value) in values.into_iter().enumerate() {
111                    suggestions.push((1, VariableSuggestion::Previous(value), ix as f64));
112                }
113            }
114            // And check if there's any env var that matches the variable to include it as a suggestion
115            let env_var_names = variable.env_var_names(true);
116            let env_var_len = env_var_names.len();
117            for (rev_ix, env_var_name) in env_var_names
118                .into_iter()
119                .enumerate()
120                .map(|(ix, item)| (env_var_len - 1 - ix, item))
121            {
122                if env::var(&env_var_name).is_ok() {
123                    suggestions.push((
124                        2,
125                        VariableSuggestion::Environment {
126                            env_var_name,
127                            value: None,
128                        },
129                        rev_ix as f64,
130                    ));
131                }
132            }
133        } else {
134            // Otherwise it's a regular variable, suggest a new value
135            suggestions.push((0, VariableSuggestion::New, 0.0));
136
137            // Find existing stored values for the variable
138            let mut existing_values = self
139                .storage
140                .find_variable_values(
141                    flat_root_cmd,
142                    &variable.flat_name,
143                    variable.flat_names.clone(),
144                    get_working_dir(),
145                    &context,
146                    &self.tuning.variables,
147                )
148                .await?;
149
150            // Check if the user already selected some value for the same variable
151            if let Some(values) = previous_values {
152                for (ix, value) in values.into_iter().enumerate() {
153                    // If there's an existing suggestion for a value previously selected
154                    if let Some(index) = existing_values.iter().position(|(s, _)| s.value == value) {
155                        // Include it first, ignoring its relevance ordering
156                        let (existing, _) = existing_values.remove(index);
157                        suggestions.push((1, VariableSuggestion::Existing(existing), ix as f64));
158                    } else {
159                        // If there's no stored value (previous value was secret), suggest it
160                        suggestions.push((1, VariableSuggestion::Previous(value), ix as f64));
161                    }
162                }
163            }
164
165            // Check if there's any env var that matches the variable to include it as a suggestion
166            let env_var_names = variable.env_var_names(true);
167            let env_var_len = env_var_names.len();
168            for (rev_ix, env_var_name) in env_var_names
169                .into_iter()
170                .enumerate()
171                .map(|(ix, item)| (env_var_len - 1 - ix, item))
172            {
173                if let Ok(value) = env::var(&env_var_name)
174                    && !value.trim().is_empty()
175                {
176                    let value = variable.apply_functions_to(value);
177                    // Check if there's already an existing value for that suggestion
178                    if let Some(existing_index) = existing_values.iter().position(|(s, _)| s.value == value) {
179                        // Include it now, ignoring its ordering
180                        let (existing, _) = existing_values.remove(existing_index);
181                        suggestions.push((2, VariableSuggestion::Existing(existing), rev_ix as f64));
182                    } else {
183                        // Otherwise, include the environment suggestion
184                        suggestions.push((
185                            2,
186                            VariableSuggestion::Environment {
187                                env_var_name,
188                                value: Some(value),
189                            },
190                            rev_ix as f64,
191                        ));
192                    };
193                }
194            }
195
196            // Include Remaining existing suggestions
197            suggestions.extend(
198                existing_values
199                    .into_iter()
200                    .map(|(s, score)| (3, VariableSuggestion::Existing(s), score)),
201            );
202
203            // And suggestions from the variable options (not already present)
204            let options = variable
205                .options
206                .iter()
207                .filter(|o| {
208                    !suggestions.iter().any(|(_, s, _)| match s {
209                        VariableSuggestion::Environment { value: Some(value), .. } => value == *o,
210                        VariableSuggestion::Existing(sv) => &sv.value == *o,
211                        VariableSuggestion::Completion(value) => value == *o,
212                        _ => false,
213                    })
214                })
215                .map(|o| (4, VariableSuggestion::Derived(o.to_owned()), 0.0))
216                .collect::<Vec<_>>();
217            suggestions.extend(options);
218        }
219
220        // Find and stream completions for the variable, which might be slow for network related completions
221        let completions = self
222            .storage
223            .get_completions_for(flat_root_cmd, variable.flat_names.clone())
224            .await?;
225        let completion_stream = if !completions.is_empty() {
226            let completion_points = self.tuning.variables.completion.points as f64;
227            let stream = resolve_completions(completions, context.clone()).await;
228            Some(stream.map(move |(score_boost, result)| (completion_points + score_boost, result)))
229        } else {
230            None
231        };
232
233        Ok((suggestions, completion_stream))
234    }
235
236    /// Inserts a new variable value
237    #[instrument(skip_all)]
238    pub async fn insert_variable_value(&self, value: VariableValue) -> Result<VariableValue> {
239        tracing::info!(
240            "Inserting a variable value for '{}' '{}': {}",
241            value.flat_root_cmd,
242            value.flat_variable,
243            value.value
244        );
245        self.storage.insert_variable_value(value).await
246    }
247
248    /// Updates an existing variable value
249    #[instrument(skip_all)]
250    pub async fn update_variable_value(&self, value: VariableValue) -> Result<VariableValue> {
251        tracing::info!(
252            "Updating variable value '{}': {}",
253            value.id.unwrap_or_default(),
254            value.value
255        );
256        self.storage.update_variable_value(value).await
257    }
258
259    /// Increases the usage of a variable value, returning the new usage count
260    #[instrument(skip_all)]
261    pub async fn increment_variable_value_usage(
262        &self,
263        value_id: i32,
264        context: BTreeMap<String, String>,
265    ) -> Result<i32> {
266        tracing::info!("Increasing usage for variable value '{value_id}'");
267        self.storage
268            .increment_variable_value_usage(value_id, get_working_dir(), &context)
269            .await
270    }
271
272    /// Deletes an existing variable value
273    #[instrument(skip_all)]
274    pub async fn delete_variable_value(&self, id: i32) -> Result<()> {
275        tracing::info!("Deleting variable value: {}", id);
276        self.storage.delete_variable_value(id).await
277    }
278}