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#[derive(Clone)]
20#[cfg_attr(test, derive(Debug))]
21pub struct CommandTemplate {
22 pub flat_root_cmd: String,
24 pub parts: Vec<TemplatePart>,
26}
27impl CommandTemplate {
28 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 pub fn has_pending_variable(&self) -> bool {
54 self.parts.iter().any(|part| matches!(part, TemplatePart::Variable(_)))
55 }
56
57 pub fn previous_values_for(&self, flat_variable_name: &str) -> Option<Vec<String>> {
59 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 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 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 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 pub fn set_variable_value(&mut self, index: usize, value: Option<String>) -> Option<String> {
115 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 pub fn set_variable_values(&mut self, values: &[Option<String>]) {
128 let mut values_iter = values.iter();
130
131 for part in self.parts.iter_mut() {
133 if matches!(part, TemplatePart::Variable(_) | TemplatePart::VariableValue(_, _)) {
135 if let Some(value) = values_iter.next() {
137 part.set_value(value.clone());
138 } else {
139 break;
140 }
141 }
142 }
143 }
144
145 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#[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 pub fn set_value(&mut self, value: Option<String>) -> Option<String> {
177 if !matches!(self, Self::Variable(_) | Self::VariableValue(_, _)) {
179 return None;
180 }
181
182 let taken_part = mem::take(self);
184
185 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#[derive(Clone)]
225#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
226pub struct Variable {
227 pub display: String,
229 pub options: Vec<String>,
231 pub flat_names: Vec<String>,
233 pub flat_name: String,
235 pub functions: Vec<VariableFunction>,
237 pub secret: bool,
239}
240impl Variable {
241 pub fn parse(text: impl Into<String>) -> Self {
243 let display: String = text.into();
244
245 let (content, secret) = match is_secret_variable(&display) {
247 Some(inner) => (inner, true),
248 None => (display.as_str(), false),
249 };
250
251 let parts: Vec<&str> = content.split(':').collect();
253 let mut functions = Vec::new();
254 let mut boundary_index = parts.len();
255
256 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 functions.reverse();
270
271 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 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 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 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 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#[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 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 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
438fn 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
449fn is_separator(c: char) -> bool {
451 c == '-' || c == '_' || c.is_whitespace()
452}
453
454fn replace_separators(s: &str, separator: char) -> String {
456 let mut result = String::with_capacity(s.len());
457
458 let mut words = s.split(is_separator).filter(|word| !word.is_empty());
460
461 if let Some(first_word) = words.next() {
463 result.push_str(first_word);
464 }
465 for word in words {
467 result.push(separator);
468 result.push_str(word);
469 }
470
471 result
472}
473
474pub fn idempotent_percent_encode(input: &str, encode_set: &'static AsciiSet) -> String {
482 if let Ok(decoded) = percent_decode_str(input).decode_utf8() {
484 let re_encoded = utf8_percent_encode(&decoded, encode_set).to_string();
486
487 if re_encoded == input {
489 return re_encoded;
490 }
491 }
492
493 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 assert_eq!(cmd.previous_values_for("var1"), None);
537 assert_eq!(cmd.previous_values_for("var2"), None);
538
539 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 cmd.set_variable_value(1, Some("val1".into()));
546 assert_eq!(cmd.previous_values_for("var1"), Some(vec!["val1".to_string()]));
547
548 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 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 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 cmd.set_variable_value(1, Some("secret_value".to_string()));
570 let context_after_secret: Vec<_> = cmd.variable_context().into_iter().collect();
571 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 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 assert_eq!(cmd.variable_at(0), Some(&var1));
603 assert_eq!(cmd.variable_at(1), Some(&var2));
604
605 assert_eq!(cmd.variable_at(2), None);
607
608 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 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 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 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 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 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 cmd.set_variable_values(&[Some("val1".into()), Some("val2".into())]);
680 assert!(!cmd.has_pending_variable());
681
682 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 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 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 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 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 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 let var1 = Variable::parse("my-variable");
901 assert_eq!(var1.env_var_names(true), HashSet::from(["MY_VARIABLE".into()]));
902
903 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 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 let var4 = Variable::parse("*my-secret*");
920 assert_eq!(var4.env_var_names(true), HashSet::from(["MY_SECRET".into()]));
921
922 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 let var_none = Variable::parse("text");
931 assert_eq!(var_none.apply_functions_to("Hello World"), "Hello World");
932
933 let var_upper = Variable::parse("text:upper");
935 assert_eq!(var_upper.apply_functions_to("Hello World"), "HELLO WORLD");
936
937 let var_kebab_upper = Variable::parse("text:kebab:upper");
939 assert_eq!(var_kebab_upper.apply_functions_to("Hello World"), "HELLO-WORLD");
940
941 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 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 let var_upper = Variable::parse("text:upper");
955 assert_eq!(var_upper.check_functions_char('a'), Some("A".to_string()));
956
957 let var_lower = Variable::parse("text:lower");
959 assert_eq!(var_lower.check_functions_char('a'), None);
960
961 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 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 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 assert_eq!(VariableFunction::UpperCase.apply_to("some text"), "SOME TEXT");
986 assert_eq!(VariableFunction::UpperCase.apply_to("SomeText"), "SOMETEXT");
987
988 assert_eq!(VariableFunction::LowerCase.apply_to("SOME TEXT"), "some text");
990 assert_eq!(VariableFunction::LowerCase.apply_to("SomeText"), "sometext");
991
992 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 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 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 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 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 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 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 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 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}