use crate::api::models::VariableDraft;
pub fn variables_in(text: &str, secret: bool) -> Vec<VariableDraft> {
text.lines()
.filter_map(pair_in)
.map(|(key, value)| VariableDraft {
key: key.to_owned(),
value,
secret,
})
.collect()
}
fn pair_in(line: &str) -> Option<(&str, String)> {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
return None;
}
let line = line.strip_prefix("export ").unwrap_or(line);
let (key, value) = line.split_once('=')?;
let key = key.trim();
if !is_a_key(key) {
return None;
}
Some((key, unquoted(value.trim())))
}
fn is_a_key(key: &str) -> bool {
!key.is_empty()
&& !key.starts_with(|c: char| c.is_ascii_digit())
&& key.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
}
fn unquoted(value: &str) -> String {
for quote in ['"', '\''] {
if let Some(inner) = value
.strip_prefix(quote)
.and_then(|rest| rest.strip_suffix(quote))
{
return inner.to_owned();
}
}
match value.split_once(" #") {
Some((before, _)) => before.trim_end().to_owned(),
None => value.to_owned(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn only(text: &str) -> VariableDraft {
variables_in(text, false)
.pop()
.expect("the text holds exactly one variable")
}
#[test]
fn it_should_read_a_plain_assignment() {
assert_eq!(only("PORT=8080").key, "PORT");
}
#[test]
fn it_should_read_the_value_of_a_plain_assignment() {
assert_eq!(only("PORT=8080").value, "8080");
}
#[test]
fn it_should_tolerate_the_export_people_write_in_front() {
assert_eq!(only("export PORT=8080").key, "PORT");
}
#[test]
fn it_should_keep_every_equals_sign_after_the_first() {
assert_eq!(
only("DATABASE_URL=postgres://u:p@host/db?a=b").value,
"postgres://u:p@host/db?a=b",
);
}
#[test]
fn it_should_take_off_double_quotes() {
assert_eq!(only(r#"GREETING="hola mundo""#).value, "hola mundo");
}
#[test]
fn it_should_take_off_single_quotes() {
assert_eq!(only("GREETING='hola mundo'").value, "hola mundo");
}
#[test]
fn it_should_keep_a_hash_that_is_inside_quotes() {
assert_eq!(only(r#"PASSWORD="a#b""#).value, "a#b");
}
#[test]
fn it_should_drop_a_trailing_comment_from_an_unquoted_value() {
assert_eq!(only("PORT=8080 # the http one").value, "8080");
}
#[test]
fn it_should_skip_a_commented_line() {
assert!(variables_in("# PORT=8080", false).is_empty());
}
#[test]
fn it_should_skip_a_blank_line() {
assert!(variables_in("\n \n", false).is_empty());
}
#[test]
fn it_should_skip_a_line_that_is_not_an_assignment() {
assert!(variables_in("just some prose", false).is_empty());
}
#[test]
fn it_should_refuse_a_key_a_shell_would_not_export() {
assert!(variables_in("not-a-key=1", false).is_empty());
}
#[test]
fn it_should_refuse_a_key_that_starts_with_a_digit() {
assert!(variables_in("1PORT=8080", false).is_empty());
}
#[test]
fn it_should_read_every_variable_in_a_file() {
assert_eq!(variables_in("A=1\n\n# comment\nB=2\n", false).len(), 2);
}
#[test]
fn it_should_mark_them_all_secret_when_asked_to() {
assert!(variables_in("TOKEN=abc", true)[0].secret);
}
#[test]
fn it_should_leave_them_plain_when_not_asked_to() {
assert!(!variables_in("PORT=8080", false)[0].secret);
}
#[test]
fn it_should_leave_a_platform_reference_exactly_as_written() {
assert_eq!(
only("DATABASE_URL=${{ Postgres.DATABASE_URL }}").value,
"${{ Postgres.DATABASE_URL }}",
);
}
}