easy_arg 0.2.6

EasyArg read variables from command line arguments/system envrioment/.env files
Documentation
use std::{
    collections::HashMap,
    env,
    fs::File,
    io::{self, BufRead},
    path::Path,
};

use regex::Regex;

use crate::utils::{get_bool, get_string, get_vec, must_get_bool, must_get_string};

#[derive(Debug)]
enum LineType {
    /// key = val
    Data(String, String),
    /// [title]
    Section(String),
    /// [[title]]
    SubSection(String),
    /// ---
    SectionEnd,
    // # comment...
    Comment,
    Empty,
    Invalid,
}

#[derive(Debug)]
pub struct Section {
    pub key: String,
    pub parent: Option<String>,
    pub extends: Vec<String>,
    pub data: HashMap<String, String>,
}

impl Clone for Section {
    fn clone(&self) -> Self {
        Section { key: self.key.clone(), parent: self.parent.clone(), data: self.data.clone(), extends: vec![] }
    }
}

impl Section {
    pub fn new(key: String, parent: Option<String>) -> Section {
        Section { key, parent, data: HashMap::new(), extends: vec![] }
    }

    pub fn get_string(&self, key: &str) -> String {
        get_string(&self.data, key)
    }

    pub fn get_bool(&self, key: &str) -> bool {
        get_bool(&self.data, key)
    }

    pub fn get_vec(&self, key: &str) -> Vec<String> {
        get_vec(&self.data, key)
    }

    pub fn must_get_string(&self, key: &str) -> String {
        must_get_string(&self.data, key)
    }

    pub fn must_get_bool(&self, key: &str) -> bool {
        must_get_bool(&self.data, key)
    }
}

/// read key/value from .env file
pub fn load_env(file: &str) -> (HashMap<String, String>, Vec<Section>) {
    let mut config: HashMap<String, String> = HashMap::new();
    if !std::path::Path::new(file).exists() {
        return (config, vec![]);
    }

    let match_title = Regex::new(r"(?m)([\w-]+)").unwrap();
    let mut sections: Vec<Section> = vec![];
    let mut current_section: Option<Section> = None;
    let mut current_sec_title: Option<String> = None;

    if let Ok(lines) = read_lines(file) {
        for line in lines {
            if let Ok(line) = line {
                let line = handle_line(line);
                match line {
                    LineType::Data(key, val) => {
                        if let Some(sec) = current_section.as_mut() {
                            sec.data.insert(key, val);
                        } else {
                            config.insert(key, val);
                        }
                    }
                    LineType::Section(title) => {
                        if let Some(sec) = current_section {
                            sections.push(sec);
                        }
                        let mut key = title;
                        let mut extends: Vec<&str> = vec![];
                        let key2 = key.clone();
                        if key.contains("+") {
                            let matches: Vec<&str> = match_title.find_iter(key2.as_str()).map(|f| f.as_str()).collect();
                            if matches.len() > 0 {
                                key = matches[0].to_string();
                                extends.extend_from_slice(&matches[1..])
                            }
                        }
                        let mut section = Section::new(key.clone(), None);
                        section.extends = extends.iter().map(|f| f.to_string()).collect();
                        current_section = Some(section);
                        current_sec_title = Some(key);
                    }
                    LineType::SubSection(title) => {
                        if let Some(sec) = current_section {
                            sections.push(sec);
                        }
                        let mut key = title;
                        let mut extends: Vec<&str> = vec![];
                        let key2 = key.clone();
                        if key.contains("+") {
                            let matches: Vec<&str> = match_title.find_iter(key2.as_str()).map(|f| f.as_str()).collect();
                            if matches.len() > 0 {
                                key = matches[0].to_string();
                                extends.extend_from_slice(&matches[1..])
                            }
                        }
                        let mut section = Section::new(key.clone(), current_sec_title.clone());
                        section.extends = extends.iter().map(|f| f.to_string()).collect();
                        current_section = Some(section)
                    }
                    LineType::SectionEnd => {
                        if let Some(sec) = current_section {
                            sections.push(sec);
                            current_section = None;
                        } else if current_sec_title.is_some() {
                            current_sec_title = None;
                        }
                    }
                    _ => {}
                }
            }
        }
        if let Some(sec) = current_section {
            sections.push(sec);
        }
    }
    let section_copy = sections.clone();

    for sec in sections.iter_mut() {
        for ext in sec.extends.iter() {
            for sec2 in section_copy.iter() {
                if sec2.key == *ext {
                    sec.data.extend(sec2.data.clone());
                }
            }
        }
    }
    (config, sections)
}

fn handle_line(line: String) -> LineType {
    let not_empty = Regex::new(r"[\w\-]").expect("not empty");
    let is_comment = Regex::new(r"^\s*?#").expect("is comment");
    if line.len() == 0 || !not_empty.is_match(&line) {
        return LineType::Empty;
    }
    let line = line.replace("\"", "");
    if is_comment.is_match(&line) {
        let is_section = Regex::new(r"(?m)^#[ \t]*\[{1}([\w+-]+)").unwrap();
        let is_sub_section = Regex::new(r"(?m)^#[ \t]*\[{2}([\w+-]+)").unwrap();
        if line.starts_with("#---") {
            return LineType::SectionEnd;
        } else if is_section.is_match(&line) {
            let title = extract_title(is_section, line);
            if let Some(title) = title {
                return LineType::Section(title);
            }
            return LineType::Invalid;
        } else if is_sub_section.is_match(&line) {
            let title = extract_title(is_sub_section, line);
            if let Some(title) = title {
                return LineType::SubSection(title);
            }
            return LineType::Invalid;
        } else {
            return LineType::Comment;
        }
    }
    if line.contains("=") {
        let arr: Vec<&str> = line.split("=").collect();
        let key = arr[0].to_string().trim().to_string();
        if !not_empty.is_match(&key) {
            return LineType::Invalid;
        }
        let mut value = arr[1].trim().to_string();
        if value.contains("#") {
            let arr: Vec<&str> = value.split("#").collect();
            value = arr[0].trim().to_string();
        }
        if value.len() == 0 || !not_empty.is_match(&value) {
            value = String::new();
        } else if value.contains("$") {
            let r = Regex::new(r"(?m)\$\{(\w+)\}").expect("re");
            for cap in r.captures_iter(&value.clone()) {
                let env_key = &cap[1];
                let val = env::var_os(env_key);
                if let Some(val) = val {
                    let val2 = val.to_str().expect("os str to str");
                    let repl_key = format!("${{{}}}", env_key);
                    value = value.replace(&repl_key, val2);
                } else {
                    println!("cannot find env {}", env_key);
                }
            }
        }
        return LineType::Data(key, value);
    } else {
        return LineType::Data(line.trim().to_string(), "true".to_string());
    }
}

fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
where
    P: AsRef<Path>,
{
    let file = File::open(filename)?;
    Ok(io::BufReader::new(file).lines())
}

pub fn extract_title(re: Regex, line: String) -> Option<String> {
    let result = re.captures(&line);
    match result {
        Some(val) => {
            return Some(val[1].to_string());
        }
        None => {}
    }
    return None;
}

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

    #[test]
    pub fn test_load_env() {
        let result = load_env("./.easy_arg.env");
        println!("{:?}", result.0);
        for sec in result.1.into_iter() {
            println!("{:?}", sec);
        }
    }

    #[test]
    pub fn test_title() {
        let re = Regex::new(r"(?m)^#[ \t]*\[{1}([\w+-]+)").unwrap();
        let result = extract_title(re, "#[abc_-+ddd]".to_string()).unwrap();
        assert_eq!(result, "abc_-+ddd".to_string());
        let re = Regex::new(r"(?m)^#[ \t]*\[{2}([\w+-]+)").unwrap();
        let result = extract_title(re, "# [[efg_-+ccc]]".to_string()).unwrap();
        assert_eq!(result, "efg_-+ccc".to_string())
    }

    #[test]
    pub fn test_captures() {
        let re = Regex::new(r"(?m)([\w]+)").unwrap();
        let a = "#[abc+xyz+123]";
        re.find_iter(a).for_each(|f| {
            println!("result {:?}", f.as_str());
        })
    }
}