envcast 1.0.0

Short, clear description of what the project does
Documentation
use regex::{Match, Regex};
use std::collections::HashMap;

enum ValueMatch<'h> {
    SingleQuoted(Match<'h>),
    DoubleQuoted(Match<'h>),
    Backticks(Match<'h>),
    Unquoted(Match<'h>),
}

pub fn parse_dotenv_vars(text: &str) -> HashMap<String, String> {
    let pattern = concat!(
        r#"(?m)^\s*"#,           // Multiline mode with optional whitespace
        r#"(?:export\s+)?"#,     // Optional prefix export
        r#"([\w.-]+)"#,          // Group 1: variable name
        r#"(?:\s*=\s*?|:\s+?)"#, // Delimiter `=` or `:`
        r#"(?:"#,                // Optional value groups start
        r#"'((?:\\'|[^'])*)'|"#, // Group 2: '...' (single quotes)
        r#""((?:\\"|[^"])*)"|"#, // Group 3: "..." (double quotes)
        r#"`((?:\\`|[^`])*)`|"#, // Group 4: `...` (backticks)
        r#"([^#\r\n]*)"#,        // Group 5: unquoted
        r#")?"#,                 // Optional value groups end
        r#"\s*(?:#.*)?$"#        // Optional comment
    );
    let re = Regex::new(pattern).unwrap();
    let mut vars = HashMap::new();

    for caps in re.captures_iter(text) {
        if let Some(key) = caps.get(1) {
            let value_match = if let Some(val) = caps.get(2) {
                ValueMatch::SingleQuoted(val)
            } else if let Some(val) = caps.get(3) {
                ValueMatch::DoubleQuoted(val)
            } else if let Some(val) = caps.get(4) {
                ValueMatch::Backticks(val)
            } else {
                ValueMatch::Unquoted(caps.get(5).unwrap())
            };
            let value = match value_match {
                ValueMatch::SingleQuoted(value) => value.as_str().into(),
                ValueMatch::DoubleQuoted(value) => value.as_str().replace("\\n", "\n"),
                ValueMatch::Backticks(value) => value.as_str().into(),
                ValueMatch::Unquoted(value) => value.as_str().trim().into(),
            };
            vars.insert(key.as_str().into(), value);
        }
    }

    vars
}

#[cfg(test)]
mod tests {
    use super::*;

    static DOTENV_FILE_CONTENT: &str = concat!(
        "BASIC=basic\n",
        "\n",
        "# previous line intentionally left blank\n",
        "AFTER_LINE=after_line\n",
        "EMPTY=\n",
        "EMPTY_SINGLE_QUOTES=''\n",
        "EMPTY_DOUBLE_QUOTES=\"\"\n",
        "EMPTY_BACKTICKS=``\n",
        "EMPTY_INLINE_COMMENTS= # comment\n",
        "SINGLE_QUOTES='single_quotes'\n",
        "SINGLE_QUOTES_SPACED='    single quotes    '\n",
        "DOUBLE_QUOTES=\"double_quotes\"\n",
        "DOUBLE_QUOTES_SPACED=\"    double quotes    \"\n",
        "DOUBLE_QUOTES_INSIDE_SINGLE='double \"quotes\" work inside single quotes'\n",
        "DOUBLE_QUOTES_WITH_NO_SPACE_BRACKET=\"{ port: $MONGOLAB_PORT}\"\n",
        "SINGLE_QUOTES_INSIDE_DOUBLE=\"single 'quotes' work inside double quotes\"\n",
        "BACKTICKS_INSIDE_SINGLE='`backticks` work inside single quotes'\n",
        "BACKTICKS_INSIDE_DOUBLE=\"`backticks` work inside double quotes\"\n",
        "BACKTICKS=`backticks`\n",
        "BACKTICKS_SPACED=`    backticks    `\n",
        "DOUBLE_QUOTES_INSIDE_BACKTICKS=`double \"quotes\" work inside backticks`\n",
        "SINGLE_QUOTES_INSIDE_BACKTICKS=`single 'quotes' work inside backticks`\n",
        "DOUBLE_AND_SINGLE_QUOTES_INSIDE_BACKTICKS=`double \"quotes\" and single 'quotes' work inside backticks`\n",
        "EXPAND_NEWLINES=\"expand\\nnew\\nlines\"\n",
        "DONT_EXPAND_UNQUOTED=dontexpand\\nnewlines\n",
        "DONT_EXPAND_SQUOTED='dontexpand\\nnewlines'\n",
        "# COMMENTS=work\n",
        "INLINE_COMMENTS=inline comments # work #very #well\n",
        "INLINE_COMMENTS_SINGLE_QUOTES='inline comments outside of #singlequotes' # work\n",
        "INLINE_COMMENTS_DOUBLE_QUOTES=\"inline comments outside of #doublequotes\" # work\n",
        "INLINE_COMMENTS_BACKTICKS=`inline comments outside of #backticks` # work\n",
        "INLINE_COMMENTS_SPACE=inline comments start with a#number sign. no space required.\n",
        "EQUAL_SIGNS=equals==\n",
        "RETAIN_INNER_QUOTES={\"foo\": \"bar\"}\n",
        "RETAIN_INNER_QUOTES_AS_STRING='{\"foo\": \"bar\"}'\n",
        "RETAIN_INNER_QUOTES_AS_BACKTICKS=`{\"foo\": \"bar's\"}`\n",
        "TRIM_SPACE_FROM_UNQUOTED=    some spaced out string\n",
        "USERNAME=therealnerdybeast@example.tld\n",
        "    SPACED_KEY = parsed\n",
        "    EMPTY_SPACED_KEY =\n",
        "\n",
        "MULTI_DOUBLE_QUOTED=\"THIS\n",
        "IS\n",
        "A\n",
        "MULTILINE\n",
        "STRING\"\n",
        "\n",
        "MULTI_SINGLE_QUOTED='THIS\n",
        "IS\n",
        "A\n",
        "MULTILINE\n",
        "STRING'\n",
        "\n",
        "MULTI_BACKTICKED=`THIS\n",
        "IS\n",
        "A\n",
        "\"MULTILINE'S\"\n",
        "STRING`\n",
        "\n",
        "MULTI_PEM_DOUBLE_QUOTED=\"-----BEGIN PUBLIC KEY-----\n",
        "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNl1tL3QjKp3DZWM0T3u\n",
        "LgGJQwu9WqyzHKZ6WIA5T+7zPjO1L8l3S8k8YzBrfH4mqWOD1GBI8Yjq2L1ac3Y/\n",
        "bTdfHN8CmQr2iDJC0C6zY8YV93oZB3x0zC/LPbRYpF8f6OqX1lZj5vo2zJZy4fI/\n",
        "kKcI5jHYc8VJq+KCuRZrvn+3V+KuL9tF9v8ZgjF2PZbU+LsCy5Yqg1M8f5Jp5f6V\n",
        "u4QuUoobAgMBAAE=\n",
        "-----END PUBLIC KEY-----\"\n",
    );

    #[test]
    fn test_parse_dotenv_vars() {
        let result = parse_dotenv_vars(DOTENV_FILE_CONTENT);
        assert_eq!(result["BASIC"], "basic");
        assert_eq!(result["AFTER_LINE"], "after_line");
        assert_eq!(result["EMPTY"], "");
        assert_eq!(result["EMPTY_SINGLE_QUOTES"], "");
        assert_eq!(result["EMPTY_DOUBLE_QUOTES"], "");
        assert_eq!(result["EMPTY_BACKTICKS"], "");
        assert_eq!(result["EMPTY_INLINE_COMMENTS"], "");
        assert_eq!(result["SINGLE_QUOTES"], "single_quotes");
        assert_eq!(result["SINGLE_QUOTES_SPACED"], "    single quotes    ");
        assert_eq!(result["DOUBLE_QUOTES"], "double_quotes");
        assert_eq!(result["DOUBLE_QUOTES_SPACED"], "    double quotes    ");
        assert_eq!(
            result["DOUBLE_QUOTES_INSIDE_SINGLE"],
            "double \"quotes\" work inside single quotes"
        );
        assert_eq!(
            result["DOUBLE_QUOTES_WITH_NO_SPACE_BRACKET"],
            "{ port: $MONGOLAB_PORT}"
        );
        assert_eq!(
            result["SINGLE_QUOTES_INSIDE_DOUBLE"],
            "single 'quotes' work inside double quotes"
        );
        assert_eq!(
            result["BACKTICKS_INSIDE_SINGLE"],
            "`backticks` work inside single quotes"
        );
        assert_eq!(
            result["BACKTICKS_INSIDE_DOUBLE"],
            "`backticks` work inside double quotes"
        );
        assert_eq!(result["BACKTICKS"], "backticks");
        assert_eq!(result["BACKTICKS_SPACED"], "    backticks    ");
        assert_eq!(
            result["DOUBLE_QUOTES_INSIDE_BACKTICKS"],
            "double \"quotes\" work inside backticks"
        );
        assert_eq!(
            result["SINGLE_QUOTES_INSIDE_BACKTICKS"],
            "single 'quotes' work inside backticks"
        );
        assert_eq!(
            result["DOUBLE_AND_SINGLE_QUOTES_INSIDE_BACKTICKS"],
            "double \"quotes\" and single 'quotes' work inside backticks"
        );
        assert_eq!(result["EXPAND_NEWLINES"], "expand\nnew\nlines");
        assert_eq!(result["DONT_EXPAND_UNQUOTED"], "dontexpand\\nnewlines");
        assert_eq!(result["DONT_EXPAND_SQUOTED"], "dontexpand\\nnewlines");
        assert_eq!(result["INLINE_COMMENTS"], "inline comments");
        assert_eq!(
            result["INLINE_COMMENTS_SINGLE_QUOTES"],
            "inline comments outside of #singlequotes"
        );
        assert_eq!(
            result["INLINE_COMMENTS_DOUBLE_QUOTES"],
            "inline comments outside of #doublequotes"
        );
        assert_eq!(
            result["INLINE_COMMENTS_BACKTICKS"],
            "inline comments outside of #backticks"
        );
        assert_eq!(
            result["INLINE_COMMENTS_SPACE"],
            "inline comments start with a"
        );
        assert_eq!(result["EQUAL_SIGNS"], "equals==");
        assert_eq!(result["RETAIN_INNER_QUOTES"], "{\"foo\": \"bar\"}");
        assert_eq!(
            result["RETAIN_INNER_QUOTES_AS_STRING"],
            "{\"foo\": \"bar\"}"
        );
        assert_eq!(
            result["RETAIN_INNER_QUOTES_AS_BACKTICKS"],
            "{\"foo\": \"bar's\"}"
        );
        assert_eq!(result["TRIM_SPACE_FROM_UNQUOTED"], "some spaced out string");
        assert_eq!(result["USERNAME"], "therealnerdybeast@example.tld");
        assert_eq!(result["SPACED_KEY"], "parsed");
        assert_eq!(result["EMPTY_SPACED_KEY"], "");
        assert_eq!(
            result["MULTI_DOUBLE_QUOTED"],
            "THIS\nIS\nA\nMULTILINE\nSTRING"
        );
        assert_eq!(
            result["MULTI_SINGLE_QUOTED"],
            "THIS\nIS\nA\nMULTILINE\nSTRING"
        );
        assert_eq!(
            result["MULTI_BACKTICKED"],
            "THIS\nIS\nA\n\"MULTILINE'S\"\nSTRING"
        );
        assert_eq!(
            result["MULTI_PEM_DOUBLE_QUOTED"],
            concat!(
                "-----BEGIN PUBLIC KEY-----\n",
                "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNl1tL3QjKp3DZWM0T3u\n",
                "LgGJQwu9WqyzHKZ6WIA5T+7zPjO1L8l3S8k8YzBrfH4mqWOD1GBI8Yjq2L1ac3Y/\n",
                "bTdfHN8CmQr2iDJC0C6zY8YV93oZB3x0zC/LPbRYpF8f6OqX1lZj5vo2zJZy4fI/\n",
                "kKcI5jHYc8VJq+KCuRZrvn+3V+KuL9tF9v8ZgjF2PZbU+LsCy5Yqg1M8f5Jp5f6V\n",
                "u4QuUoobAgMBAAE=\n",
                "-----END PUBLIC KEY-----",
            )
        );
        assert_eq!(result.len(), 41);
    }
}