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