intelli_shell/model/
template.rs

1use std::{
2    collections::{BTreeMap, HashSet},
3    convert::Infallible,
4    fmt::{Display, Formatter},
5    mem,
6    str::FromStr,
7};
8
9use heck::ToShoutySnakeCase;
10use itertools::Itertools;
11use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, percent_decode_str, utf8_percent_encode};
12
13use super::VariableValue;
14use crate::utils::{
15    COMMAND_VARIABLE_REGEX, COMMAND_VARIABLE_REGEX_ALT, SplitCaptures, SplitItem, flatten_str, flatten_variable_name,
16};
17
18/// A command containing variables
19#[derive(Clone)]
20#[cfg_attr(test, derive(Debug))]
21pub struct CommandTemplate {
22    /// The flattened root command (i.e., the first word)
23    pub flat_root_cmd: String,
24    /// The different parts of the command template, including variables or values
25    pub parts: Vec<TemplatePart>,
26}
27impl CommandTemplate {
28    /// Parses the given command as a [`CommandTemplate`]
29    pub fn parse(cmd: impl AsRef<str>, alt: bool) -> Self {
30        let cmd = cmd.as_ref();
31        let regex = if alt {
32            &COMMAND_VARIABLE_REGEX_ALT
33        } else {
34            &COMMAND_VARIABLE_REGEX
35        };
36        let splitter = SplitCaptures::new(regex, cmd);
37        let parts = splitter
38            .map(|e| match e {
39                SplitItem::Unmatched(t) => TemplatePart::Text(t.to_owned()),
40                SplitItem::Captured(c) => {
41                    TemplatePart::Variable(Variable::parse(c.get(1).or(c.get(2)).unwrap().as_str()))
42                }
43            })
44            .collect::<Vec<_>>();
45
46        CommandTemplate {
47            flat_root_cmd: flatten_str(cmd.split_whitespace().next().unwrap_or(cmd)),
48            parts,
49        }
50    }
51
52    /// Checks if the command has any variables without value
53    pub fn has_pending_variable(&self) -> bool {
54        self.parts.iter().any(|part| matches!(part, TemplatePart::Variable(_)))
55    }
56
57    /// Retrieves the previously selected values for the given flat variable name
58    pub fn previous_values_for(&self, flat_variable_name: &str) -> Option<Vec<String>> {
59        // Find all filled variables that match the flat name, collecting their unique values
60        let values = self
61            .parts
62            .iter()
63            .filter_map(|part| {
64                if let TemplatePart::VariableValue(v, value) = part
65                    && v.flat_name == flat_variable_name
66                {
67                    Some(value.clone())
68                } else {
69                    None
70                }
71            })
72            .unique()
73            .collect::<Vec<_>>();
74
75        if values.is_empty() { None } else { Some(values) }
76    }
77
78    /// Retrieves the context for a variable in the command
79    pub fn variable_context(&self) -> BTreeMap<String, String> {
80        self.parts
81            .iter()
82            .filter_map(|part| {
83                if let TemplatePart::VariableValue(v, value) = part
84                    && !v.secret
85                {
86                    Some((v.flat_name.clone(), value.clone()))
87                } else {
88                    None
89                }
90            })
91            .collect()
92    }
93
94    /// Counts the total number of variables in the template (both filled and unfilled)
95    pub fn count_variables(&self) -> usize {
96        self.parts
97            .iter()
98            .filter(|part| matches!(part, TemplatePart::Variable(_) | TemplatePart::VariableValue(_, _)))
99            .count()
100    }
101
102    /// Returns the variable at a specific index (0-based)
103    pub fn variable_at(&self, index: usize) -> Option<&Variable> {
104        self.parts
105            .iter()
106            .filter_map(|part| match part {
107                TemplatePart::Variable(v) | TemplatePart::VariableValue(v, _) => Some(v),
108                _ => None,
109            })
110            .nth(index)
111    }
112
113    /// Updates the value of the variable at the given index, returning the previous value if any (0-based)
114    pub fn set_variable_value(&mut self, index: usize, value: Option<String>) -> Option<String> {
115        // Find the part that corresponds to the variable at the given index
116        self.parts
117            .iter_mut()
118            .filter(|part| matches!(part, TemplatePart::Variable(_) | TemplatePart::VariableValue(_, _)))
119            .nth(index)
120            .and_then(|part| part.set_value(value))
121    }
122
123    /// Sets the values for the variables in the template, in order.
124    ///
125    /// This function will iterate through the variables and assign the values from the given slice in order starting
126    /// from the first one until no more values are available.
127    pub fn set_variable_values(&mut self, values: &[Option<String>]) {
128        // Create an iterator for the values to consume them as we find variables
129        let mut values_iter = values.iter();
130
131        // Iterate through all parts of the command template
132        for part in self.parts.iter_mut() {
133            // We only care about parts that are variables
134            if matches!(part, TemplatePart::Variable(_) | TemplatePart::VariableValue(_, _)) {
135                // If we run out of values, stop. Otherwise, update the part.
136                if let Some(value) = values_iter.next() {
137                    part.set_value(value.clone());
138                } else {
139                    break;
140                }
141            }
142        }
143    }
144
145    /// Creates a [VariableValue] for this command with the given flat variable name and value
146    pub fn new_variable_value_for(
147        &self,
148        flat_variable_name: impl Into<String>,
149        value: impl Into<String>,
150    ) -> VariableValue {
151        VariableValue::new(&self.flat_root_cmd, flat_variable_name, value)
152    }
153}
154impl Display for CommandTemplate {
155    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
156        for part in self.parts.iter() {
157            write!(f, "{part}")?;
158        }
159        Ok(())
160    }
161}
162
163/// Represents a part of a command, which can be either text, a variable, or a variable value
164#[derive(Clone)]
165#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
166pub enum TemplatePart {
167    Text(String),
168    Variable(Variable),
169    VariableValue(Variable, String),
170}
171impl TemplatePart {
172    /// Updates a `Variable` or `VariableValue` part based on the provided value, returning the previous value if any.
173    /// - If `Some(value)` is provided, it becomes a `VariableValue`.
174    /// - If `None` is provided, it becomes a `Variable`.
175    /// - `Text` parts are ignored.
176    pub fn set_value(&mut self, value: Option<String>) -> Option<String> {
177        // We only care about parts that can hold a variable
178        if !matches!(self, Self::Variable(_) | Self::VariableValue(_, _)) {
179            return None;
180        }
181
182        // Temporarily replace self with a default to take ownership of the variable `v`
183        let taken_part = mem::take(self);
184
185        // Get previous value and determine new part
186        let (new_part, previous_value) = match taken_part {
187            Self::Variable(v) => (
188                match value {
189                    Some(val) => Self::VariableValue(v, val),
190                    None => Self::Variable(v),
191                },
192                None,
193            ),
194            Self::VariableValue(v, old_val) => (
195                match value {
196                    Some(val) => Self::VariableValue(v, val),
197                    None => Self::Variable(v),
198                },
199                Some(old_val),
200            ),
201            other => (other, None),
202        };
203
204        *self = new_part;
205        previous_value
206    }
207}
208impl Default for TemplatePart {
209    fn default() -> Self {
210        Self::Text(String::new())
211    }
212}
213impl Display for TemplatePart {
214    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
215        match self {
216            TemplatePart::Text(t) => write!(f, "{t}"),
217            TemplatePart::Variable(v) => write!(f, "{{{{{}}}}}", v.display),
218            TemplatePart::VariableValue(_, value) => write!(f, "{value}"),
219        }
220    }
221}
222
223/// Represents a variable from a command template
224#[derive(Clone)]
225#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
226pub struct Variable {
227    /// The variable as it was displayed on the command (e.g., `"Opt1|Opt2:lower:kebab"`)
228    pub display: String,
229    /// Parsed variable values derived from `display` (e.g., `["Opt1","Opt2"]`)
230    pub options: Vec<String>,
231    /// Flattened variable names derived from `options` (e.g., `["opt1","opt2"]`)
232    pub flat_names: Vec<String>,
233    /// Flattened variable name derived from `flat_names` (e.g., `"opt1|opt2"`)
234    pub flat_name: String,
235    /// Parsed variable functions to apply (e.g., `["lower","kebab"]`)
236    pub functions: Vec<VariableFunction>,
237    /// Whether the variable is secret
238    pub secret: bool,
239}
240impl Variable {
241    /// Parses a variable text into its model
242    pub fn parse(text: impl Into<String>) -> Self {
243        let display: String = text.into();
244
245        // Determine if the variable is secret or not
246        let (content, secret) = match is_secret_variable(&display) {
247            Some(inner) => (inner, true),
248            None => (display.as_str(), false),
249        };
250
251        // Split the variable content in parts
252        let parts: Vec<&str> = content.split(':').collect();
253        let mut functions = Vec::new();
254        let mut boundary_index = parts.len();
255
256        // Iterate from right-to-left to find the boundary between options and functions
257        if parts.len() > 1 {
258            for (i, part) in parts.iter().enumerate().rev() {
259                if let Ok(func) = VariableFunction::from_str(part) {
260                    functions.push(func);
261                    boundary_index = i;
262                } else {
263                    break;
264                }
265            }
266        }
267
268        // The collected functions are in reverse order
269        functions.reverse();
270
271        // Join the option parts back together, then split them by the pipe character
272        let options_str = &parts[..boundary_index].join(":");
273        let (options, flat_names) = if options_str.is_empty() {
274            (vec![], vec![])
275        } else {
276            let mut options = Vec::new();
277            let mut flat_names = Vec::new();
278            let mut seen_options = HashSet::new();
279            let mut seen_flat_names = HashSet::new();
280
281            for option in options_str
282                .split('|')
283                .map(|o| o.trim())
284                .filter(|o| !o.is_empty())
285                .map(String::from)
286            {
287                if seen_options.insert(option.clone()) {
288                    let flat_name = flatten_variable_name(&option);
289                    if seen_flat_names.insert(flat_name.clone()) {
290                        flat_names.push(flat_name);
291                    }
292                    options.push(option);
293                }
294            }
295
296            (options, flat_names)
297        };
298
299        // Build back the flat name for this variable
300        let flat_name = flat_names.iter().join("|");
301
302        Self {
303            display,
304            options,
305            flat_names,
306            flat_name,
307            functions,
308            secret,
309        }
310    }
311
312    /// Retrieves the env var names where the value for this variable might reside (in order of preference)
313    pub fn env_var_names(&self, include_individual: bool) -> HashSet<String> {
314        let mut names = HashSet::new();
315        let env_var_name = self.display.to_shouty_snake_case();
316        if !env_var_name.trim().is_empty() && env_var_name.trim() != "PATH" {
317            names.insert(env_var_name.trim().to_owned());
318        }
319        let env_var_name_no_fn = self.flat_name.to_shouty_snake_case();
320        if !env_var_name_no_fn.trim().is_empty() && env_var_name_no_fn.trim() != "PATH" {
321            names.insert(env_var_name_no_fn.trim().to_owned());
322        }
323        if include_individual {
324            names.extend(
325                self.flat_names
326                    .iter()
327                    .map(|o| o.to_shouty_snake_case())
328                    .filter(|o| !o.trim().is_empty())
329                    .filter(|o| o.trim() != "PATH")
330                    .map(|o| o.trim().to_owned()),
331            );
332        }
333        names
334    }
335
336    /// Applies variable functions to the given text
337    pub fn apply_functions_to(&self, text: impl Into<String>) -> String {
338        let text = text.into();
339        let mut result = text;
340        for func in self.functions.iter() {
341            result = func.apply_to(result);
342        }
343        result
344    }
345
346    /// Iterates every function to check if a char has to be replaced
347    pub fn check_functions_char(&self, ch: char) -> Option<String> {
348        let mut out: Option<String> = None;
349        for func in self.functions.iter() {
350            if let Some(ref mut out) = out {
351                let mut new_out = String::from("");
352                for ch in out.chars() {
353                    if let Some(replacement) = func.check_char(ch) {
354                        new_out.push_str(&replacement);
355                    } else {
356                        new_out.push(ch);
357                    }
358                }
359                *out = new_out;
360            } else if let Some(replacement) = func.check_char(ch) {
361                out = Some(replacement);
362            }
363        }
364        out
365    }
366}
367impl FromStr for Variable {
368    type Err = Infallible;
369
370    fn from_str(s: &str) -> Result<Self, Self::Err> {
371        Ok(Self::parse(s))
372    }
373}
374
375/// Functions that can be applied to variable values
376#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::EnumString)]
377pub enum VariableFunction {
378    #[strum(serialize = "kebab")]
379    KebabCase,
380    #[strum(serialize = "snake")]
381    SnakeCase,
382    #[strum(serialize = "upper")]
383    UpperCase,
384    #[strum(serialize = "lower")]
385    LowerCase,
386    #[strum(serialize = "url")]
387    UrlEncode,
388}
389impl VariableFunction {
390    /// Applies this function to the given text
391    pub fn apply_to(&self, input: impl AsRef<str>) -> String {
392        let input = input.as_ref();
393        match self {
394            Self::KebabCase => replace_separators(input, '-'),
395            Self::SnakeCase => replace_separators(input, '_'),
396            Self::UpperCase => input.to_uppercase(),
397            Self::LowerCase => input.to_lowercase(),
398            Self::UrlEncode => idempotent_percent_encode(input, NON_ALPHANUMERIC),
399        }
400    }
401
402    /// Checks if this char would be transformed by this function
403    pub fn check_char(&self, ch: char) -> Option<String> {
404        match self {
405            Self::KebabCase | Self::SnakeCase => {
406                let separator = if *self == Self::KebabCase { '-' } else { '_' };
407                if ch != separator && is_separator(ch) {
408                    Some(separator.to_string())
409                } else {
410                    None
411                }
412            }
413            Self::UpperCase => {
414                if ch.is_lowercase() {
415                    Some(ch.to_uppercase().to_string())
416                } else {
417                    None
418                }
419            }
420            Self::LowerCase => {
421                if ch.is_uppercase() {
422                    Some(ch.to_lowercase().to_string())
423                } else {
424                    None
425                }
426            }
427            Self::UrlEncode => {
428                if ch.is_ascii_alphanumeric() {
429                    None
430                } else {
431                    Some(idempotent_percent_encode(&ch.to_string(), NON_ALPHANUMERIC))
432                }
433            }
434        }
435    }
436}
437
438/// Checks if a given variable is a secret (must not be stored), returning the inner variable name if it is
439fn is_secret_variable(variable_name: &str) -> Option<&str> {
440    if (variable_name.starts_with('*') && variable_name.ends_with('*') && variable_name.len() > 1)
441        || (variable_name.starts_with('{') && variable_name.ends_with('}'))
442    {
443        Some(&variable_name[1..variable_name.len() - 1])
444    } else {
445        None
446    }
447}
448
449/// Checks if a character is a separator
450fn is_separator(c: char) -> bool {
451    c == '-' || c == '_' || c.is_whitespace()
452}
453
454// This function replaces any sequence of separators with a single one
455fn replace_separators(s: &str, separator: char) -> String {
456    let mut result = String::with_capacity(s.len());
457
458    // Split the string using the custom separator logic and filter out empty results
459    let mut words = s.split(is_separator).filter(|word| !word.is_empty());
460
461    // Join the first word without a separator
462    if let Some(first_word) = words.next() {
463        result.push_str(first_word);
464    }
465    // Append the separator and the rest of the words
466    for word in words {
467        result.push(separator);
468        result.push_str(word);
469    }
470
471    result
472}
473
474/// Idempotent percent-encodes a string.
475///
476/// This function first checks if the input is already a correctly percent-encoded string
477/// according to the provided `encode_set`.
478/// - If it is, the input is returned
479/// - If it is not (i.e., it's unencoded, partially encoded, or incorrectly encoded), the function encodes the entire
480///   input string and returns a new `String`
481pub fn idempotent_percent_encode(input: &str, encode_set: &'static AsciiSet) -> String {
482    // Attempt to decode the input
483    if let Ok(decoded) = percent_decode_str(input).decode_utf8() {
484        // If successful, re-encode the decoded string using the same character set
485        let re_encoded = utf8_percent_encode(&decoded, encode_set).to_string();
486
487        // If the re-encoded string matches the original input, it means the input was already perfectly encoded
488        if re_encoded == input {
489            return re_encoded;
490        }
491    }
492
493    // In all other cases (decoding failed, or the re-encoded string is different), encode it fully
494    utf8_percent_encode(input, encode_set).to_string().to_string()
495}
496
497#[cfg(test)]
498mod tests {
499    use pretty_assertions::assert_eq;
500
501    use super::*;
502    #[test]
503    fn test_parse_command_with_variables() {
504        let cmd = CommandTemplate::parse("git commit -m {{{message}}} --author {{author:kebab}}", false);
505        assert_eq!(cmd.flat_root_cmd, "git");
506        assert_eq!(cmd.parts.len(), 4);
507        assert_eq!(cmd.parts[0], TemplatePart::Text("git commit -m ".into()));
508        assert!(matches!(cmd.parts[1], TemplatePart::Variable(_)));
509        assert_eq!(cmd.parts[2], TemplatePart::Text(" --author ".into()));
510        assert!(matches!(cmd.parts[3], TemplatePart::Variable(_)));
511    }
512
513    #[test]
514    fn test_parse_command_no_variables() {
515        let cmd = CommandTemplate::parse("echo 'hello world'", false);
516        assert_eq!(cmd.flat_root_cmd, "echo");
517        assert_eq!(cmd.parts.len(), 1);
518        assert_eq!(cmd.parts[0], TemplatePart::Text("echo 'hello world'".into()));
519    }
520
521    #[test]
522    fn test_has_pending_variable() {
523        let mut cmd = CommandTemplate::parse("cmd {{var1}} {{var2}}", false);
524        assert!(cmd.has_pending_variable());
525        cmd.set_variable_value(0, Some("value1".to_string()));
526        assert!(cmd.has_pending_variable());
527        cmd.set_variable_value(1, Some("value2".to_string()));
528        assert!(!cmd.has_pending_variable());
529    }
530
531    #[test]
532    fn test_previous_values_for() {
533        let mut cmd = CommandTemplate::parse("cmd {{var1}} {{var1}} {{var2}}", false);
534
535        // 1. No values set yet
536        assert_eq!(cmd.previous_values_for("var1"), None);
537        assert_eq!(cmd.previous_values_for("var2"), None);
538
539        // 2. Set one value for var1
540        cmd.set_variable_value(0, Some("val1".into()));
541        assert_eq!(cmd.previous_values_for("var1"), Some(vec!["val1".to_string()]));
542        assert_eq!(cmd.previous_values_for("var2"), None);
543
544        // 3. Set another value for var1
545        cmd.set_variable_value(1, Some("val1".into()));
546        assert_eq!(cmd.previous_values_for("var1"), Some(vec!["val1".to_string()]));
547
548        // 4. Set a different value for var1
549        cmd.set_variable_value(1, Some("val1_other".into()));
550        let mut values = cmd.previous_values_for("var1").unwrap();
551        values.sort();
552        assert_eq!(values, vec!["val1".to_string(), "val1_other".to_string()]);
553
554        // 5. Set value for var2
555        cmd.set_variable_value(2, Some("val2".into()));
556        assert_eq!(cmd.previous_values_for("var2"), Some(vec!["val2".to_string()]));
557    }
558
559    #[test]
560    fn test_variable_context() {
561        let mut cmd = CommandTemplate::parse("cmd {{var1}} {{{secret_var}}} {{var2}}", false);
562
563        // Set value for the last variable
564        cmd.set_variable_value(2, Some("value2".to_string()));
565        let context_before_secret: Vec<_> = cmd.variable_context().into_iter().collect();
566        assert_eq!(context_before_secret, vec![("var2".to_string(), "value2".to_string())]);
567
568        // Set value for the secret variable
569        cmd.set_variable_value(1, Some("secret_value".to_string()));
570        let context_after_secret: Vec<_> = cmd.variable_context().into_iter().collect();
571        // The secret variable value should not be in the context
572        assert_eq!(context_after_secret, context_before_secret);
573    }
574
575    #[test]
576    fn test_variable_context_is_empty() {
577        let cmd = CommandTemplate::parse("cmd {{var1}}", false);
578        let context: Vec<_> = cmd.variable_context().into_iter().collect();
579        assert!(context.is_empty());
580    }
581
582    #[test]
583    fn test_count_variables() {
584        let mut cmd = CommandTemplate::parse("cmd {{var1}} middle {{var2}}", false);
585        assert_eq!(cmd.count_variables(), 2);
586
587        cmd.set_variable_value(0, Some("value1".to_string()));
588        assert_eq!(cmd.count_variables(), 2);
589
590        // Test with no variables
591        let cmd_no_vars = CommandTemplate::parse("cmd no-vars", false);
592        assert_eq!(cmd_no_vars.count_variables(), 0);
593    }
594
595    #[test]
596    fn test_variable_at() {
597        let cmd = CommandTemplate::parse("cmd {{var1}} middle {{var2}}", false);
598        let var1 = Variable::parse("var1");
599        let var2 = Variable::parse("var2");
600
601        // Test valid indices
602        assert_eq!(cmd.variable_at(0), Some(&var1));
603        assert_eq!(cmd.variable_at(1), Some(&var2));
604
605        // Test out-of-bounds index
606        assert_eq!(cmd.variable_at(2), None);
607
608        // Test with no variables
609        let cmd_no_vars = CommandTemplate::parse("cmd no-vars", false);
610        assert_eq!(cmd_no_vars.variable_at(0), None);
611    }
612
613    #[test]
614    fn test_set_variable_value() {
615        let mut cmd = CommandTemplate::parse("cmd {{var1}} {{var2}}", false);
616
617        // 1. Set value for the first time
618        let prev = cmd.set_variable_value(0, Some("val1".into()));
619        assert_eq!(prev, None);
620        let var1 = Variable::parse("var1");
621        assert_eq!(cmd.parts[1], TemplatePart::VariableValue(var1.clone(), "val1".into()));
622
623        // 2. Update value
624        let prev = cmd.set_variable_value(0, Some("new_val1".into()));
625        assert_eq!(prev, Some("val1".into()));
626        assert_eq!(
627            cmd.parts[1],
628            TemplatePart::VariableValue(var1.clone(), "new_val1".into())
629        );
630
631        // 3. Unset value
632        let prev = cmd.set_variable_value(0, None);
633        assert_eq!(prev, Some("new_val1".into()));
634        assert_eq!(cmd.parts[1], TemplatePart::Variable(var1.clone()));
635
636        // 4. Set value on another index
637        let prev = cmd.set_variable_value(1, Some("val2".into()));
638        assert_eq!(prev, None);
639        let var2 = Variable::parse("var2");
640        assert_eq!(cmd.parts[3], TemplatePart::VariableValue(var2.clone(), "val2".into()));
641
642        // 5. Test out of bounds
643        let prev = cmd.set_variable_value(2, Some("val3".into()));
644        assert_eq!(prev, None);
645    }
646
647    #[test]
648    fn test_set_variable_values_full_update() {
649        let mut cmd = CommandTemplate::parse("cmd {{var1}} {{var2}}", false);
650        let values = vec![Some("value1".to_string()), Some("value2".to_string())];
651        cmd.set_variable_values(&values);
652
653        let var1 = Variable::parse("var1");
654        let var2 = Variable::parse("var2");
655        assert_eq!(cmd.parts[1], TemplatePart::VariableValue(var1, "value1".into()));
656        assert_eq!(cmd.parts[3], TemplatePart::VariableValue(var2, "value2".into()));
657        assert!(!cmd.has_pending_variable());
658    }
659
660    #[test]
661    fn test_set_variable_values_partial_update() {
662        let mut cmd = CommandTemplate::parse("cmd {{var1}} {{var2}} {{var3}}", false);
663        let values = vec![Some("value1".to_string()), Some("value2".to_string())];
664        cmd.set_variable_values(&values);
665
666        let var1 = Variable::parse("var1");
667        let var2 = Variable::parse("var2");
668        let var3 = Variable::parse("var3");
669        assert_eq!(cmd.parts[1], TemplatePart::VariableValue(var1, "value1".into()));
670        assert_eq!(cmd.parts[3], TemplatePart::VariableValue(var2, "value2".into()));
671        assert_eq!(cmd.parts[5], TemplatePart::Variable(var3));
672        assert!(cmd.has_pending_variable());
673    }
674
675    #[test]
676    fn test_set_variable_values_with_none_to_unset() {
677        let mut cmd = CommandTemplate::parse("cmd {{var1}} {{var2}}", false);
678        // Initially set both values
679        cmd.set_variable_values(&[Some("val1".into()), Some("val2".into())]);
680        assert!(!cmd.has_pending_variable());
681
682        // Now, set_variable_values with a new set of values where the first is None (unset) and the second is updated
683        let values = vec![None, Some("new_val2".to_string())];
684        cmd.set_variable_values(&values);
685
686        let var1 = Variable::parse("var1");
687        let var2 = Variable::parse("var2");
688        assert_eq!(cmd.parts[1], TemplatePart::Variable(var1));
689        assert_eq!(cmd.parts[3], TemplatePart::VariableValue(var2, "new_val2".into()));
690        assert!(cmd.has_pending_variable());
691    }
692
693    #[test]
694    fn test_set_variable_values_empty_slice() {
695        let mut cmd = CommandTemplate::parse("cmd {{var1}} {{var2}}", false);
696        let original_parts = cmd.parts.clone();
697        cmd.set_variable_values(&[]);
698
699        assert_eq!(cmd.parts, original_parts);
700    }
701
702    #[test]
703    fn test_set_variable_values_more_values_than_variables() {
704        let mut cmd = CommandTemplate::parse("cmd {{var1}}", false);
705        let values = vec![Some("value1".to_string()), Some("ignored".to_string())];
706        cmd.set_variable_values(&values);
707
708        let var1 = Variable::parse("var1");
709        assert_eq!(cmd.parts[1], TemplatePart::VariableValue(var1, "value1".into()));
710        assert!(!cmd.has_pending_variable());
711    }
712
713    #[test]
714    fn test_template_part_set_value() {
715        let var = Variable::parse("v");
716
717        // Test setting a value on a Variable
718        let mut part1 = TemplatePart::Variable(var.clone());
719        let prev1 = part1.set_value(Some("value".into()));
720        assert_eq!(prev1, None);
721        assert_eq!(part1, TemplatePart::VariableValue(var.clone(), "value".into()));
722
723        // Test updating a value on a VariableValue
724        let mut part2 = TemplatePart::VariableValue(var.clone(), "old".into());
725        let prev2 = part2.set_value(Some("new".into()));
726        assert_eq!(prev2, Some("old".into()));
727        assert_eq!(part2, TemplatePart::VariableValue(var.clone(), "new".into()));
728
729        // Test unsetting a value on a VariableValue
730        let mut part3 = TemplatePart::VariableValue(var.clone(), "value".into());
731        let prev3 = part3.set_value(None);
732        assert_eq!(prev3, Some("value".into()));
733        assert_eq!(part3, TemplatePart::Variable(var.clone()));
734
735        // Test setting None on a Variable (should not change)
736        let mut part4 = TemplatePart::Variable(var.clone());
737        let prev4 = part4.set_value(None);
738        assert_eq!(prev4, None);
739        assert_eq!(part4, TemplatePart::Variable(var.clone()));
740
741        // Test on a Text part (should not change)
742        let mut part5 = TemplatePart::Text("text".into());
743        let prev5 = part5.set_value(Some("value".into()));
744        assert_eq!(prev5, None);
745        assert_eq!(part5, TemplatePart::Text("text".into()));
746    }
747
748    #[test]
749    fn test_parse_simple_variable() {
750        let variable = Variable::parse("my_variable");
751        assert_eq!(
752            variable,
753            Variable {
754                display: "my_variable".into(),
755                options: vec!["my_variable".into()],
756                flat_names: vec!["my_variable".into()],
757                flat_name: "my_variable".into(),
758                functions: vec![],
759                secret: false,
760            }
761        );
762    }
763
764    #[test]
765    fn test_parse_secret_variable() {
766        let variable = Variable::parse("{my_secret}");
767        assert_eq!(
768            variable,
769            Variable {
770                display: "{my_secret}".into(),
771                options: vec!["my_secret".into()],
772                flat_names: vec!["my_secret".into()],
773                flat_name: "my_secret".into(),
774                functions: vec![],
775                secret: true,
776            }
777        );
778    }
779
780    #[test]
781    fn test_parse_variable_with_multiple_options() {
782        let variable = Variable::parse("Option 1 | option 1 | Option 2 | Option 2 | Option 3");
783        assert_eq!(
784            variable,
785            Variable {
786                display: "Option 1 | option 1 | Option 2 | Option 2 | Option 3".into(),
787                options: vec![
788                    "Option 1".into(),
789                    "option 1".into(),
790                    "Option 2".into(),
791                    "Option 3".into()
792                ],
793                flat_names: vec!["option 1".into(), "option 2".into(), "option 3".into()],
794                flat_name: "option 1|option 2|option 3".into(),
795                functions: vec![],
796                secret: false,
797            }
798        );
799    }
800
801    #[test]
802    fn test_parse_variable_with_single_function() {
803        let variable = Variable::parse("my_variable:kebab");
804        assert_eq!(
805            variable,
806            Variable {
807                display: "my_variable:kebab".into(),
808                options: vec!["my_variable".into()],
809                flat_names: vec!["my_variable".into()],
810                flat_name: "my_variable".into(),
811                functions: vec![VariableFunction::KebabCase],
812                secret: false,
813            }
814        );
815    }
816
817    #[test]
818    fn test_parse_variable_with_multiple_functions() {
819        let variable = Variable::parse("my_variable:snake:upper");
820        assert_eq!(
821            variable,
822            Variable {
823                display: "my_variable:snake:upper".into(),
824                options: vec!["my_variable".into()],
825                flat_names: vec!["my_variable".into()],
826                flat_name: "my_variable".into(),
827                functions: vec![VariableFunction::SnakeCase, VariableFunction::UpperCase],
828                secret: false,
829            }
830        );
831    }
832
833    #[test]
834    fn test_parse_variable_with_options_and_functions() {
835        let variable = Variable::parse("opt1|opt2:lower:kebab");
836        assert_eq!(
837            variable,
838            Variable {
839                display: "opt1|opt2:lower:kebab".into(),
840                options: vec!["opt1".into(), "opt2".into()],
841                flat_names: vec!["opt1".into(), "opt2".into()],
842                flat_name: "opt1|opt2".into(),
843                functions: vec![VariableFunction::LowerCase, VariableFunction::KebabCase],
844                secret: false,
845            }
846        );
847    }
848
849    #[test]
850    fn test_parse_variable_with_colon_in_options() {
851        let variable = Variable::parse("key:value:kebab");
852        assert_eq!(
853            variable,
854            Variable {
855                display: "key:value:kebab".into(),
856                options: vec!["key:value".into()],
857                flat_names: vec!["key:value".into()],
858                flat_name: "key:value".into(),
859                functions: vec![VariableFunction::KebabCase],
860                secret: false,
861            }
862        );
863    }
864
865    #[test]
866    fn test_parse_variable_with_only_functions() {
867        let variable = Variable::parse(":snake");
868        assert_eq!(
869            variable,
870            Variable {
871                display: ":snake".into(),
872                options: vec![],
873                flat_names: vec![],
874                flat_name: "".into(),
875                functions: vec![VariableFunction::SnakeCase],
876                secret: false,
877            }
878        );
879    }
880
881    #[test]
882    fn test_parse_variable_that_is_a_function_name() {
883        let variable = Variable::parse("kebab");
884        assert_eq!(
885            variable,
886            Variable {
887                display: "kebab".into(),
888                options: vec!["kebab".into()],
889                flat_names: vec!["kebab".into()],
890                flat_name: "kebab".into(),
891                functions: vec![],
892                secret: false,
893            }
894        );
895    }
896
897    #[test]
898    fn test_variable_env_var_names() {
899        // Simple variable
900        let var1 = Variable::parse("my-variable");
901        assert_eq!(var1.env_var_names(true), HashSet::from(["MY_VARIABLE".into()]));
902
903        // Variable with options
904        let var2 = Variable::parse("option1|option2");
905        assert_eq!(
906            var2.env_var_names(true),
907            HashSet::from(["OPTION1_OPTION2".into(), "OPTION1".into(), "OPTION2".into()])
908        );
909        assert_eq!(var2.env_var_names(false), HashSet::from(["OPTION1_OPTION2".into()]));
910
911        // Variable with functions
912        let var3 = Variable::parse("my-variable:kebab:upper");
913        assert_eq!(
914            var3.env_var_names(true),
915            HashSet::from(["MY_VARIABLE_KEBAB_UPPER".into(), "MY_VARIABLE".into()])
916        );
917
918        // Secret variable with asterisks
919        let var4 = Variable::parse("*my-secret*");
920        assert_eq!(var4.env_var_names(true), HashSet::from(["MY_SECRET".into()]));
921
922        // Secret variable with braces
923        let var5 = Variable::parse("{my-secret}");
924        assert_eq!(var5.env_var_names(true), HashSet::from(["MY_SECRET".into()]));
925    }
926
927    #[test]
928    fn test_variable_apply_functions_to() {
929        // No functions, should not change the input
930        let var_none = Variable::parse("text");
931        assert_eq!(var_none.apply_functions_to("Hello World"), "Hello World");
932
933        // Single function
934        let var_upper = Variable::parse("text:upper");
935        assert_eq!(var_upper.apply_functions_to("Hello World"), "HELLO WORLD");
936
937        // Chained functions: kebab then upper
938        let var_kebab_upper = Variable::parse("text:kebab:upper");
939        assert_eq!(var_kebab_upper.apply_functions_to("Hello World"), "HELLO-WORLD");
940
941        // Chained functions: snake then lower
942        let var_snake_lower = Variable::parse("text:snake:lower");
943        assert_eq!(var_snake_lower.apply_functions_to("Hello World"), "hello_world");
944    }
945
946    #[test]
947    fn test_variable_check_functions_char() {
948        // No functions, should always be None
949        let var_none = Variable::parse("text");
950        assert_eq!(var_none.check_functions_char('a'), None);
951        assert_eq!(var_none.check_functions_char(' '), None);
952
953        // Single function with a change
954        let var_upper = Variable::parse("text:upper");
955        assert_eq!(var_upper.check_functions_char('a'), Some("A".to_string()));
956
957        // Single function with no change
958        let var_lower = Variable::parse("text:lower");
959        assert_eq!(var_lower.check_functions_char('a'), None);
960
961        // Chained functions
962        let var_upper_kebab = Variable::parse("text:upper:kebab");
963        assert_eq!(var_upper_kebab.check_functions_char('a'), Some("A".to_string()));
964        assert_eq!(var_upper_kebab.check_functions_char(' '), Some("-".to_string()));
965        assert_eq!(var_upper_kebab.check_functions_char('-'), None);
966    }
967
968    #[test]
969    fn test_variable_function_apply_to() {
970        // KebabCase
971        assert_eq!(VariableFunction::KebabCase.apply_to("some text"), "some-text");
972        assert_eq!(VariableFunction::KebabCase.apply_to("Some Text"), "Some-Text");
973        assert_eq!(VariableFunction::KebabCase.apply_to("some_text"), "some-text");
974        assert_eq!(VariableFunction::KebabCase.apply_to("-"), "");
975        assert_eq!(VariableFunction::KebabCase.apply_to("_"), "");
976
977        // SnakeCase
978        assert_eq!(VariableFunction::SnakeCase.apply_to("some text"), "some_text");
979        assert_eq!(VariableFunction::SnakeCase.apply_to("Some Text"), "Some_Text");
980        assert_eq!(VariableFunction::SnakeCase.apply_to("some-text"), "some_text");
981        assert_eq!(VariableFunction::SnakeCase.apply_to("-"), "");
982        assert_eq!(VariableFunction::SnakeCase.apply_to("_"), "");
983
984        // UpperCase
985        assert_eq!(VariableFunction::UpperCase.apply_to("some text"), "SOME TEXT");
986        assert_eq!(VariableFunction::UpperCase.apply_to("SomeText"), "SOMETEXT");
987
988        // LowerCase
989        assert_eq!(VariableFunction::LowerCase.apply_to("SOME TEXT"), "some text");
990        assert_eq!(VariableFunction::LowerCase.apply_to("SomeText"), "sometext");
991
992        // Urlencode
993        assert_eq!(VariableFunction::UrlEncode.apply_to("some text"), "some%20text");
994        assert_eq!(VariableFunction::UrlEncode.apply_to("Some Text"), "Some%20Text");
995        assert_eq!(VariableFunction::UrlEncode.apply_to("some-text"), "some%2Dtext");
996        assert_eq!(VariableFunction::UrlEncode.apply_to("some_text"), "some%5Ftext");
997        assert_eq!(
998            VariableFunction::UrlEncode.apply_to("!@#$%^&*()"),
999            "%21%40%23%24%25%5E%26%2A%28%29"
1000        );
1001        assert_eq!(VariableFunction::UrlEncode.apply_to("some%20text"), "some%20text");
1002    }
1003
1004    #[test]
1005    fn test_variable_function_check_char() {
1006        // KebabCase
1007        assert_eq!(VariableFunction::KebabCase.check_char(' '), Some("-".to_string()));
1008        assert_eq!(VariableFunction::KebabCase.check_char('_'), Some("-".to_string()));
1009        assert_eq!(VariableFunction::KebabCase.check_char('-'), None);
1010        assert_eq!(VariableFunction::KebabCase.check_char('A'), None);
1011
1012        // SnakeCase
1013        assert_eq!(VariableFunction::SnakeCase.check_char(' '), Some("_".to_string()));
1014        assert_eq!(VariableFunction::SnakeCase.check_char('-'), Some("_".to_string()));
1015        assert_eq!(VariableFunction::SnakeCase.check_char('_'), None);
1016        assert_eq!(VariableFunction::SnakeCase.check_char('A'), None);
1017
1018        // UpperCase
1019        assert_eq!(VariableFunction::UpperCase.check_char('a'), Some("A".to_string()));
1020        assert_eq!(VariableFunction::UpperCase.check_char('A'), None);
1021        assert_eq!(VariableFunction::UpperCase.check_char(' '), None);
1022
1023        // LowerCase
1024        assert_eq!(VariableFunction::LowerCase.check_char('A'), Some("a".to_string()));
1025        assert_eq!(VariableFunction::LowerCase.check_char('a'), None);
1026        assert_eq!(VariableFunction::LowerCase.check_char(' '), None);
1027
1028        // UrlEncode
1029        assert_eq!(VariableFunction::UrlEncode.check_char(' '), Some("%20".to_string()));
1030        assert_eq!(VariableFunction::UrlEncode.check_char('!'), Some("%21".to_string()));
1031        assert_eq!(VariableFunction::UrlEncode.check_char('A'), None);
1032        assert_eq!(VariableFunction::UrlEncode.check_char('1'), None);
1033        assert_eq!(VariableFunction::UrlEncode.check_char('-'), Some("%2D".to_string()));
1034        assert_eq!(VariableFunction::UrlEncode.check_char('_'), Some("%5F".to_string()));
1035    }
1036
1037    #[test]
1038    fn test_is_secret_variable() {
1039        // Test with asterisks
1040        assert_eq!(is_secret_variable("*secret*"), Some("secret"));
1041        assert_eq!(is_secret_variable("* another secret *"), Some(" another secret "));
1042        assert_eq!(is_secret_variable("**"), Some(""));
1043
1044        // Test with braces
1045        assert_eq!(is_secret_variable("{secret}"), Some("secret"));
1046        assert_eq!(is_secret_variable("{ another secret }"), Some(" another secret "));
1047        assert_eq!(is_secret_variable("{}"), Some(""));
1048
1049        // Test non-secret variables
1050        assert_eq!(is_secret_variable("not-secret"), None);
1051        assert_eq!(is_secret_variable("*not-secret"), None);
1052        assert_eq!(is_secret_variable("not-secret*"), None);
1053        assert_eq!(is_secret_variable("{not-secret"), None);
1054        assert_eq!(is_secret_variable("not-secret}"), None);
1055        assert_eq!(is_secret_variable(""), None);
1056        assert_eq!(is_secret_variable("*"), None);
1057        assert_eq!(is_secret_variable("{"), None);
1058        assert_eq!(is_secret_variable("}*"), None);
1059        assert_eq!(is_secret_variable("*{"), None);
1060    }
1061}