projclean 0.1.0

Project Cleaner
Documentation
use anyhow::{anyhow, Error, Result};
use regex::Regex;
use std::{
    collections::{HashMap, HashSet},
    str::FromStr,
};

const BUILTIN_PROJECTS: &str = include_str!("default.csv");

#[derive(Debug, Default)]
pub struct Config {
    pub projects: Vec<Project>,
}

impl Config {
    pub fn find_project(&self, name: &str) -> Option<&Project> {
        self.projects.iter().find(|project| {
            project
                .check
                .as_ref()
                .map(|check| check.as_str() == name)
                .unwrap_or(true)
        })
    }
    pub fn match_patch<'a, 'b>(
        &'a self,
        matches: &mut HashMap<&'a str, (HashSet<&'b str>, HashSet<&'b str>)>,
        name: &'b str,
    ) {
        for project in &self.projects {
            let (purge_matches, check_matches) = matches.entry(&project.id).or_default();
            if project.test_purge(name) {
                purge_matches.insert(name);
            }
            if project.test_check(name) {
                check_matches.insert(name);
            }
        }
    }

    pub fn is_empty_projects(&self) -> bool {
        self.projects.is_empty()
    }

    pub fn is_project_no_check(&self, id: &str) -> bool {
        if let Some(project) = self
            .projects
            .iter()
            .find(|project| project.id.as_str() == id)
        {
            project.check.is_none()
        } else {
            false
        }
    }

    pub fn get_project_name(&self, id: &str) -> Option<String> {
        if let Some(project) = self
            .projects
            .iter()
            .find(|project| project.id.as_str() == id)
        {
            project.name.clone()
        } else {
            None
        }
    }

    pub fn add_default_projects(&mut self) {
        self.add_projects_from_file(BUILTIN_PROJECTS)
            .expect("broken builtin config file");
    }

    pub fn add_projects_from_file(&mut self, content: &str) -> Result<()> {
        for (index, line) in content.lines().enumerate() {
            let line = line.trim();
            if line.is_empty() {
                continue;
            }
            self.add_project(line)
                .map_err(|_| anyhow!("Invalid project value '{}' at line {}", line, index + 1))?;
        }
        Ok(())
    }

    pub fn add_project(&mut self, value: &str) -> Result<()> {
        let project: Project = value.parse()?;
        self.projects.push(project);
        Ok(())
    }

    pub fn list_projects(&self) -> Result<()> {
        for project in &self.projects {
            println!("{}", project.id);
        }
        Ok(())
    }
}

#[derive(Debug)]
pub struct Project {
    id: String,
    purge: Regex,
    check: Option<Regex>,
    name: Option<String>,
}

impl Project {
    pub fn get_id(&self) -> &str {
        &self.id
    }

    pub fn test_purge(&self, name: &str) -> bool {
        self.purge.is_match(name)
    }

    pub fn test_check(&self, name: &str) -> bool {
        match self.check.as_ref() {
            Some(check) => check.is_match(name),
            None => false,
        }
    }
}

impl FromStr for Project {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let parts: Vec<&str> = s.split(';').collect();
        let (purge, check, name) = match parts.len() {
            1 => (parts[0].trim(), "", ""),
            2 => (parts[0].trim(), parts[1].trim(), ""),
            3 => (parts[0].trim(), parts[1].trim(), parts[2].trim()),
            _ => ("", "", ""),
        };
        let err = || anyhow!("Invalid project value '{}'", s);
        if purge.is_empty() {
            return Err(err());
        }
        Ok(Project {
            id: s.to_string(),
            purge: to_regex(purge).map_err(|_| err())?,
            check: if check.is_empty() {
                None
            } else {
                let check = to_regex(check).map_err(|_| err())?;
                Some(check)
            },
            name: if name.is_empty() {
                None
            } else {
                Some(name.to_string())
            },
        })
    }
}

fn to_regex(value: &str) -> Result<Regex> {
    let re = if value
        .chars()
        .all(|v| v.is_alphanumeric() || v == '.' || v == '-' || v == '_')
    {
        format!("^{}$", value.replace('.', "\\."))
    } else {
        value.to_string()
    };
    Regex::new(&re).map_err(|_| anyhow!("Invalid regex value '{}'", value))
}

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

    #[test]
    fn test_project() {
        let project: Project = "target".parse().unwrap();
        assert!(project.test_purge("target"));
        assert!(!project.test_purge("-target"));
        assert!(!project.test_purge("target-"));
        assert!(!project.test_purge("Target"));

        let project: Project = "^(Debug|Release)$;\\.sln$".parse().unwrap();
        assert!(project.test_purge("Debug"));
        assert!(!project.test_purge("Debug-"));
        assert!(!project.test_purge("-Debug"));
        assert!(project.test_check("App.sln"));
    }
}