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 extract_root_cmd,
17};
18
19#[derive(Clone)]
21#[cfg_attr(test, derive(Debug))]
22pub struct CommandTemplate {
23 pub flat_root_cmd: String,
25 pub parts: Vec<TemplatePart>,
27}
28impl CommandTemplate {
29 pub fn parse(cmd: impl AsRef<str>, alt: bool) -> Self {
31 let cmd = cmd.as_ref();
32 let regex = if alt {
33 &COMMAND_VARIABLE_REGEX_ALT
34 } else {
35 &COMMAND_VARIABLE_REGEX
36 };
37 let splitter = SplitCaptures::new(regex, cmd);
38 let parts = splitter
39 .map(|e| match e {
40 SplitItem::Unmatched(t) => TemplatePart::Text(t.to_owned()),
41 SplitItem::Captured(c) => {
42 TemplatePart::Variable(Variable::parse(c.get(1).or(c.get(2)).unwrap().as_str()))
43 }
44 })
45 .collect::<Vec<_>>();
46
47 CommandTemplate {
48 flat_root_cmd: flatten_str(extract_root_cmd(cmd).as_deref().unwrap_or(cmd)),
49 parts,
50 }
51 }
52
53 pub fn has_pending_variable(&self) -> bool {
55 self.parts.iter().any(|part| matches!(part, TemplatePart::Variable(_)))
56 }
57
58 pub fn previous_values_for(&self, flat_variable_name: &str) -> Option<Vec<String>> {
60 let values = self
62 .parts
63 .iter()
64 .filter_map(|part| {
65 if let TemplatePart::VariableValue(v, value) = part
66 && v.flat_name == flat_variable_name
67 {
68 Some(value.clone())
69 } else {
70 None
71 }
72 })
73 .unique()
74 .collect::<Vec<_>>();
75
76 if values.is_empty() { None } else { Some(values) }
77 }
78
79 pub fn variable_context(&self) -> BTreeMap<String, String> {
81 self.parts
82 .iter()
83 .filter_map(|part| {
84 if let TemplatePart::VariableValue(v, value) = part
85 && !v.secret
86 {
87 Some((v.flat_name.clone(), value.clone()))
88 } else {
89 None
90 }
91 })
92 .collect()
93 }
94
95 pub fn count_variables(&self) -> usize {
97 self.parts
98 .iter()
99 .filter(|part| matches!(part, TemplatePart::Variable(_) | TemplatePart::VariableValue(_, _)))
100 .count()
101 }
102
103 pub fn variable_at(&self, index: usize) -> Option<&Variable> {
105 self.parts
106 .iter()
107 .filter_map(|part| match part {
108 TemplatePart::Variable(v) | TemplatePart::VariableValue(v, _) => Some(v),
109 _ => None,
110 })
111 .nth(index)
112 }
113
114 pub fn set_variable_value(&mut self, index: usize, value: Option<String>) -> Option<String> {
116 self.parts
118 .iter_mut()
119 .filter(|part| matches!(part, TemplatePart::Variable(_) | TemplatePart::VariableValue(_, _)))
120 .nth(index)
121 .and_then(|part| part.set_value(value))
122 }
123
124 pub fn set_variable_values(&mut self, values: &[Option<String>]) {
129 let mut values_iter = values.iter();
131
132 for part in self.parts.iter_mut() {
134 if matches!(part, TemplatePart::Variable(_) | TemplatePart::VariableValue(_, _)) {
136 if let Some(value) = values_iter.next() {
138 part.set_value(value.clone());
139 } else {
140 break;
141 }
142 }
143 }
144 }
145
146 pub fn new_variable_value_for(
148 &self,
149 flat_variable_name: impl Into<String>,
150 value: impl Into<String>,
151 ) -> VariableValue {
152 VariableValue::new(&self.flat_root_cmd, flat_variable_name, value)
153 }
154}
155impl Display for CommandTemplate {
156 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
157 for part in self.parts.iter() {
158 write!(f, "{part}")?;
159 }
160 Ok(())
161 }
162}
163
164#[derive(Clone)]
166#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
167pub enum TemplatePart {
168 Text(String),
169 Variable(Variable),
170 VariableValue(Variable, String),
171}
172impl TemplatePart {
173 pub fn set_value(&mut self, value: Option<String>) -> Option<String> {
178 if !matches!(self, Self::Variable(_) | Self::VariableValue(_, _)) {
180 return None;
181 }
182
183 let taken_part = mem::take(self);
185
186 let (new_part, previous_value) = match taken_part {
188 Self::Variable(v) => (
189 match value {
190 Some(val) => Self::VariableValue(v, val),
191 None => Self::Variable(v),
192 },
193 None,
194 ),
195 Self::VariableValue(v, old_val) => (
196 match value {
197 Some(val) => Self::VariableValue(v, val),
198 None => Self::Variable(v),
199 },
200 Some(old_val),
201 ),
202 other => (other, None),
203 };
204
205 *self = new_part;
206 previous_value
207 }
208}
209impl Default for TemplatePart {
210 fn default() -> Self {
211 Self::Text(String::new())
212 }
213}
214impl Display for TemplatePart {
215 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
216 match self {
217 TemplatePart::Text(t) => write!(f, "{t}"),
218 TemplatePart::Variable(v) => write!(f, "{{{{{}}}}}", v.display),
219 TemplatePart::VariableValue(_, value) => write!(f, "{value}"),
220 }
221 }
222}
223
224#[derive(Clone)]
226#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
227pub struct Variable {
228 pub display: String,
230 pub options: Vec<String>,
232 pub flat_names: Vec<String>,
234 pub flat_name: String,
236 pub functions: Vec<VariableFunction>,
238 pub secret: bool,
240}
241impl Variable {
242 pub fn parse(text: impl Into<String>) -> Self {
244 let display: String = text.into();
245
246 let (content, secret) = match is_secret_variable(&display) {
248 Some(inner) => (inner, true),
249 None => (display.as_str(), false),
250 };
251
252 let parts: Vec<&str> = content.split(':').collect();
254 let mut functions = Vec::new();
255 let mut boundary_index = parts.len();
256
257 if parts.len() > 1 {
259 for (i, part) in parts.iter().enumerate().rev() {
260 if let Ok(func) = VariableFunction::from_str(part) {
261 functions.push(func);
262 boundary_index = i;
263 } else {
264 break;
265 }
266 }
267 }
268
269 functions.reverse();
271
272 let options_str = &parts[..boundary_index].join(":");
274 let (options, flat_names) = if options_str.is_empty() {
275 (vec![], vec![])
276 } else {
277 let mut options = Vec::new();
278 let mut flat_names = Vec::new();
279 let mut seen_options = HashSet::new();
280 let mut seen_flat_names = HashSet::new();
281
282 for option in options_str
283 .split('|')
284 .map(|o| o.trim())
285 .filter(|o| !o.is_empty())
286 .map(String::from)
287 {
288 if seen_options.insert(option.clone()) {
289 let flat_name = flatten_variable_name(&option);
290 if seen_flat_names.insert(flat_name.clone()) {
291 flat_names.push(flat_name);
292 }
293 options.push(option);
294 }
295 }
296
297 (options, flat_names)
298 };
299
300 let flat_name = flat_names.iter().join("|");
302
303 Self {
304 display,
305 options,
306 flat_names,
307 flat_name,
308 functions,
309 secret,
310 }
311 }
312
313 pub fn env_var_names(&self, include_individual: bool) -> HashSet<String> {
315 let mut names = HashSet::new();
316 let env_var_name = self.display.to_shouty_snake_case();
317 if !env_var_name.trim().is_empty() && env_var_name.trim() != "PATH" {
318 names.insert(env_var_name.trim().to_owned());
319 }
320 let env_var_name_no_fn = self.flat_name.to_shouty_snake_case();
321 if !env_var_name_no_fn.trim().is_empty() && env_var_name_no_fn.trim() != "PATH" {
322 names.insert(env_var_name_no_fn.trim().to_owned());
323 }
324 if include_individual {
325 names.extend(
326 self.flat_names
327 .iter()
328 .map(|o| o.to_shouty_snake_case())
329 .filter(|o| !o.trim().is_empty())
330 .filter(|o| o.trim() != "PATH")
331 .map(|o| o.trim().to_owned()),
332 );
333 }
334 names
335 }
336
337 pub fn apply_functions_to(&self, text: impl Into<String>) -> String {
339 let text = text.into();
340 let mut result = text;
341 for func in self.functions.iter() {
342 result = func.apply_to(result);
343 }
344 result
345 }
346
347 pub fn check_functions_char(&self, ch: char) -> Option<String> {
349 let mut out: Option<String> = None;
350 for func in self.functions.iter() {
351 if let Some(ref mut out) = out {
352 let mut new_out = String::from("");
353 for ch in out.chars() {
354 if let Some(replacement) = func.check_char(ch) {
355 new_out.push_str(&replacement);
356 } else {
357 new_out.push(ch);
358 }
359 }
360 *out = new_out;
361 } else if let Some(replacement) = func.check_char(ch) {
362 out = Some(replacement);
363 }
364 }
365 out
366 }
367}
368impl FromStr for Variable {
369 type Err = Infallible;
370
371 fn from_str(s: &str) -> Result<Self, Self::Err> {
372 Ok(Self::parse(s))
373 }
374}
375
376#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::EnumString)]
378pub enum VariableFunction {
379 #[strum(serialize = "kebab")]
380 KebabCase,
381 #[strum(serialize = "snake")]
382 SnakeCase,
383 #[strum(serialize = "upper")]
384 UpperCase,
385 #[strum(serialize = "lower")]
386 LowerCase,
387 #[strum(serialize = "url")]
388 UrlEncode,
389}
390impl VariableFunction {
391 pub fn apply_to(&self, input: impl AsRef<str>) -> String {
393 let input = input.as_ref();
394 match self {
395 Self::KebabCase => replace_separators(input, '-'),
396 Self::SnakeCase => replace_separators(input, '_'),
397 Self::UpperCase => input.to_uppercase(),
398 Self::LowerCase => input.to_lowercase(),
399 Self::UrlEncode => idempotent_percent_encode(input, NON_ALPHANUMERIC),
400 }
401 }
402
403 pub fn check_char(&self, ch: char) -> Option<String> {
405 match self {
406 Self::KebabCase | Self::SnakeCase => {
407 let separator = if *self == Self::KebabCase { '-' } else { '_' };
408 if ch != separator && is_separator(ch) {
409 Some(separator.to_string())
410 } else {
411 None
412 }
413 }
414 Self::UpperCase => {
415 if ch.is_lowercase() {
416 Some(ch.to_uppercase().to_string())
417 } else {
418 None
419 }
420 }
421 Self::LowerCase => {
422 if ch.is_uppercase() {
423 Some(ch.to_lowercase().to_string())
424 } else {
425 None
426 }
427 }
428 Self::UrlEncode => {
429 if ch.is_ascii_alphanumeric() {
430 None
431 } else {
432 Some(idempotent_percent_encode(&ch.to_string(), NON_ALPHANUMERIC))
433 }
434 }
435 }
436 }
437}
438
439fn is_secret_variable(variable_name: &str) -> Option<&str> {
441 if (variable_name.starts_with('*') && variable_name.ends_with('*') && variable_name.len() > 1)
442 || (variable_name.starts_with('{') && variable_name.ends_with('}'))
443 {
444 Some(&variable_name[1..variable_name.len() - 1])
445 } else {
446 None
447 }
448}
449
450fn is_separator(c: char) -> bool {
452 c == '-' || c == '_' || c.is_whitespace()
453}
454
455fn replace_separators(s: &str, separator: char) -> String {
457 let mut result = String::with_capacity(s.len());
458
459 let mut words = s.split(is_separator).filter(|word| !word.is_empty());
461
462 if let Some(first_word) = words.next() {
464 result.push_str(first_word);
465 }
466 for word in words {
468 result.push(separator);
469 result.push_str(word);
470 }
471
472 result
473}
474
475pub fn idempotent_percent_encode(input: &str, encode_set: &'static AsciiSet) -> String {
483 if let Ok(decoded) = percent_decode_str(input).decode_utf8() {
485 let re_encoded = utf8_percent_encode(&decoded, encode_set).to_string();
487
488 if re_encoded == input {
490 return re_encoded;
491 }
492 }
493
494 utf8_percent_encode(input, encode_set).to_string().to_string()
496}
497
498#[cfg(test)]
499mod tests {
500 use pretty_assertions::assert_eq;
501
502 use super::*;
503 #[test]
504 fn test_parse_command_with_variables() {
505 let cmd = CommandTemplate::parse("git commit -m {{{message}}} --author {{author:kebab}}", false);
506 assert_eq!(cmd.flat_root_cmd, "git");
507 assert_eq!(cmd.parts.len(), 4);
508 assert_eq!(cmd.parts[0], TemplatePart::Text("git commit -m ".into()));
509 assert!(matches!(cmd.parts[1], TemplatePart::Variable(_)));
510 assert_eq!(cmd.parts[2], TemplatePart::Text(" --author ".into()));
511 assert!(matches!(cmd.parts[3], TemplatePart::Variable(_)));
512 }
513
514 #[test]
515 fn test_parse_command_no_variables() {
516 let cmd = CommandTemplate::parse("echo 'hello world'", false);
517 assert_eq!(cmd.flat_root_cmd, "echo");
518 assert_eq!(cmd.parts.len(), 1);
519 assert_eq!(cmd.parts[0], TemplatePart::Text("echo 'hello world'".into()));
520 }
521
522 #[test]
523 fn test_has_pending_variable() {
524 let mut cmd = CommandTemplate::parse("cmd {{var1}} {{var2}}", false);
525 assert!(cmd.has_pending_variable());
526 cmd.set_variable_value(0, Some("value1".to_string()));
527 assert!(cmd.has_pending_variable());
528 cmd.set_variable_value(1, Some("value2".to_string()));
529 assert!(!cmd.has_pending_variable());
530 }
531
532 #[test]
533 fn test_previous_values_for() {
534 let mut cmd = CommandTemplate::parse("cmd {{var1}} {{var1}} {{var2}}", false);
535
536 assert_eq!(cmd.previous_values_for("var1"), None);
538 assert_eq!(cmd.previous_values_for("var2"), None);
539
540 cmd.set_variable_value(0, Some("val1".into()));
542 assert_eq!(cmd.previous_values_for("var1"), Some(vec!["val1".to_string()]));
543 assert_eq!(cmd.previous_values_for("var2"), None);
544
545 cmd.set_variable_value(1, Some("val1".into()));
547 assert_eq!(cmd.previous_values_for("var1"), Some(vec!["val1".to_string()]));
548
549 cmd.set_variable_value(1, Some("val1_other".into()));
551 let mut values = cmd.previous_values_for("var1").unwrap();
552 values.sort();
553 assert_eq!(values, vec!["val1".to_string(), "val1_other".to_string()]);
554
555 cmd.set_variable_value(2, Some("val2".into()));
557 assert_eq!(cmd.previous_values_for("var2"), Some(vec!["val2".to_string()]));
558 }
559
560 #[test]
561 fn test_variable_context() {
562 let mut cmd = CommandTemplate::parse("cmd {{var1}} {{{secret_var}}} {{var2}}", false);
563
564 cmd.set_variable_value(2, Some("value2".to_string()));
566 let context_before_secret: Vec<_> = cmd.variable_context().into_iter().collect();
567 assert_eq!(context_before_secret, vec![("var2".to_string(), "value2".to_string())]);
568
569 cmd.set_variable_value(1, Some("secret_value".to_string()));
571 let context_after_secret: Vec<_> = cmd.variable_context().into_iter().collect();
572 assert_eq!(context_after_secret, context_before_secret);
574 }
575
576 #[test]
577 fn test_variable_context_is_empty() {
578 let cmd = CommandTemplate::parse("cmd {{var1}}", false);
579 let context: Vec<_> = cmd.variable_context().into_iter().collect();
580 assert!(context.is_empty());
581 }
582
583 #[test]
584 fn test_count_variables() {
585 let mut cmd = CommandTemplate::parse("cmd {{var1}} middle {{var2}}", false);
586 assert_eq!(cmd.count_variables(), 2);
587
588 cmd.set_variable_value(0, Some("value1".to_string()));
589 assert_eq!(cmd.count_variables(), 2);
590
591 let cmd_no_vars = CommandTemplate::parse("cmd no-vars", false);
593 assert_eq!(cmd_no_vars.count_variables(), 0);
594 }
595
596 #[test]
597 fn test_variable_at() {
598 let cmd = CommandTemplate::parse("cmd {{var1}} middle {{var2}}", false);
599 let var1 = Variable::parse("var1");
600 let var2 = Variable::parse("var2");
601
602 assert_eq!(cmd.variable_at(0), Some(&var1));
604 assert_eq!(cmd.variable_at(1), Some(&var2));
605
606 assert_eq!(cmd.variable_at(2), None);
608
609 let cmd_no_vars = CommandTemplate::parse("cmd no-vars", false);
611 assert_eq!(cmd_no_vars.variable_at(0), None);
612 }
613
614 #[test]
615 fn test_set_variable_value() {
616 let mut cmd = CommandTemplate::parse("cmd {{var1}} {{var2}}", false);
617
618 let prev = cmd.set_variable_value(0, Some("val1".into()));
620 assert_eq!(prev, None);
621 let var1 = Variable::parse("var1");
622 assert_eq!(cmd.parts[1], TemplatePart::VariableValue(var1.clone(), "val1".into()));
623
624 let prev = cmd.set_variable_value(0, Some("new_val1".into()));
626 assert_eq!(prev, Some("val1".into()));
627 assert_eq!(
628 cmd.parts[1],
629 TemplatePart::VariableValue(var1.clone(), "new_val1".into())
630 );
631
632 let prev = cmd.set_variable_value(0, None);
634 assert_eq!(prev, Some("new_val1".into()));
635 assert_eq!(cmd.parts[1], TemplatePart::Variable(var1.clone()));
636
637 let prev = cmd.set_variable_value(1, Some("val2".into()));
639 assert_eq!(prev, None);
640 let var2 = Variable::parse("var2");
641 assert_eq!(cmd.parts[3], TemplatePart::VariableValue(var2.clone(), "val2".into()));
642
643 let prev = cmd.set_variable_value(2, Some("val3".into()));
645 assert_eq!(prev, None);
646 }
647
648 #[test]
649 fn test_set_variable_values_full_update() {
650 let mut cmd = CommandTemplate::parse("cmd {{var1}} {{var2}}", false);
651 let values = vec![Some("value1".to_string()), Some("value2".to_string())];
652 cmd.set_variable_values(&values);
653
654 let var1 = Variable::parse("var1");
655 let var2 = Variable::parse("var2");
656 assert_eq!(cmd.parts[1], TemplatePart::VariableValue(var1, "value1".into()));
657 assert_eq!(cmd.parts[3], TemplatePart::VariableValue(var2, "value2".into()));
658 assert!(!cmd.has_pending_variable());
659 }
660
661 #[test]
662 fn test_set_variable_values_partial_update() {
663 let mut cmd = CommandTemplate::parse("cmd {{var1}} {{var2}} {{var3}}", false);
664 let values = vec![Some("value1".to_string()), Some("value2".to_string())];
665 cmd.set_variable_values(&values);
666
667 let var1 = Variable::parse("var1");
668 let var2 = Variable::parse("var2");
669 let var3 = Variable::parse("var3");
670 assert_eq!(cmd.parts[1], TemplatePart::VariableValue(var1, "value1".into()));
671 assert_eq!(cmd.parts[3], TemplatePart::VariableValue(var2, "value2".into()));
672 assert_eq!(cmd.parts[5], TemplatePart::Variable(var3));
673 assert!(cmd.has_pending_variable());
674 }
675
676 #[test]
677 fn test_set_variable_values_with_none_to_unset() {
678 let mut cmd = CommandTemplate::parse("cmd {{var1}} {{var2}}", false);
679 cmd.set_variable_values(&[Some("val1".into()), Some("val2".into())]);
681 assert!(!cmd.has_pending_variable());
682
683 let values = vec![None, Some("new_val2".to_string())];
685 cmd.set_variable_values(&values);
686
687 let var1 = Variable::parse("var1");
688 let var2 = Variable::parse("var2");
689 assert_eq!(cmd.parts[1], TemplatePart::Variable(var1));
690 assert_eq!(cmd.parts[3], TemplatePart::VariableValue(var2, "new_val2".into()));
691 assert!(cmd.has_pending_variable());
692 }
693
694 #[test]
695 fn test_set_variable_values_empty_slice() {
696 let mut cmd = CommandTemplate::parse("cmd {{var1}} {{var2}}", false);
697 let original_parts = cmd.parts.clone();
698 cmd.set_variable_values(&[]);
699
700 assert_eq!(cmd.parts, original_parts);
701 }
702
703 #[test]
704 fn test_set_variable_values_more_values_than_variables() {
705 let mut cmd = CommandTemplate::parse("cmd {{var1}}", false);
706 let values = vec![Some("value1".to_string()), Some("ignored".to_string())];
707 cmd.set_variable_values(&values);
708
709 let var1 = Variable::parse("var1");
710 assert_eq!(cmd.parts[1], TemplatePart::VariableValue(var1, "value1".into()));
711 assert!(!cmd.has_pending_variable());
712 }
713
714 #[test]
715 fn test_template_part_set_value() {
716 let var = Variable::parse("v");
717
718 let mut part1 = TemplatePart::Variable(var.clone());
720 let prev1 = part1.set_value(Some("value".into()));
721 assert_eq!(prev1, None);
722 assert_eq!(part1, TemplatePart::VariableValue(var.clone(), "value".into()));
723
724 let mut part2 = TemplatePart::VariableValue(var.clone(), "old".into());
726 let prev2 = part2.set_value(Some("new".into()));
727 assert_eq!(prev2, Some("old".into()));
728 assert_eq!(part2, TemplatePart::VariableValue(var.clone(), "new".into()));
729
730 let mut part3 = TemplatePart::VariableValue(var.clone(), "value".into());
732 let prev3 = part3.set_value(None);
733 assert_eq!(prev3, Some("value".into()));
734 assert_eq!(part3, TemplatePart::Variable(var.clone()));
735
736 let mut part4 = TemplatePart::Variable(var.clone());
738 let prev4 = part4.set_value(None);
739 assert_eq!(prev4, None);
740 assert_eq!(part4, TemplatePart::Variable(var.clone()));
741
742 let mut part5 = TemplatePart::Text("text".into());
744 let prev5 = part5.set_value(Some("value".into()));
745 assert_eq!(prev5, None);
746 assert_eq!(part5, TemplatePart::Text("text".into()));
747 }
748
749 #[test]
750 fn test_parse_simple_variable() {
751 let variable = Variable::parse("my_variable");
752 assert_eq!(
753 variable,
754 Variable {
755 display: "my_variable".into(),
756 options: vec!["my_variable".into()],
757 flat_names: vec!["my_variable".into()],
758 flat_name: "my_variable".into(),
759 functions: vec![],
760 secret: false,
761 }
762 );
763 }
764
765 #[test]
766 fn test_parse_secret_variable() {
767 let variable = Variable::parse("{my_secret}");
768 assert_eq!(
769 variable,
770 Variable {
771 display: "{my_secret}".into(),
772 options: vec!["my_secret".into()],
773 flat_names: vec!["my_secret".into()],
774 flat_name: "my_secret".into(),
775 functions: vec![],
776 secret: true,
777 }
778 );
779 }
780
781 #[test]
782 fn test_parse_variable_with_multiple_options() {
783 let variable = Variable::parse("Option 1 | option 1 | Option 2 | Option 2 | Option 3");
784 assert_eq!(
785 variable,
786 Variable {
787 display: "Option 1 | option 1 | Option 2 | Option 2 | Option 3".into(),
788 options: vec![
789 "Option 1".into(),
790 "option 1".into(),
791 "Option 2".into(),
792 "Option 3".into()
793 ],
794 flat_names: vec!["option 1".into(), "option 2".into(), "option 3".into()],
795 flat_name: "option 1|option 2|option 3".into(),
796 functions: vec![],
797 secret: false,
798 }
799 );
800 }
801
802 #[test]
803 fn test_parse_variable_with_single_function() {
804 let variable = Variable::parse("my_variable:kebab");
805 assert_eq!(
806 variable,
807 Variable {
808 display: "my_variable:kebab".into(),
809 options: vec!["my_variable".into()],
810 flat_names: vec!["my_variable".into()],
811 flat_name: "my_variable".into(),
812 functions: vec![VariableFunction::KebabCase],
813 secret: false,
814 }
815 );
816 }
817
818 #[test]
819 fn test_parse_variable_with_multiple_functions() {
820 let variable = Variable::parse("my_variable:snake:upper");
821 assert_eq!(
822 variable,
823 Variable {
824 display: "my_variable:snake:upper".into(),
825 options: vec!["my_variable".into()],
826 flat_names: vec!["my_variable".into()],
827 flat_name: "my_variable".into(),
828 functions: vec![VariableFunction::SnakeCase, VariableFunction::UpperCase],
829 secret: false,
830 }
831 );
832 }
833
834 #[test]
835 fn test_parse_variable_with_options_and_functions() {
836 let variable = Variable::parse("opt1|opt2:lower:kebab");
837 assert_eq!(
838 variable,
839 Variable {
840 display: "opt1|opt2:lower:kebab".into(),
841 options: vec!["opt1".into(), "opt2".into()],
842 flat_names: vec!["opt1".into(), "opt2".into()],
843 flat_name: "opt1|opt2".into(),
844 functions: vec![VariableFunction::LowerCase, VariableFunction::KebabCase],
845 secret: false,
846 }
847 );
848 }
849
850 #[test]
851 fn test_parse_variable_with_colon_in_options() {
852 let variable = Variable::parse("key:value:kebab");
853 assert_eq!(
854 variable,
855 Variable {
856 display: "key:value:kebab".into(),
857 options: vec!["key:value".into()],
858 flat_names: vec!["key:value".into()],
859 flat_name: "key:value".into(),
860 functions: vec![VariableFunction::KebabCase],
861 secret: false,
862 }
863 );
864 }
865
866 #[test]
867 fn test_parse_variable_with_only_functions() {
868 let variable = Variable::parse(":snake");
869 assert_eq!(
870 variable,
871 Variable {
872 display: ":snake".into(),
873 options: vec![],
874 flat_names: vec![],
875 flat_name: "".into(),
876 functions: vec![VariableFunction::SnakeCase],
877 secret: false,
878 }
879 );
880 }
881
882 #[test]
883 fn test_parse_variable_that_is_a_function_name() {
884 let variable = Variable::parse("kebab");
885 assert_eq!(
886 variable,
887 Variable {
888 display: "kebab".into(),
889 options: vec!["kebab".into()],
890 flat_names: vec!["kebab".into()],
891 flat_name: "kebab".into(),
892 functions: vec![],
893 secret: false,
894 }
895 );
896 }
897
898 #[test]
899 fn test_variable_env_var_names() {
900 let var1 = Variable::parse("my-variable");
902 assert_eq!(var1.env_var_names(true), HashSet::from(["MY_VARIABLE".into()]));
903
904 let var2 = Variable::parse("option1|option2");
906 assert_eq!(
907 var2.env_var_names(true),
908 HashSet::from(["OPTION1_OPTION2".into(), "OPTION1".into(), "OPTION2".into()])
909 );
910 assert_eq!(var2.env_var_names(false), HashSet::from(["OPTION1_OPTION2".into()]));
911
912 let var3 = Variable::parse("my-variable:kebab:upper");
914 assert_eq!(
915 var3.env_var_names(true),
916 HashSet::from(["MY_VARIABLE_KEBAB_UPPER".into(), "MY_VARIABLE".into()])
917 );
918
919 let var4 = Variable::parse("*my-secret*");
921 assert_eq!(var4.env_var_names(true), HashSet::from(["MY_SECRET".into()]));
922
923 let var5 = Variable::parse("{my-secret}");
925 assert_eq!(var5.env_var_names(true), HashSet::from(["MY_SECRET".into()]));
926 }
927
928 #[test]
929 fn test_variable_apply_functions_to() {
930 let var_none = Variable::parse("text");
932 assert_eq!(var_none.apply_functions_to("Hello World"), "Hello World");
933
934 let var_upper = Variable::parse("text:upper");
936 assert_eq!(var_upper.apply_functions_to("Hello World"), "HELLO WORLD");
937
938 let var_kebab_upper = Variable::parse("text:kebab:upper");
940 assert_eq!(var_kebab_upper.apply_functions_to("Hello World"), "HELLO-WORLD");
941
942 let var_snake_lower = Variable::parse("text:snake:lower");
944 assert_eq!(var_snake_lower.apply_functions_to("Hello World"), "hello_world");
945 }
946
947 #[test]
948 fn test_variable_check_functions_char() {
949 let var_none = Variable::parse("text");
951 assert_eq!(var_none.check_functions_char('a'), None);
952 assert_eq!(var_none.check_functions_char(' '), None);
953
954 let var_upper = Variable::parse("text:upper");
956 assert_eq!(var_upper.check_functions_char('a'), Some("A".to_string()));
957
958 let var_lower = Variable::parse("text:lower");
960 assert_eq!(var_lower.check_functions_char('a'), None);
961
962 let var_upper_kebab = Variable::parse("text:upper:kebab");
964 assert_eq!(var_upper_kebab.check_functions_char('a'), Some("A".to_string()));
965 assert_eq!(var_upper_kebab.check_functions_char(' '), Some("-".to_string()));
966 assert_eq!(var_upper_kebab.check_functions_char('-'), None);
967 }
968
969 #[test]
970 fn test_variable_function_apply_to() {
971 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("some_text"), "some-text");
975 assert_eq!(VariableFunction::KebabCase.apply_to("-"), "");
976 assert_eq!(VariableFunction::KebabCase.apply_to("_"), "");
977
978 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("some-text"), "some_text");
982 assert_eq!(VariableFunction::SnakeCase.apply_to("-"), "");
983 assert_eq!(VariableFunction::SnakeCase.apply_to("_"), "");
984
985 assert_eq!(VariableFunction::UpperCase.apply_to("some text"), "SOME TEXT");
987 assert_eq!(VariableFunction::UpperCase.apply_to("SomeText"), "SOMETEXT");
988
989 assert_eq!(VariableFunction::LowerCase.apply_to("SOME TEXT"), "some text");
991 assert_eq!(VariableFunction::LowerCase.apply_to("SomeText"), "sometext");
992
993 assert_eq!(VariableFunction::UrlEncode.apply_to("some text"), "some%20text");
995 assert_eq!(VariableFunction::UrlEncode.apply_to("Some Text"), "Some%20Text");
996 assert_eq!(VariableFunction::UrlEncode.apply_to("some-text"), "some%2Dtext");
997 assert_eq!(VariableFunction::UrlEncode.apply_to("some_text"), "some%5Ftext");
998 assert_eq!(
999 VariableFunction::UrlEncode.apply_to("!@#$%^&*()"),
1000 "%21%40%23%24%25%5E%26%2A%28%29"
1001 );
1002 assert_eq!(VariableFunction::UrlEncode.apply_to("some%20text"), "some%20text");
1003 }
1004
1005 #[test]
1006 fn test_variable_function_check_char() {
1007 assert_eq!(VariableFunction::KebabCase.check_char(' '), Some("-".to_string()));
1009 assert_eq!(VariableFunction::KebabCase.check_char('_'), Some("-".to_string()));
1010 assert_eq!(VariableFunction::KebabCase.check_char('-'), None);
1011 assert_eq!(VariableFunction::KebabCase.check_char('A'), None);
1012
1013 assert_eq!(VariableFunction::SnakeCase.check_char(' '), Some("_".to_string()));
1015 assert_eq!(VariableFunction::SnakeCase.check_char('-'), Some("_".to_string()));
1016 assert_eq!(VariableFunction::SnakeCase.check_char('_'), None);
1017 assert_eq!(VariableFunction::SnakeCase.check_char('A'), None);
1018
1019 assert_eq!(VariableFunction::UpperCase.check_char('a'), Some("A".to_string()));
1021 assert_eq!(VariableFunction::UpperCase.check_char('A'), None);
1022 assert_eq!(VariableFunction::UpperCase.check_char(' '), None);
1023
1024 assert_eq!(VariableFunction::LowerCase.check_char('A'), Some("a".to_string()));
1026 assert_eq!(VariableFunction::LowerCase.check_char('a'), None);
1027 assert_eq!(VariableFunction::LowerCase.check_char(' '), None);
1028
1029 assert_eq!(VariableFunction::UrlEncode.check_char(' '), Some("%20".to_string()));
1031 assert_eq!(VariableFunction::UrlEncode.check_char('!'), Some("%21".to_string()));
1032 assert_eq!(VariableFunction::UrlEncode.check_char('A'), None);
1033 assert_eq!(VariableFunction::UrlEncode.check_char('1'), None);
1034 assert_eq!(VariableFunction::UrlEncode.check_char('-'), Some("%2D".to_string()));
1035 assert_eq!(VariableFunction::UrlEncode.check_char('_'), Some("%5F".to_string()));
1036 }
1037
1038 #[test]
1039 fn test_is_secret_variable() {
1040 assert_eq!(is_secret_variable("*secret*"), Some("secret"));
1042 assert_eq!(is_secret_variable("* another secret *"), Some(" another secret "));
1043 assert_eq!(is_secret_variable("**"), Some(""));
1044
1045 assert_eq!(is_secret_variable("{secret}"), Some("secret"));
1047 assert_eq!(is_secret_variable("{ another secret }"), Some(" another secret "));
1048 assert_eq!(is_secret_variable("{}"), Some(""));
1049
1050 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("not-secret}"), 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 assert_eq!(is_secret_variable("*{"), None);
1061 }
1062}