intelli_shell/service/
variable.rs

1use std::{
2    collections::{BTreeMap, HashMap},
3    env,
4};
5
6use color_eyre::Result;
7use heck::ToShoutySnakeCase;
8use tracing::instrument;
9
10use super::IntelliShellService;
11use crate::{
12    errors::{InsertError, UpdateError},
13    model::{CommandPart, DynamicCommand, Variable, VariableSuggestion, VariableValue},
14    utils::{format_env_var, get_working_dir},
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 dynamic one
36        let dynamic = DynamicCommand::parse(command);
37        // For each one of the parts
38        for part in dynamic.parts {
39            match part {
40                // Just parsed commands doesn't contain variable values
41                CommandPart::VariableValue(_, _) => unreachable!(),
42                // Text parts are not variables so we can push them directly to the output
43                CommandPart::Text(t) => output.push_str(&t),
44                // Variables must be replaced
45                CommandPart::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.name),
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.name);
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    pub async fn search_variable_suggestions(
89        &self,
90        root_cmd: &str,
91        variable: &Variable,
92        context: impl IntoIterator<Item = (String, String)>,
93    ) -> Result<Vec<VariableSuggestion>> {
94        tracing::info!("Searching for variable suggestions: [{root_cmd}] {}", variable.name);
95
96        let mut suggestions = Vec::new();
97
98        if variable.secret {
99            // If the variable is a secret, suggest a new secret value
100            suggestions.push(VariableSuggestion::Secret);
101            // And check if there's any env var that matches the variable to include it as a suggestion
102            for env_var_name in variable.env_var_names(true) {
103                if env::var(&env_var_name).is_ok() {
104                    suggestions.push(VariableSuggestion::Environment {
105                        env_var_name,
106                        value: None,
107                    });
108                }
109            }
110        } else {
111            // Otherwise it's a regular variable, suggest a new value
112            suggestions.push(VariableSuggestion::New);
113
114            // Find sorted values for the variable
115            let context = BTreeMap::from_iter(context);
116            let mut existing_values = self
117                .storage
118                .find_variable_values(
119                    root_cmd,
120                    &variable.name,
121                    get_working_dir(),
122                    &context,
123                    &self.tuning.variables,
124                )
125                .await?;
126
127            // If there's a suggestion for a value previously selected for the same variable
128            let previous_value = context.get(&variable.name).cloned();
129            if let Some(previous_value) = previous_value
130                && let Some(index) = existing_values.iter().position(|s| s.value == previous_value)
131            {
132                // Include it first, ignoring its relevance ordering
133                suggestions.push(VariableSuggestion::Existing(existing_values.remove(index)));
134            }
135
136            // Check if there's any env var that matches the variable to include it as a suggestion
137            for env_var_name in variable.env_var_names(true) {
138                if let Ok(value) = env::var(&env_var_name)
139                    && !value.trim().is_empty()
140                {
141                    let value = variable.apply_functions_to(value);
142                    // Check if there's already an existing value for that suggestion
143                    if let Some(index) = existing_values.iter().position(|s| s.value == value) {
144                        // Include it now, ignoring its ordering
145                        suggestions.push(VariableSuggestion::Existing(existing_values.remove(index)));
146                    } else {
147                        // Otherwise, include the environment suggestion
148                        suggestions.push(VariableSuggestion::Environment {
149                            env_var_name,
150                            value: Some(value),
151                        });
152                    };
153                }
154            }
155            // Include remaining existing values
156            suggestions.extend(existing_values.into_iter().map(VariableSuggestion::Existing));
157            // And suggestions from the variable options (not already present)
158            let options = variable
159                .options
160                .iter()
161                .filter(|o| {
162                    !suggestions.iter().any(|s| match s {
163                        VariableSuggestion::Environment { value: Some(value), .. } => value == *o,
164                        VariableSuggestion::Existing(sv) => &sv.value == *o,
165                        _ => false,
166                    })
167                })
168                .map(|o| VariableSuggestion::Derived(o.to_owned()))
169                .collect::<Vec<_>>();
170            suggestions.extend(options);
171        }
172
173        Ok(suggestions)
174    }
175
176    /// Inserts a new variable value
177    #[instrument(skip_all)]
178    pub async fn insert_variable_value(&self, value: VariableValue) -> Result<VariableValue, InsertError> {
179        tracing::info!(
180            "Inserting a variable value for '{}' '{}': {}",
181            value.flat_root_cmd,
182            value.flat_variable,
183            value.value
184        );
185        self.storage.insert_variable_value(value).await
186    }
187
188    /// Updates an existing variable value
189    #[instrument(skip_all)]
190    pub async fn update_variable_value(&self, value: VariableValue) -> Result<VariableValue, UpdateError> {
191        tracing::info!(
192            "Updating variable value '{}': {}",
193            value.id.unwrap_or_default(),
194            value.value
195        );
196        self.storage.update_variable_value(value).await
197    }
198
199    /// Increases the usage of a variable value, returning the new usage count
200    #[instrument(skip_all)]
201    pub async fn increment_variable_value_usage(
202        &self,
203        value_id: i32,
204        context: impl IntoIterator<Item = (String, String)>,
205    ) -> Result<i32, UpdateError> {
206        tracing::info!("Increasing usage for variable value '{value_id}'");
207        let context = BTreeMap::from_iter(context);
208        self.storage
209            .increment_variable_value_usage(value_id, get_working_dir(), &context)
210            .await
211    }
212
213    /// Deletes an existing variable value
214    #[instrument(skip_all)]
215    pub async fn delete_variable_value(&self, id: i32) -> Result<()> {
216        tracing::info!("Deleting variable value: {}", id);
217        self.storage.delete_variable_value(id).await
218    }
219}