envvar-parser 0.1.0

A simple library for parsing environment variables
Documentation
use std::error::Error;
use std::fmt::Debug;
use std::result::Result;

pub struct SyntaxError {
    pos: usize,
    msg: String,
}

impl std::fmt::Display for SyntaxError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Syntax error at {}: {}", self.pos, self.msg)
    }
}

impl std::fmt::Debug for SyntaxError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Syntax error at {}: {}", self.pos, self.msg)
    }
}

impl Error for SyntaxError {}

#[derive(Debug, Clone)]
pub struct Span {
    _start: usize,
    _end: usize,
}

#[derive(Debug)]
pub enum Token {
    ROOT(Vec<Token>, Option<Span>),
    Comment(String, Option<Span>),
    Variable(String, String, Option<Span>),
}

impl Token {
    pub fn create_root<T>(tokens: T) -> Token
    where
        T: IntoIterator<Item = Token>,
    {
        Token::ROOT(tokens.into_iter().collect(), None)
    }

    pub fn create_comment<T>(comment: T) -> Token
    where
        T: ToString,
    {
        Token::Comment(comment.to_string(), None)
    }

    pub fn create_variable<T, U>(name: T, value: U) -> Token
    where
        T: ToString,
        U: ToString,
    {
        Token::Variable(name.to_string(), value.to_string(), None)
    }

    fn append(&mut self, token: Token) {
        match self {
            Token::ROOT(tokens, _) => tokens.push(token),
            _ => {}
        }
    }

    pub fn parse<C: AsRef<[u8]>>(content: C) -> Result<Token, Box<dyn Error>> {
        let mut root_token = Token::ROOT(Vec::new(), None);
        let content_ref = content.as_ref();

        let mut key_characters: Vec<u8> = vec![];

        key_characters.append(&mut (b'a'..b'z').collect::<Vec<u8>>());
        key_characters.append(&mut (b'A'..b'Z').collect::<Vec<u8>>());
        key_characters.append(&mut (b'0'..b'9').collect::<Vec<u8>>());
        key_characters.push(b'_');

        let mut pos_current = 0;
        let mut acumulator_type = AcumulatorKind::ROOT;
        let mut collector_variable_key = String::new();
        let mut collector_variable_value = String::new();
        let mut collector_comment = String::new();

        loop {
            if pos_current >= content_ref.len() {
                break;
            }
            let char = content_ref[pos_current];
            pos_current += 1;

            match acumulator_type {
                AcumulatorKind::ROOT => match char {
                    b'#' => {
                        acumulator_type = AcumulatorKind::COMMENT;
                        continue;
                    }
                    b'\n' | b'\t' | b' ' | b'\r' => {
                        continue;
                    }
                    char if key_characters.contains(&char) => {
                        acumulator_type = AcumulatorKind::VariableKey;
                        collector_variable_key = String::from_utf8(vec![char]).unwrap();
                        continue;
                    }
                    _ => {
                        return Err(Box::new(SyntaxError {
                            pos: pos_current,
                            msg: "Unexpected character".to_string(),
                        }));
                    }
                },
                AcumulatorKind::VariableValue(AcumulatorVariableValueKind::StartValue) => {
                    match char {
                        b' ' | b'\t' | b'\r' | b'\n' => {
                            continue;
                        }
                        b'"' => {
                            acumulator_type = AcumulatorKind::VariableValue(
                                AcumulatorVariableValueKind::StringDoubleQuote,
                            );
                            collector_variable_value = String::new();
                            continue;
                        }
                        b'\'' => {
                            acumulator_type = AcumulatorKind::VariableValue(
                                AcumulatorVariableValueKind::StringSimpleQuote,
                            );
                            collector_variable_value = String::new();
                            continue;
                        }
                        char if char != b' ' => {
                            acumulator_type = AcumulatorKind::VariableValue(
                                AcumulatorVariableValueKind::StringWithoutQuotes,
                            );
                            collector_variable_value = String::from(char as char);
                            continue;
                        }
                        _ => {
                            return Err(Box::new(SyntaxError {
                                pos: pos_current,
                                msg: "Unexpected character".to_string(),
                            }));
                        }
                    }
                }
                AcumulatorKind::VariableValue(AcumulatorVariableValueKind::StringWithoutQuotes) => {
                    match char {
                        b'\n' => {
                            acumulator_type = AcumulatorKind::ROOT;
                            root_token.append(Token::Variable(
                                collector_variable_key.clone(),
                                collector_variable_value.clone().trim().to_string(),
                                None,
                            ));
                            continue;
                        }
                        b'#' => {
                            acumulator_type = AcumulatorKind::COMMENT;
                            root_token.append(Token::Variable(
                                collector_variable_key.clone(),
                                collector_variable_value.clone().trim().to_string(),
                                None,
                            ));
                            continue;
                        }
                        _ => {
                            collector_variable_value.push(char as char);
                            continue;
                        }
                    }
                }
                AcumulatorKind::VariableValue(AcumulatorVariableValueKind::StringSimpleQuote) => {
                    match char {
                        b'\'' if content_ref[pos_current - 2] != b'\\' => {
                            acumulator_type = AcumulatorKind::ROOT;
                            root_token.append(Token::Variable(
                                collector_variable_key.clone(),
                                collector_variable_value.replace("\\'", "'").clone(),
                                None,
                            ));
                            continue;
                        }
                        _ => {
                            collector_variable_value.push(char as char);
                            continue;
                        }
                    }
                }
                AcumulatorKind::VariableValue(AcumulatorVariableValueKind::StringDoubleQuote) => {
                    match char {
                        b'\"' if content_ref[pos_current - 2] != b'\\' => {
                            acumulator_type = AcumulatorKind::ROOT;
                            root_token.append(Token::Variable(
                                collector_variable_key.clone(),
                                collector_variable_value.replace("\\\"", "\"").clone(),
                                None,
                            ));
                            continue;
                        }
                        _ => {
                            collector_variable_value.push(char as char);
                            continue;
                        }
                    }
                }

                AcumulatorKind::VariableEqual => match char {
                    b' ' | b'\t' => {
                        continue;
                    }
                    b'=' => {
                        acumulator_type =
                            AcumulatorKind::VariableValue(AcumulatorVariableValueKind::StartValue);
                        continue;
                    }
                    _ => {
                        return Err(Box::new(SyntaxError {
                            pos: pos_current,
                            msg: "Unexpected character".to_string(),
                        }));
                    }
                },
                AcumulatorKind::VariableKey => match char {
                    b' ' | b'\t' => {
                        acumulator_type = AcumulatorKind::VariableEqual;
                        continue;
                    }
                    b'=' => {
                        acumulator_type =
                            AcumulatorKind::VariableValue(AcumulatorVariableValueKind::StartValue);
                        continue;
                    }
                    char if key_characters.contains(&char) => {
                        collector_variable_key.push(char as char);
                        continue;
                    }
                    _ => {
                        return Err(Box::new(SyntaxError {
                            pos: pos_current,
                            msg: "Unexpected character".to_string(),
                        }));
                    }
                },
                AcumulatorKind::COMMENT => match char {
                    b'\n' => {
                        acumulator_type = AcumulatorKind::ROOT;
                        root_token.append(Token::Comment(
                            collector_comment.clone().trim().to_string(),
                            None,
                        ));
                        continue;
                    }
                    _ => {
                        collector_comment.push(char as char);
                        continue;
                    }
                },
            }
        }

        Ok(root_token)
    }

    pub fn serialize(token: Token) -> Result<String, Box<String>> {
        let tokens = match token {
            Token::ROOT(tokens, _) => tokens,
            _ => return Err(Box::new("Invalid token require a ROOT token".to_string())),
        };

        let mut serialized_string = String::new();

        for token in tokens {
            match token {
                Token::Variable(key, value, _) => {
                    let next_string = value.replace("'", "\\'");

                    serialized_string.push_str(&format!("{}='{}'\n", key, next_string));
                }
                Token::Comment(comment, _) => {
                    serialized_string.push_str(&format!("# {}\n", comment));
                }
                _ => {}
            }
        }

        Ok(serialized_string)
    }
}

#[derive(Debug)]
pub struct EnvMap {}

enum AcumulatorVariableValueKind {
    StartValue,
    StringDoubleQuote,
    StringWithoutQuotes,
    StringSimpleQuote,
}

enum AcumulatorKind {
    ROOT,
    COMMENT,
    VariableKey,
    VariableEqual,
    VariableValue(AcumulatorVariableValueKind),
}

impl Default for AcumulatorKind {
    fn default() -> Self {
        Self::ROOT
    }
}

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

    #[test]
    fn parse_from_binary() {
        let payload = b"
            # Comment
            key = value
            KEY = 'val\\'ue' # inline comment
            KEY2=\"VALUE2\"
            KEY3 = ABC ASDF    # inline comment
        ";

        let envs = Token::parse(payload).unwrap();

        match envs {
            Token::ROOT(tokens, _) => {
                match &tokens[0] {
                    Token::Comment(comment, _) => assert_eq!(comment, "Comment"),
                    _ => panic!("Unexpected token"),
                }

                match &tokens[1] {
                    Token::Variable(key, value, _) => {
                        assert_eq!(key, "key");
                        assert_eq!(value, "value");
                    }
                    _ => panic!("Unexpected token"),
                }

                match &tokens[2] {
                    Token::Variable(key, value, _) => {
                        assert_eq!(key, "KEY");
                        assert_eq!(value, "val'ue");
                    }
                    _ => panic!("Unexpected token"),
                }

                match &tokens[4] {
                    Token::Variable(key, value, _) => {
                        assert_eq!(key, "KEY2");
                        assert_eq!(value, "VALUE2");
                    }
                    _ => panic!("Unexpected token"),
                }

                match &tokens[5] {
                    Token::Variable(key, value, _) => {
                        assert_eq!(key, "KEY3");
                        assert_eq!(value, "ABC ASDF");
                    }
                    _ => panic!("Unexpected token"),
                }
            }
            _ => panic!("Unexpected token"),
        }
    }

    #[test]
    fn serialize_to_binary() {
        let payload = Token::create_root([
            Token::create_comment("Comment"),
            Token::create_variable("KEY1", "value"),
            Token::create_variable("KEY2", "VALUE2"),
            Token::create_variable("KEY3", "ABC ASDF"),
        ]);

        let body_payload = Token::serialize(payload).unwrap();

        assert_eq!(
            "# Comment\nKEY1='value'\nKEY2='VALUE2'\nKEY3='ABC ASDF'\n",
            body_payload
        );
    }
}