1use std::{
2 collections::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 crate::utils::{COMMAND_VARIABLE_REGEX, SplitCaptures, SplitItem, flatten_str, flatten_variable};
14
15pub enum VariableSuggestion {
17 Secret,
19 New,
21 Environment {
23 env_var_name: String,
24 value: Option<String>,
25 },
26 Existing(VariableValue),
28 Derived(String),
30}
31
32#[derive(Clone)]
34#[cfg_attr(debug_assertions, derive(Debug))]
35pub struct VariableValue {
36 pub id: Option<i32>,
38 pub flat_root_cmd: String,
40 pub flat_variable: String,
42 pub value: String,
44}
45
46impl VariableValue {
47 pub fn new(root_cmd: impl Into<String>, variable_name: impl Into<String>, value: impl Into<String>) -> Self {
49 Self {
50 id: None,
51 flat_root_cmd: flatten_str(root_cmd.into()),
52 flat_variable: flatten_variable(variable_name.into()),
53 value: value.into(),
54 }
55 }
56}
57
58#[cfg_attr(debug_assertions, derive(Debug))]
60#[derive(Clone)]
61pub struct DynamicCommand {
62 pub root: String,
64 pub parts: Vec<CommandPart>,
66}
67impl DynamicCommand {
68 pub fn parse(cmd: impl AsRef<str>) -> Self {
70 let cmd = cmd.as_ref();
71 let splitter = SplitCaptures::new(&COMMAND_VARIABLE_REGEX, cmd);
72 let parts = splitter
73 .map(|e| match e {
74 SplitItem::Unmatched(t) => CommandPart::Text(t.to_owned()),
75 SplitItem::Captured(v) => CommandPart::Variable(Variable::parse(v.get(1).unwrap().as_str())),
76 })
77 .collect::<Vec<_>>();
78
79 DynamicCommand {
80 root: cmd.split_whitespace().next().unwrap_or(cmd).to_owned(),
81 parts,
82 }
83 }
84
85 pub fn has_pending_variable(&self) -> bool {
87 self.parts.iter().any(|part| matches!(part, CommandPart::Variable(_)))
88 }
89
90 pub fn current_variable(&self) -> Option<&Variable> {
92 self.parts.iter().find_map(|part| {
93 if let CommandPart::Variable(v) = part {
94 Some(v)
95 } else {
96 None
97 }
98 })
99 }
100
101 pub fn current_variable_context(&self) -> impl IntoIterator<Item = (String, String)> {
103 self.parts
104 .iter()
105 .take_while(|part| !matches!(part, CommandPart::Variable(_)))
106 .filter_map(|part| {
107 if let CommandPart::VariableValue(v, value) = part
108 && !v.secret
109 {
110 Some((v.name.clone(), value.clone()))
111 } else {
112 None
113 }
114 })
115 }
116
117 pub fn set_next_variable(&mut self, value: impl Into<String>) {
119 if let Some(part) = self.parts.iter_mut().find(|p| matches!(p, CommandPart::Variable(_))) {
121 if let CommandPart::Variable(v) = mem::take(part) {
123 *part = CommandPart::VariableValue(v, value.into());
124 } else {
125 unreachable!();
126 }
127 }
128 }
129
130 pub fn new_variable_value_for(&self, variable_name: impl Into<String>, value: impl Into<String>) -> VariableValue {
132 VariableValue::new(&self.root, variable_name, value)
133 }
134}
135impl FromStr for DynamicCommand {
136 type Err = Infallible;
137
138 fn from_str(s: &str) -> Result<Self, Self::Err> {
139 Ok(Self::parse(s))
140 }
141}
142impl Display for DynamicCommand {
143 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
144 for part in self.parts.iter() {
145 write!(f, "{part}")?;
146 }
147 Ok(())
148 }
149}
150
151#[cfg_attr(debug_assertions, derive(Debug, PartialEq, Eq))]
153#[derive(Clone)]
154pub enum CommandPart {
155 Text(String),
156 Variable(Variable),
157 VariableValue(Variable, String),
158}
159impl Default for CommandPart {
160 fn default() -> Self {
161 Self::Text(String::new())
162 }
163}
164impl Display for CommandPart {
165 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
166 match self {
167 CommandPart::Text(t) => write!(f, "{t}"),
168 CommandPart::Variable(v) => write!(f, "{{{{{}}}}}", v.name),
169 CommandPart::VariableValue(_, value) => write!(f, "{value}"),
170 }
171 }
172}
173
174#[cfg_attr(debug_assertions, derive(Debug, PartialEq, Eq))]
176#[derive(Clone)]
177pub struct Variable {
178 pub name: String,
180 pub options: Vec<String>,
182 pub functions: Vec<VariableFunction>,
184 pub secret: bool,
186}
187impl Variable {
188 pub fn parse(text: impl Into<String>) -> Self {
190 let name: String = text.into();
191
192 let (variable_name, secret) = match is_secret_variable(&name) {
194 Some(inner) => (inner, true),
195 None => (name.as_str(), false),
196 };
197
198 let parts: Vec<&str> = variable_name.split(':').collect();
200 let mut functions = Vec::new();
201 let mut boundary_index = parts.len();
202
203 if parts.len() > 1 {
205 for (i, part) in parts.iter().enumerate().rev() {
206 if let Ok(func) = VariableFunction::from_str(part) {
207 functions.push(func);
208 boundary_index = i;
209 } else {
210 break;
211 }
212 }
213 }
214
215 functions.reverse();
217
218 let options_str = &parts[..boundary_index].join(":");
220 let options = if options_str.is_empty() {
221 vec![]
222 } else {
223 options_str
224 .split('|')
225 .map(|o| o.trim())
226 .filter(|o| !o.is_empty())
227 .map(String::from)
228 .collect()
229 };
230
231 Self {
232 name,
233 options,
234 functions,
235 secret,
236 }
237 }
238
239 pub fn env_var_names(&self, include_options: bool) -> HashSet<String> {
241 let mut names = HashSet::new();
242 let env_var_name = self.name.to_shouty_snake_case();
243 if !env_var_name.trim().is_empty() && env_var_name.trim() != "PATH" {
244 names.insert(env_var_name.trim().to_owned());
245 }
246 let env_var_name_no_fn = self.options.iter().join("|").to_shouty_snake_case();
247 if !env_var_name_no_fn.trim().is_empty() && env_var_name_no_fn.trim() != "PATH" {
248 names.insert(env_var_name_no_fn.trim().to_owned());
249 }
250 if include_options {
251 names.extend(
252 self.options
253 .iter()
254 .map(|o| o.to_shouty_snake_case())
255 .filter(|o| !o.trim().is_empty())
256 .filter(|o| o.trim() != "PATH")
257 .map(|o| o.trim().to_owned()),
258 );
259 }
260 names
261 }
262
263 pub fn apply_functions_to(&self, text: impl Into<String>) -> String {
265 let text = text.into();
266 let mut result = text;
267 for func in self.functions.iter() {
268 result = func.apply_to(result);
269 }
270 result
271 }
272
273 pub fn check_functions_char(&self, ch: char) -> Option<String> {
275 let mut out: Option<String> = None;
276 for func in self.functions.iter() {
277 if let Some(ref mut out) = out {
278 let mut new_out = String::from("");
279 for ch in out.chars() {
280 if let Some(replacement) = func.check_char(ch) {
281 new_out.push_str(&replacement);
282 } else {
283 new_out.push(ch);
284 }
285 }
286 *out = new_out;
287 } else if let Some(replacement) = func.check_char(ch) {
288 out = Some(replacement);
289 }
290 }
291 out
292 }
293}
294impl FromStr for Variable {
295 type Err = Infallible;
296
297 fn from_str(s: &str) -> Result<Self, Self::Err> {
298 Ok(Self::parse(s))
299 }
300}
301
302#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::EnumString)]
304pub enum VariableFunction {
305 #[strum(serialize = "kebab")]
306 KebabCase,
307 #[strum(serialize = "snake")]
308 SnakeCase,
309 #[strum(serialize = "upper")]
310 UpperCase,
311 #[strum(serialize = "lower")]
312 LowerCase,
313 #[strum(serialize = "url")]
314 Urlencode,
315}
316impl VariableFunction {
317 pub fn apply_to(&self, input: impl AsRef<str>) -> String {
319 let input = input.as_ref();
320 match self {
321 Self::KebabCase => replace_separators(input, '-'),
322 Self::SnakeCase => replace_separators(input, '_'),
323 Self::UpperCase => input.to_uppercase(),
324 Self::LowerCase => input.to_lowercase(),
325 Self::Urlencode => idempotent_percent_encode(input, NON_ALPHANUMERIC),
326 }
327 }
328
329 pub fn check_char(&self, ch: char) -> Option<String> {
331 match self {
332 Self::KebabCase | Self::SnakeCase => {
333 let separator = if *self == Self::KebabCase { '-' } else { '_' };
334 if ch != separator && is_separator(ch) {
335 Some(separator.to_string())
336 } else {
337 None
338 }
339 }
340 Self::UpperCase => {
341 if ch.is_lowercase() {
342 Some(ch.to_uppercase().to_string())
343 } else {
344 None
345 }
346 }
347 Self::LowerCase => {
348 if ch.is_uppercase() {
349 Some(ch.to_lowercase().to_string())
350 } else {
351 None
352 }
353 }
354 Self::Urlencode => {
355 if ch.is_ascii_alphanumeric() {
356 None
357 } else {
358 Some(idempotent_percent_encode(&ch.to_string(), NON_ALPHANUMERIC))
359 }
360 }
361 }
362 }
363}
364
365fn is_secret_variable(variable_name: &str) -> Option<&str> {
367 if (variable_name.starts_with('*') && variable_name.ends_with('*') && variable_name.len() > 1)
368 || (variable_name.starts_with('{') && variable_name.ends_with('}'))
369 {
370 Some(&variable_name[1..variable_name.len() - 1])
371 } else {
372 None
373 }
374}
375
376fn is_separator(c: char) -> bool {
378 c == '-' || c == '_' || c.is_whitespace()
379}
380
381fn replace_separators(s: &str, separator: char) -> String {
383 let mut result = String::with_capacity(s.len());
384
385 let mut words = s.split(is_separator).filter(|word| !word.is_empty());
387
388 if let Some(first_word) = words.next() {
390 result.push_str(first_word);
391 }
392 for word in words {
394 result.push(separator);
395 result.push_str(word);
396 }
397
398 result
399}
400
401pub fn idempotent_percent_encode(input: &str, encode_set: &'static AsciiSet) -> String {
409 if let Ok(decoded) = percent_decode_str(input).decode_utf8() {
411 let re_encoded = utf8_percent_encode(&decoded, encode_set).to_string();
413
414 if re_encoded == input {
416 return re_encoded;
417 }
418 }
419
420 utf8_percent_encode(input, encode_set).to_string().to_string()
422}
423
424#[cfg(test)]
425mod tests {
426 use pretty_assertions::assert_eq;
427
428 use super::*;
429 #[test]
430 fn test_parse_command_with_variables() {
431 let cmd = DynamicCommand::parse("git commit -m {{{message}}} --author {{author:kebab}}");
432 assert_eq!(cmd.root, "git");
433 assert_eq!(cmd.parts.len(), 4);
434 assert_eq!(cmd.parts[0], CommandPart::Text("git commit -m ".into()));
435 assert!(matches!(cmd.parts[1], CommandPart::Variable(_)));
436 assert_eq!(cmd.parts[2], CommandPart::Text(" --author ".into()));
437 assert!(matches!(cmd.parts[3], CommandPart::Variable(_)));
438 }
439
440 #[test]
441 fn test_parse_command_no_variables() {
442 let cmd = DynamicCommand::parse("echo 'hello world'");
443 assert_eq!(cmd.root, "echo");
444 assert_eq!(cmd.parts.len(), 1);
445 assert_eq!(cmd.parts[0], CommandPart::Text("echo 'hello world'".into()));
446 }
447
448 #[test]
449 fn test_set_next_variable() {
450 let mut cmd = DynamicCommand::parse("cmd {{var1}} {{var2}}");
451 cmd.set_next_variable("value1");
452 let var1 = Variable::parse("var1");
453 assert_eq!(cmd.parts[1], CommandPart::VariableValue(var1, "value1".into()));
454 cmd.set_next_variable("value2");
455 let var2 = Variable::parse("var2");
456 assert_eq!(cmd.parts[3], CommandPart::VariableValue(var2, "value2".into()));
457 }
458
459 #[test]
460 fn test_has_pending_variable() {
461 let mut cmd = DynamicCommand::parse("cmd {{var1}} {{var2}}");
462 assert!(cmd.has_pending_variable());
463 cmd.set_next_variable("value1");
464 assert!(cmd.has_pending_variable());
465 cmd.set_next_variable("value2");
466 assert!(!cmd.has_pending_variable());
467 }
468
469 #[test]
470 fn test_current_variable() {
471 let mut cmd = DynamicCommand::parse("cmd {{var1}} {{var2}}");
472 assert_eq!(cmd.current_variable().map(|l| l.name.as_str()), Some("var1"));
473 cmd.set_next_variable("value1");
474 assert_eq!(cmd.current_variable().map(|l| l.name.as_str()), Some("var2"));
475 cmd.set_next_variable("value2");
476 assert_eq!(cmd.current_variable(), None);
477 }
478
479 #[test]
480 fn test_current_variable_context() {
481 let mut cmd = DynamicCommand::parse("cmd {{var1}} {{{secret_var}}} {{var2}}");
482
483 cmd.set_next_variable("value1");
485 let context_before_secret: Vec<_> = cmd.current_variable_context().into_iter().collect();
486 assert_eq!(context_before_secret, vec![("var1".to_string(), "value1".to_string())]);
487
488 cmd.set_next_variable("secret_value");
490 let context_after_secret: Vec<_> = cmd.current_variable_context().into_iter().collect();
491 assert_eq!(context_after_secret, context_before_secret);
493 }
494
495 #[test]
496 fn test_current_variable_context_is_empty() {
497 let cmd = DynamicCommand::parse("cmd {{var1}}");
498 let context: Vec<_> = cmd.current_variable_context().into_iter().collect();
499 assert!(context.is_empty());
500 }
501
502 #[test]
503 fn test_parse_simple_variable() {
504 let variable = Variable::parse("my_variable");
505 assert_eq!(
506 variable,
507 Variable {
508 name: "my_variable".into(),
509 options: vec!["my_variable".into()],
510 functions: vec![],
511 secret: false,
512 }
513 );
514 }
515
516 #[test]
517 fn test_parse_secret_variable() {
518 let variable = Variable::parse("{my_secret}");
519 assert_eq!(
520 variable,
521 Variable {
522 name: "{my_secret}".into(),
523 options: vec!["my_secret".into()],
524 functions: vec![],
525 secret: true,
526 }
527 );
528 }
529
530 #[test]
531 fn test_parse_variable_with_multiple_options() {
532 let variable = Variable::parse("option1|option2|option3");
533 assert_eq!(
534 variable,
535 Variable {
536 name: "option1|option2|option3".into(),
537 options: vec!["option1".into(), "option2".into(), "option3".into()],
538 functions: vec![],
539 secret: false,
540 }
541 );
542 }
543
544 #[test]
545 fn test_parse_variable_with_single_function() {
546 let variable = Variable::parse("my_variable:kebab");
547 assert_eq!(
548 variable,
549 Variable {
550 name: "my_variable:kebab".into(),
551 options: vec!["my_variable".into()],
552 functions: vec![VariableFunction::KebabCase],
553 secret: false,
554 }
555 );
556 }
557
558 #[test]
559 fn test_parse_variable_with_multiple_functions() {
560 let variable = Variable::parse("my_variable:snake:upper");
561 assert_eq!(
562 variable,
563 Variable {
564 name: "my_variable:snake:upper".into(),
565 options: vec!["my_variable".into()],
566 functions: vec![VariableFunction::SnakeCase, VariableFunction::UpperCase],
567 secret: false,
568 }
569 );
570 }
571
572 #[test]
573 fn test_parse_variable_with_options_and_functions() {
574 let variable = Variable::parse("opt1|opt2:lower:kebab");
575 assert_eq!(
576 variable,
577 Variable {
578 name: "opt1|opt2:lower:kebab".into(),
579 options: vec!["opt1".into(), "opt2".into()],
580 functions: vec![VariableFunction::LowerCase, VariableFunction::KebabCase],
581 secret: false,
582 }
583 );
584 }
585
586 #[test]
587 fn test_parse_variable_with_colon_in_options() {
588 let variable = Variable::parse("key:value:kebab");
589 assert_eq!(
590 variable,
591 Variable {
592 name: "key:value:kebab".into(),
593 options: vec!["key:value".into()],
594 functions: vec![VariableFunction::KebabCase],
595 secret: false,
596 }
597 );
598 }
599
600 #[test]
601 fn test_parse_variable_with_only_functions() {
602 let variable = Variable::parse(":snake");
603 assert_eq!(
604 variable,
605 Variable {
606 name: ":snake".into(),
607 options: vec![],
608 functions: vec![VariableFunction::SnakeCase],
609 secret: false,
610 }
611 );
612 }
613
614 #[test]
615 fn test_parse_variable_that_is_a_function_name() {
616 let variable = Variable::parse("kebab");
617 assert_eq!(
618 variable,
619 Variable {
620 name: "kebab".into(),
621 options: vec!["kebab".into()],
622 functions: vec![],
623 secret: false,
624 }
625 );
626 }
627
628 #[test]
629 fn test_variable_env_var_names() {
630 let var1 = Variable::parse("my-variable");
632 assert_eq!(var1.env_var_names(true), HashSet::from(["MY_VARIABLE".into()]));
633
634 let var2 = Variable::parse("option1|option2");
636 assert_eq!(
637 var2.env_var_names(true),
638 HashSet::from(["OPTION1_OPTION2".into(), "OPTION1".into(), "OPTION2".into()])
639 );
640 assert_eq!(var2.env_var_names(false), HashSet::from(["OPTION1_OPTION2".into()]));
641
642 let var3 = Variable::parse("my-variable:kebab:upper");
644 assert_eq!(
645 var3.env_var_names(true),
646 HashSet::from(["MY_VARIABLE_KEBAB_UPPER".into(), "MY_VARIABLE".into()])
647 );
648
649 let var4 = Variable::parse("*my-secret*");
651 assert_eq!(var4.env_var_names(true), HashSet::from(["MY_SECRET".into()]));
652
653 let var5 = Variable::parse("{my-secret}");
655 assert_eq!(var5.env_var_names(true), HashSet::from(["MY_SECRET".into()]));
656 }
657
658 #[test]
659 fn test_variable_apply_functions_to() {
660 let var_none = Variable::parse("text");
662 assert_eq!(var_none.apply_functions_to("Hello World"), "Hello World");
663
664 let var_upper = Variable::parse("text:upper");
666 assert_eq!(var_upper.apply_functions_to("Hello World"), "HELLO WORLD");
667
668 let var_kebab_upper = Variable::parse("text:kebab:upper");
670 assert_eq!(var_kebab_upper.apply_functions_to("Hello World"), "HELLO-WORLD");
671
672 let var_snake_lower = Variable::parse("text:snake:lower");
674 assert_eq!(var_snake_lower.apply_functions_to("Hello World"), "hello_world");
675 }
676
677 #[test]
678 fn test_variable_check_functions_char() {
679 let var_none = Variable::parse("text");
681 assert_eq!(var_none.check_functions_char('a'), None);
682 assert_eq!(var_none.check_functions_char(' '), None);
683
684 let var_upper = Variable::parse("text:upper");
686 assert_eq!(var_upper.check_functions_char('a'), Some("A".to_string()));
687
688 let var_lower = Variable::parse("text:lower");
690 assert_eq!(var_lower.check_functions_char('a'), None);
691
692 let var_upper_kebab = Variable::parse("text:upper:kebab");
694 assert_eq!(var_upper_kebab.check_functions_char('a'), Some("A".to_string()));
695 assert_eq!(var_upper_kebab.check_functions_char(' '), Some("-".to_string()));
696 assert_eq!(var_upper_kebab.check_functions_char('-'), None);
697 }
698
699 #[test]
700 fn test_variable_function_apply_to() {
701 assert_eq!(VariableFunction::KebabCase.apply_to("some text"), "some-text");
703 assert_eq!(VariableFunction::KebabCase.apply_to("Some Text"), "Some-Text");
704 assert_eq!(VariableFunction::KebabCase.apply_to("some_text"), "some-text");
705 assert_eq!(VariableFunction::KebabCase.apply_to("-"), "");
706 assert_eq!(VariableFunction::KebabCase.apply_to("_"), "");
707
708 assert_eq!(VariableFunction::SnakeCase.apply_to("some text"), "some_text");
710 assert_eq!(VariableFunction::SnakeCase.apply_to("Some Text"), "Some_Text");
711 assert_eq!(VariableFunction::SnakeCase.apply_to("some-text"), "some_text");
712 assert_eq!(VariableFunction::SnakeCase.apply_to("-"), "");
713 assert_eq!(VariableFunction::SnakeCase.apply_to("_"), "");
714
715 assert_eq!(VariableFunction::UpperCase.apply_to("some text"), "SOME TEXT");
717 assert_eq!(VariableFunction::UpperCase.apply_to("SomeText"), "SOMETEXT");
718
719 assert_eq!(VariableFunction::LowerCase.apply_to("SOME TEXT"), "some text");
721 assert_eq!(VariableFunction::LowerCase.apply_to("SomeText"), "sometext");
722
723 assert_eq!(VariableFunction::Urlencode.apply_to("some text"), "some%20text");
725 assert_eq!(VariableFunction::Urlencode.apply_to("Some Text"), "Some%20Text");
726 assert_eq!(VariableFunction::Urlencode.apply_to("some-text"), "some%2Dtext");
727 assert_eq!(VariableFunction::Urlencode.apply_to("some_text"), "some%5Ftext");
728 assert_eq!(
729 VariableFunction::Urlencode.apply_to("!@#$%^&*()"),
730 "%21%40%23%24%25%5E%26%2A%28%29"
731 );
732 assert_eq!(VariableFunction::Urlencode.apply_to("some%20text"), "some%20text");
733 }
734
735 #[test]
736 fn test_variable_function_check_char() {
737 assert_eq!(VariableFunction::KebabCase.check_char(' '), Some("-".to_string()));
739 assert_eq!(VariableFunction::KebabCase.check_char('_'), Some("-".to_string()));
740 assert_eq!(VariableFunction::KebabCase.check_char('-'), None);
741 assert_eq!(VariableFunction::KebabCase.check_char('A'), None);
742
743 assert_eq!(VariableFunction::SnakeCase.check_char(' '), Some("_".to_string()));
745 assert_eq!(VariableFunction::SnakeCase.check_char('-'), Some("_".to_string()));
746 assert_eq!(VariableFunction::SnakeCase.check_char('_'), None);
747 assert_eq!(VariableFunction::SnakeCase.check_char('A'), None);
748
749 assert_eq!(VariableFunction::UpperCase.check_char('a'), Some("A".to_string()));
751 assert_eq!(VariableFunction::UpperCase.check_char('A'), None);
752 assert_eq!(VariableFunction::UpperCase.check_char(' '), None);
753
754 assert_eq!(VariableFunction::LowerCase.check_char('A'), Some("a".to_string()));
756 assert_eq!(VariableFunction::LowerCase.check_char('a'), None);
757 assert_eq!(VariableFunction::LowerCase.check_char(' '), None);
758
759 assert_eq!(VariableFunction::Urlencode.check_char(' '), Some("%20".to_string()));
761 assert_eq!(VariableFunction::Urlencode.check_char('!'), Some("%21".to_string()));
762 assert_eq!(VariableFunction::Urlencode.check_char('A'), None);
763 assert_eq!(VariableFunction::Urlencode.check_char('1'), None);
764 assert_eq!(VariableFunction::Urlencode.check_char('-'), Some("%2D".to_string()));
765 assert_eq!(VariableFunction::Urlencode.check_char('_'), Some("%5F".to_string()));
766 }
767
768 #[test]
769 fn test_is_secret_variable() {
770 assert_eq!(is_secret_variable("*secret*"), Some("secret"));
772 assert_eq!(is_secret_variable("* another secret *"), Some(" another secret "));
773 assert_eq!(is_secret_variable("**"), Some(""));
774
775 assert_eq!(is_secret_variable("{secret}"), Some("secret"));
777 assert_eq!(is_secret_variable("{ another secret }"), Some(" another secret "));
778 assert_eq!(is_secret_variable("{}"), Some(""));
779
780 assert_eq!(is_secret_variable("not-secret"), None);
782 assert_eq!(is_secret_variable("*not-secret"), None);
783 assert_eq!(is_secret_variable("not-secret*"), None);
784 assert_eq!(is_secret_variable("{not-secret"), None);
785 assert_eq!(is_secret_variable("not-secret}"), None);
786 assert_eq!(is_secret_variable(""), None);
787 assert_eq!(is_secret_variable("*"), None);
788 assert_eq!(is_secret_variable("{"), None);
789 assert_eq!(is_secret_variable("}*"), None);
790 assert_eq!(is_secret_variable("*{"), None);
791 }
792}