use std::{
collections::{BTreeMap, HashSet},
convert::Infallible,
fmt::{Display, Formatter},
mem,
str::FromStr,
};
use heck::ToShoutySnakeCase;
use itertools::Itertools;
use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, percent_decode_str, utf8_percent_encode};
use super::VariableValue;
use crate::utils::{
COMMAND_VARIABLE_REGEX, COMMAND_VARIABLE_REGEX_ALT, SplitCaptures, SplitItem, flatten_str, flatten_variable_name,
};
#[derive(Clone)]
#[cfg_attr(test, derive(Debug))]
pub struct CommandTemplate {
pub flat_root_cmd: String,
pub parts: Vec<TemplatePart>,
}
impl CommandTemplate {
pub fn parse(cmd: impl AsRef<str>, alt: bool) -> Self {
let cmd = cmd.as_ref();
let regex = if alt {
&COMMAND_VARIABLE_REGEX_ALT
} else {
&COMMAND_VARIABLE_REGEX
};
let splitter = SplitCaptures::new(regex, cmd);
let parts = splitter
.map(|e| match e {
SplitItem::Unmatched(t) => TemplatePart::Text(t.to_owned()),
SplitItem::Captured(c) => {
TemplatePart::Variable(Variable::parse(c.get(1).or(c.get(2)).unwrap().as_str()))
}
})
.collect::<Vec<_>>();
CommandTemplate {
flat_root_cmd: flatten_str(cmd.split_whitespace().next().unwrap_or(cmd)),
parts,
}
}
pub fn has_pending_variable(&self) -> bool {
self.parts.iter().any(|part| matches!(part, TemplatePart::Variable(_)))
}
pub fn previous_values_for(&self, flat_variable_name: &str) -> Option<Vec<String>> {
let values = self
.parts
.iter()
.filter_map(|part| {
if let TemplatePart::VariableValue(v, value) = part
&& v.flat_name == flat_variable_name
{
Some(value.clone())
} else {
None
}
})
.unique()
.collect::<Vec<_>>();
if values.is_empty() { None } else { Some(values) }
}
pub fn variable_context(&self) -> BTreeMap<String, String> {
self.parts
.iter()
.filter_map(|part| {
if let TemplatePart::VariableValue(v, value) = part
&& !v.secret
{
Some((v.flat_name.clone(), value.clone()))
} else {
None
}
})
.collect()
}
pub fn count_variables(&self) -> usize {
self.parts
.iter()
.filter(|part| matches!(part, TemplatePart::Variable(_) | TemplatePart::VariableValue(_, _)))
.count()
}
pub fn variable_at(&self, index: usize) -> Option<&Variable> {
self.parts
.iter()
.filter_map(|part| match part {
TemplatePart::Variable(v) | TemplatePart::VariableValue(v, _) => Some(v),
_ => None,
})
.nth(index)
}
pub fn set_variable_value(&mut self, index: usize, value: Option<String>) -> Option<String> {
self.parts
.iter_mut()
.filter(|part| matches!(part, TemplatePart::Variable(_) | TemplatePart::VariableValue(_, _)))
.nth(index)
.and_then(|part| part.set_value(value))
}
pub fn set_variable_values(&mut self, values: &[Option<String>]) {
let mut values_iter = values.iter();
for part in self.parts.iter_mut() {
if matches!(part, TemplatePart::Variable(_) | TemplatePart::VariableValue(_, _)) {
if let Some(value) = values_iter.next() {
part.set_value(value.clone());
} else {
break;
}
}
}
}
pub fn new_variable_value_for(
&self,
flat_variable_name: impl Into<String>,
value: impl Into<String>,
) -> VariableValue {
VariableValue::new(&self.flat_root_cmd, flat_variable_name, value)
}
}
impl Display for CommandTemplate {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
for part in self.parts.iter() {
write!(f, "{part}")?;
}
Ok(())
}
}
#[derive(Clone)]
#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
pub enum TemplatePart {
Text(String),
Variable(Variable),
VariableValue(Variable, String),
}
impl TemplatePart {
pub fn set_value(&mut self, value: Option<String>) -> Option<String> {
if !matches!(self, Self::Variable(_) | Self::VariableValue(_, _)) {
return None;
}
let taken_part = mem::take(self);
let (new_part, previous_value) = match taken_part {
Self::Variable(v) => (
match value {
Some(val) => Self::VariableValue(v, val),
None => Self::Variable(v),
},
None,
),
Self::VariableValue(v, old_val) => (
match value {
Some(val) => Self::VariableValue(v, val),
None => Self::Variable(v),
},
Some(old_val),
),
other => (other, None),
};
*self = new_part;
previous_value
}
}
impl Default for TemplatePart {
fn default() -> Self {
Self::Text(String::new())
}
}
impl Display for TemplatePart {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
TemplatePart::Text(t) => write!(f, "{t}"),
TemplatePart::Variable(v) => write!(f, "{{{{{}}}}}", v.display),
TemplatePart::VariableValue(_, value) => write!(f, "{value}"),
}
}
}
#[derive(Clone)]
#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
pub struct Variable {
pub display: String,
pub options: Vec<String>,
pub flat_names: Vec<String>,
pub flat_name: String,
pub functions: Vec<VariableFunction>,
pub secret: bool,
}
impl Variable {
pub fn parse(text: impl Into<String>) -> Self {
let display: String = text.into();
let (content, secret) = match is_secret_variable(&display) {
Some(inner) => (inner, true),
None => (display.as_str(), false),
};
let parts: Vec<&str> = content.split(':').collect();
let mut functions = Vec::new();
let mut boundary_index = parts.len();
if parts.len() > 1 {
for (i, part) in parts.iter().enumerate().rev() {
if let Ok(func) = VariableFunction::from_str(part) {
functions.push(func);
boundary_index = i;
} else {
break;
}
}
}
functions.reverse();
let options_str = &parts[..boundary_index].join(":");
let (options, flat_names) = if options_str.is_empty() {
(vec![], vec![])
} else {
let mut options = Vec::new();
let mut flat_names = Vec::new();
let mut seen_options = HashSet::new();
let mut seen_flat_names = HashSet::new();
for option in options_str
.split('|')
.map(|o| o.trim())
.filter(|o| !o.is_empty())
.map(String::from)
{
if seen_options.insert(option.clone()) {
let flat_name = flatten_variable_name(&option);
if seen_flat_names.insert(flat_name.clone()) {
flat_names.push(flat_name);
}
options.push(option);
}
}
(options, flat_names)
};
let flat_name = flat_names.iter().join("|");
Self {
display,
options,
flat_names,
flat_name,
functions,
secret,
}
}
pub fn env_var_names(&self, include_individual: bool) -> HashSet<String> {
let mut names = HashSet::new();
let env_var_name = self.display.to_shouty_snake_case();
if !env_var_name.trim().is_empty() && env_var_name.trim() != "PATH" {
names.insert(env_var_name.trim().to_owned());
}
let env_var_name_no_fn = self.flat_name.to_shouty_snake_case();
if !env_var_name_no_fn.trim().is_empty() && env_var_name_no_fn.trim() != "PATH" {
names.insert(env_var_name_no_fn.trim().to_owned());
}
if include_individual {
names.extend(
self.flat_names
.iter()
.map(|o| o.to_shouty_snake_case())
.filter(|o| !o.trim().is_empty())
.filter(|o| o.trim() != "PATH")
.map(|o| o.trim().to_owned()),
);
}
names
}
pub fn apply_functions_to(&self, text: impl Into<String>) -> String {
let text = text.into();
let mut result = text;
for func in self.functions.iter() {
result = func.apply_to(result);
}
result
}
pub fn check_functions_char(&self, ch: char) -> Option<String> {
let mut out: Option<String> = None;
for func in self.functions.iter() {
if let Some(ref mut out) = out {
let mut new_out = String::from("");
for ch in out.chars() {
if let Some(replacement) = func.check_char(ch) {
new_out.push_str(&replacement);
} else {
new_out.push(ch);
}
}
*out = new_out;
} else if let Some(replacement) = func.check_char(ch) {
out = Some(replacement);
}
}
out
}
}
impl FromStr for Variable {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::parse(s))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::EnumString)]
pub enum VariableFunction {
#[strum(serialize = "kebab")]
KebabCase,
#[strum(serialize = "snake")]
SnakeCase,
#[strum(serialize = "upper")]
UpperCase,
#[strum(serialize = "lower")]
LowerCase,
#[strum(serialize = "url")]
UrlEncode,
}
impl VariableFunction {
pub fn apply_to(&self, input: impl AsRef<str>) -> String {
let input = input.as_ref();
match self {
Self::KebabCase => replace_separators(input, '-'),
Self::SnakeCase => replace_separators(input, '_'),
Self::UpperCase => input.to_uppercase(),
Self::LowerCase => input.to_lowercase(),
Self::UrlEncode => idempotent_percent_encode(input, NON_ALPHANUMERIC),
}
}
pub fn check_char(&self, ch: char) -> Option<String> {
match self {
Self::KebabCase | Self::SnakeCase => {
let separator = if *self == Self::KebabCase { '-' } else { '_' };
if ch != separator && is_separator(ch) {
Some(separator.to_string())
} else {
None
}
}
Self::UpperCase => {
if ch.is_lowercase() {
Some(ch.to_uppercase().to_string())
} else {
None
}
}
Self::LowerCase => {
if ch.is_uppercase() {
Some(ch.to_lowercase().to_string())
} else {
None
}
}
Self::UrlEncode => {
if ch.is_ascii_alphanumeric() {
None
} else {
Some(idempotent_percent_encode(&ch.to_string(), NON_ALPHANUMERIC))
}
}
}
}
}
fn is_secret_variable(variable_name: &str) -> Option<&str> {
if (variable_name.starts_with('*') && variable_name.ends_with('*') && variable_name.len() > 1)
|| (variable_name.starts_with('{') && variable_name.ends_with('}'))
{
Some(&variable_name[1..variable_name.len() - 1])
} else {
None
}
}
fn is_separator(c: char) -> bool {
c == '-' || c == '_' || c.is_whitespace()
}
fn replace_separators(s: &str, separator: char) -> String {
let mut result = String::with_capacity(s.len());
let mut words = s.split(is_separator).filter(|word| !word.is_empty());
if let Some(first_word) = words.next() {
result.push_str(first_word);
}
for word in words {
result.push(separator);
result.push_str(word);
}
result
}
pub fn idempotent_percent_encode(input: &str, encode_set: &'static AsciiSet) -> String {
if let Ok(decoded) = percent_decode_str(input).decode_utf8() {
let re_encoded = utf8_percent_encode(&decoded, encode_set).to_string();
if re_encoded == input {
return re_encoded;
}
}
utf8_percent_encode(input, encode_set).to_string().to_string()
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_parse_command_with_variables() {
let cmd = CommandTemplate::parse("git commit -m {{{message}}} --author {{author:kebab}}", false);
assert_eq!(cmd.flat_root_cmd, "git");
assert_eq!(cmd.parts.len(), 4);
assert_eq!(cmd.parts[0], TemplatePart::Text("git commit -m ".into()));
assert!(matches!(cmd.parts[1], TemplatePart::Variable(_)));
assert_eq!(cmd.parts[2], TemplatePart::Text(" --author ".into()));
assert!(matches!(cmd.parts[3], TemplatePart::Variable(_)));
}
#[test]
fn test_parse_command_no_variables() {
let cmd = CommandTemplate::parse("echo 'hello world'", false);
assert_eq!(cmd.flat_root_cmd, "echo");
assert_eq!(cmd.parts.len(), 1);
assert_eq!(cmd.parts[0], TemplatePart::Text("echo 'hello world'".into()));
}
#[test]
fn test_has_pending_variable() {
let mut cmd = CommandTemplate::parse("cmd {{var1}} {{var2}}", false);
assert!(cmd.has_pending_variable());
cmd.set_variable_value(0, Some("value1".to_string()));
assert!(cmd.has_pending_variable());
cmd.set_variable_value(1, Some("value2".to_string()));
assert!(!cmd.has_pending_variable());
}
#[test]
fn test_previous_values_for() {
let mut cmd = CommandTemplate::parse("cmd {{var1}} {{var1}} {{var2}}", false);
assert_eq!(cmd.previous_values_for("var1"), None);
assert_eq!(cmd.previous_values_for("var2"), None);
cmd.set_variable_value(0, Some("val1".into()));
assert_eq!(cmd.previous_values_for("var1"), Some(vec!["val1".to_string()]));
assert_eq!(cmd.previous_values_for("var2"), None);
cmd.set_variable_value(1, Some("val1".into()));
assert_eq!(cmd.previous_values_for("var1"), Some(vec!["val1".to_string()]));
cmd.set_variable_value(1, Some("val1_other".into()));
let mut values = cmd.previous_values_for("var1").unwrap();
values.sort();
assert_eq!(values, vec!["val1".to_string(), "val1_other".to_string()]);
cmd.set_variable_value(2, Some("val2".into()));
assert_eq!(cmd.previous_values_for("var2"), Some(vec!["val2".to_string()]));
}
#[test]
fn test_variable_context() {
let mut cmd = CommandTemplate::parse("cmd {{var1}} {{{secret_var}}} {{var2}}", false);
cmd.set_variable_value(2, Some("value2".to_string()));
let context_before_secret: Vec<_> = cmd.variable_context().into_iter().collect();
assert_eq!(context_before_secret, vec![("var2".to_string(), "value2".to_string())]);
cmd.set_variable_value(1, Some("secret_value".to_string()));
let context_after_secret: Vec<_> = cmd.variable_context().into_iter().collect();
assert_eq!(context_after_secret, context_before_secret);
}
#[test]
fn test_variable_context_is_empty() {
let cmd = CommandTemplate::parse("cmd {{var1}}", false);
let context: Vec<_> = cmd.variable_context().into_iter().collect();
assert!(context.is_empty());
}
#[test]
fn test_count_variables() {
let mut cmd = CommandTemplate::parse("cmd {{var1}} middle {{var2}}", false);
assert_eq!(cmd.count_variables(), 2);
cmd.set_variable_value(0, Some("value1".to_string()));
assert_eq!(cmd.count_variables(), 2);
let cmd_no_vars = CommandTemplate::parse("cmd no-vars", false);
assert_eq!(cmd_no_vars.count_variables(), 0);
}
#[test]
fn test_variable_at() {
let cmd = CommandTemplate::parse("cmd {{var1}} middle {{var2}}", false);
let var1 = Variable::parse("var1");
let var2 = Variable::parse("var2");
assert_eq!(cmd.variable_at(0), Some(&var1));
assert_eq!(cmd.variable_at(1), Some(&var2));
assert_eq!(cmd.variable_at(2), None);
let cmd_no_vars = CommandTemplate::parse("cmd no-vars", false);
assert_eq!(cmd_no_vars.variable_at(0), None);
}
#[test]
fn test_set_variable_value() {
let mut cmd = CommandTemplate::parse("cmd {{var1}} {{var2}}", false);
let prev = cmd.set_variable_value(0, Some("val1".into()));
assert_eq!(prev, None);
let var1 = Variable::parse("var1");
assert_eq!(cmd.parts[1], TemplatePart::VariableValue(var1.clone(), "val1".into()));
let prev = cmd.set_variable_value(0, Some("new_val1".into()));
assert_eq!(prev, Some("val1".into()));
assert_eq!(
cmd.parts[1],
TemplatePart::VariableValue(var1.clone(), "new_val1".into())
);
let prev = cmd.set_variable_value(0, None);
assert_eq!(prev, Some("new_val1".into()));
assert_eq!(cmd.parts[1], TemplatePart::Variable(var1.clone()));
let prev = cmd.set_variable_value(1, Some("val2".into()));
assert_eq!(prev, None);
let var2 = Variable::parse("var2");
assert_eq!(cmd.parts[3], TemplatePart::VariableValue(var2.clone(), "val2".into()));
let prev = cmd.set_variable_value(2, Some("val3".into()));
assert_eq!(prev, None);
}
#[test]
fn test_set_variable_values_full_update() {
let mut cmd = CommandTemplate::parse("cmd {{var1}} {{var2}}", false);
let values = vec![Some("value1".to_string()), Some("value2".to_string())];
cmd.set_variable_values(&values);
let var1 = Variable::parse("var1");
let var2 = Variable::parse("var2");
assert_eq!(cmd.parts[1], TemplatePart::VariableValue(var1, "value1".into()));
assert_eq!(cmd.parts[3], TemplatePart::VariableValue(var2, "value2".into()));
assert!(!cmd.has_pending_variable());
}
#[test]
fn test_set_variable_values_partial_update() {
let mut cmd = CommandTemplate::parse("cmd {{var1}} {{var2}} {{var3}}", false);
let values = vec![Some("value1".to_string()), Some("value2".to_string())];
cmd.set_variable_values(&values);
let var1 = Variable::parse("var1");
let var2 = Variable::parse("var2");
let var3 = Variable::parse("var3");
assert_eq!(cmd.parts[1], TemplatePart::VariableValue(var1, "value1".into()));
assert_eq!(cmd.parts[3], TemplatePart::VariableValue(var2, "value2".into()));
assert_eq!(cmd.parts[5], TemplatePart::Variable(var3));
assert!(cmd.has_pending_variable());
}
#[test]
fn test_set_variable_values_with_none_to_unset() {
let mut cmd = CommandTemplate::parse("cmd {{var1}} {{var2}}", false);
cmd.set_variable_values(&[Some("val1".into()), Some("val2".into())]);
assert!(!cmd.has_pending_variable());
let values = vec![None, Some("new_val2".to_string())];
cmd.set_variable_values(&values);
let var1 = Variable::parse("var1");
let var2 = Variable::parse("var2");
assert_eq!(cmd.parts[1], TemplatePart::Variable(var1));
assert_eq!(cmd.parts[3], TemplatePart::VariableValue(var2, "new_val2".into()));
assert!(cmd.has_pending_variable());
}
#[test]
fn test_set_variable_values_empty_slice() {
let mut cmd = CommandTemplate::parse("cmd {{var1}} {{var2}}", false);
let original_parts = cmd.parts.clone();
cmd.set_variable_values(&[]);
assert_eq!(cmd.parts, original_parts);
}
#[test]
fn test_set_variable_values_more_values_than_variables() {
let mut cmd = CommandTemplate::parse("cmd {{var1}}", false);
let values = vec![Some("value1".to_string()), Some("ignored".to_string())];
cmd.set_variable_values(&values);
let var1 = Variable::parse("var1");
assert_eq!(cmd.parts[1], TemplatePart::VariableValue(var1, "value1".into()));
assert!(!cmd.has_pending_variable());
}
#[test]
fn test_template_part_set_value() {
let var = Variable::parse("v");
let mut part1 = TemplatePart::Variable(var.clone());
let prev1 = part1.set_value(Some("value".into()));
assert_eq!(prev1, None);
assert_eq!(part1, TemplatePart::VariableValue(var.clone(), "value".into()));
let mut part2 = TemplatePart::VariableValue(var.clone(), "old".into());
let prev2 = part2.set_value(Some("new".into()));
assert_eq!(prev2, Some("old".into()));
assert_eq!(part2, TemplatePart::VariableValue(var.clone(), "new".into()));
let mut part3 = TemplatePart::VariableValue(var.clone(), "value".into());
let prev3 = part3.set_value(None);
assert_eq!(prev3, Some("value".into()));
assert_eq!(part3, TemplatePart::Variable(var.clone()));
let mut part4 = TemplatePart::Variable(var.clone());
let prev4 = part4.set_value(None);
assert_eq!(prev4, None);
assert_eq!(part4, TemplatePart::Variable(var.clone()));
let mut part5 = TemplatePart::Text("text".into());
let prev5 = part5.set_value(Some("value".into()));
assert_eq!(prev5, None);
assert_eq!(part5, TemplatePart::Text("text".into()));
}
#[test]
fn test_parse_simple_variable() {
let variable = Variable::parse("my_variable");
assert_eq!(
variable,
Variable {
display: "my_variable".into(),
options: vec!["my_variable".into()],
flat_names: vec!["my_variable".into()],
flat_name: "my_variable".into(),
functions: vec![],
secret: false,
}
);
}
#[test]
fn test_parse_secret_variable() {
let variable = Variable::parse("{my_secret}");
assert_eq!(
variable,
Variable {
display: "{my_secret}".into(),
options: vec!["my_secret".into()],
flat_names: vec!["my_secret".into()],
flat_name: "my_secret".into(),
functions: vec![],
secret: true,
}
);
}
#[test]
fn test_parse_variable_with_multiple_options() {
let variable = Variable::parse("Option 1 | option 1 | Option 2 | Option 2 | Option 3");
assert_eq!(
variable,
Variable {
display: "Option 1 | option 1 | Option 2 | Option 2 | Option 3".into(),
options: vec![
"Option 1".into(),
"option 1".into(),
"Option 2".into(),
"Option 3".into()
],
flat_names: vec!["option 1".into(), "option 2".into(), "option 3".into()],
flat_name: "option 1|option 2|option 3".into(),
functions: vec![],
secret: false,
}
);
}
#[test]
fn test_parse_variable_with_single_function() {
let variable = Variable::parse("my_variable:kebab");
assert_eq!(
variable,
Variable {
display: "my_variable:kebab".into(),
options: vec!["my_variable".into()],
flat_names: vec!["my_variable".into()],
flat_name: "my_variable".into(),
functions: vec![VariableFunction::KebabCase],
secret: false,
}
);
}
#[test]
fn test_parse_variable_with_multiple_functions() {
let variable = Variable::parse("my_variable:snake:upper");
assert_eq!(
variable,
Variable {
display: "my_variable:snake:upper".into(),
options: vec!["my_variable".into()],
flat_names: vec!["my_variable".into()],
flat_name: "my_variable".into(),
functions: vec![VariableFunction::SnakeCase, VariableFunction::UpperCase],
secret: false,
}
);
}
#[test]
fn test_parse_variable_with_options_and_functions() {
let variable = Variable::parse("opt1|opt2:lower:kebab");
assert_eq!(
variable,
Variable {
display: "opt1|opt2:lower:kebab".into(),
options: vec!["opt1".into(), "opt2".into()],
flat_names: vec!["opt1".into(), "opt2".into()],
flat_name: "opt1|opt2".into(),
functions: vec![VariableFunction::LowerCase, VariableFunction::KebabCase],
secret: false,
}
);
}
#[test]
fn test_parse_variable_with_colon_in_options() {
let variable = Variable::parse("key:value:kebab");
assert_eq!(
variable,
Variable {
display: "key:value:kebab".into(),
options: vec!["key:value".into()],
flat_names: vec!["key:value".into()],
flat_name: "key:value".into(),
functions: vec![VariableFunction::KebabCase],
secret: false,
}
);
}
#[test]
fn test_parse_variable_with_only_functions() {
let variable = Variable::parse(":snake");
assert_eq!(
variable,
Variable {
display: ":snake".into(),
options: vec![],
flat_names: vec![],
flat_name: "".into(),
functions: vec![VariableFunction::SnakeCase],
secret: false,
}
);
}
#[test]
fn test_parse_variable_that_is_a_function_name() {
let variable = Variable::parse("kebab");
assert_eq!(
variable,
Variable {
display: "kebab".into(),
options: vec!["kebab".into()],
flat_names: vec!["kebab".into()],
flat_name: "kebab".into(),
functions: vec![],
secret: false,
}
);
}
#[test]
fn test_variable_env_var_names() {
let var1 = Variable::parse("my-variable");
assert_eq!(var1.env_var_names(true), HashSet::from(["MY_VARIABLE".into()]));
let var2 = Variable::parse("option1|option2");
assert_eq!(
var2.env_var_names(true),
HashSet::from(["OPTION1_OPTION2".into(), "OPTION1".into(), "OPTION2".into()])
);
assert_eq!(var2.env_var_names(false), HashSet::from(["OPTION1_OPTION2".into()]));
let var3 = Variable::parse("my-variable:kebab:upper");
assert_eq!(
var3.env_var_names(true),
HashSet::from(["MY_VARIABLE_KEBAB_UPPER".into(), "MY_VARIABLE".into()])
);
let var4 = Variable::parse("*my-secret*");
assert_eq!(var4.env_var_names(true), HashSet::from(["MY_SECRET".into()]));
let var5 = Variable::parse("{my-secret}");
assert_eq!(var5.env_var_names(true), HashSet::from(["MY_SECRET".into()]));
}
#[test]
fn test_variable_apply_functions_to() {
let var_none = Variable::parse("text");
assert_eq!(var_none.apply_functions_to("Hello World"), "Hello World");
let var_upper = Variable::parse("text:upper");
assert_eq!(var_upper.apply_functions_to("Hello World"), "HELLO WORLD");
let var_kebab_upper = Variable::parse("text:kebab:upper");
assert_eq!(var_kebab_upper.apply_functions_to("Hello World"), "HELLO-WORLD");
let var_snake_lower = Variable::parse("text:snake:lower");
assert_eq!(var_snake_lower.apply_functions_to("Hello World"), "hello_world");
}
#[test]
fn test_variable_check_functions_char() {
let var_none = Variable::parse("text");
assert_eq!(var_none.check_functions_char('a'), None);
assert_eq!(var_none.check_functions_char(' '), None);
let var_upper = Variable::parse("text:upper");
assert_eq!(var_upper.check_functions_char('a'), Some("A".to_string()));
let var_lower = Variable::parse("text:lower");
assert_eq!(var_lower.check_functions_char('a'), None);
let var_upper_kebab = Variable::parse("text:upper:kebab");
assert_eq!(var_upper_kebab.check_functions_char('a'), Some("A".to_string()));
assert_eq!(var_upper_kebab.check_functions_char(' '), Some("-".to_string()));
assert_eq!(var_upper_kebab.check_functions_char('-'), None);
}
#[test]
fn test_variable_function_apply_to() {
assert_eq!(VariableFunction::KebabCase.apply_to("some text"), "some-text");
assert_eq!(VariableFunction::KebabCase.apply_to("Some Text"), "Some-Text");
assert_eq!(VariableFunction::KebabCase.apply_to("some_text"), "some-text");
assert_eq!(VariableFunction::KebabCase.apply_to("-"), "");
assert_eq!(VariableFunction::KebabCase.apply_to("_"), "");
assert_eq!(VariableFunction::SnakeCase.apply_to("some text"), "some_text");
assert_eq!(VariableFunction::SnakeCase.apply_to("Some Text"), "Some_Text");
assert_eq!(VariableFunction::SnakeCase.apply_to("some-text"), "some_text");
assert_eq!(VariableFunction::SnakeCase.apply_to("-"), "");
assert_eq!(VariableFunction::SnakeCase.apply_to("_"), "");
assert_eq!(VariableFunction::UpperCase.apply_to("some text"), "SOME TEXT");
assert_eq!(VariableFunction::UpperCase.apply_to("SomeText"), "SOMETEXT");
assert_eq!(VariableFunction::LowerCase.apply_to("SOME TEXT"), "some text");
assert_eq!(VariableFunction::LowerCase.apply_to("SomeText"), "sometext");
assert_eq!(VariableFunction::UrlEncode.apply_to("some text"), "some%20text");
assert_eq!(VariableFunction::UrlEncode.apply_to("Some Text"), "Some%20Text");
assert_eq!(VariableFunction::UrlEncode.apply_to("some-text"), "some%2Dtext");
assert_eq!(VariableFunction::UrlEncode.apply_to("some_text"), "some%5Ftext");
assert_eq!(
VariableFunction::UrlEncode.apply_to("!@#$%^&*()"),
"%21%40%23%24%25%5E%26%2A%28%29"
);
assert_eq!(VariableFunction::UrlEncode.apply_to("some%20text"), "some%20text");
}
#[test]
fn test_variable_function_check_char() {
assert_eq!(VariableFunction::KebabCase.check_char(' '), Some("-".to_string()));
assert_eq!(VariableFunction::KebabCase.check_char('_'), Some("-".to_string()));
assert_eq!(VariableFunction::KebabCase.check_char('-'), None);
assert_eq!(VariableFunction::KebabCase.check_char('A'), None);
assert_eq!(VariableFunction::SnakeCase.check_char(' '), Some("_".to_string()));
assert_eq!(VariableFunction::SnakeCase.check_char('-'), Some("_".to_string()));
assert_eq!(VariableFunction::SnakeCase.check_char('_'), None);
assert_eq!(VariableFunction::SnakeCase.check_char('A'), None);
assert_eq!(VariableFunction::UpperCase.check_char('a'), Some("A".to_string()));
assert_eq!(VariableFunction::UpperCase.check_char('A'), None);
assert_eq!(VariableFunction::UpperCase.check_char(' '), None);
assert_eq!(VariableFunction::LowerCase.check_char('A'), Some("a".to_string()));
assert_eq!(VariableFunction::LowerCase.check_char('a'), None);
assert_eq!(VariableFunction::LowerCase.check_char(' '), None);
assert_eq!(VariableFunction::UrlEncode.check_char(' '), Some("%20".to_string()));
assert_eq!(VariableFunction::UrlEncode.check_char('!'), Some("%21".to_string()));
assert_eq!(VariableFunction::UrlEncode.check_char('A'), None);
assert_eq!(VariableFunction::UrlEncode.check_char('1'), None);
assert_eq!(VariableFunction::UrlEncode.check_char('-'), Some("%2D".to_string()));
assert_eq!(VariableFunction::UrlEncode.check_char('_'), Some("%5F".to_string()));
}
#[test]
fn test_is_secret_variable() {
assert_eq!(is_secret_variable("*secret*"), Some("secret"));
assert_eq!(is_secret_variable("* another secret *"), Some(" another secret "));
assert_eq!(is_secret_variable("**"), Some(""));
assert_eq!(is_secret_variable("{secret}"), Some("secret"));
assert_eq!(is_secret_variable("{ another secret }"), Some(" another secret "));
assert_eq!(is_secret_variable("{}"), Some(""));
assert_eq!(is_secret_variable("not-secret"), None);
assert_eq!(is_secret_variable("*not-secret"), None);
assert_eq!(is_secret_variable("not-secret*"), None);
assert_eq!(is_secret_variable("{not-secret"), None);
assert_eq!(is_secret_variable("not-secret}"), None);
assert_eq!(is_secret_variable(""), None);
assert_eq!(is_secret_variable("*"), None);
assert_eq!(is_secret_variable("{"), None);
assert_eq!(is_secret_variable("}*"), None);
assert_eq!(is_secret_variable("*{"), None);
}
}