vbump 0.5.1

Bumps the version of a project, creating the appropiate git tags
Documentation
use std::path::{Path, PathBuf};
use std::fs::{File, read_dir};
use std::io::{BufReader, BufRead};

use anyhow::Result;
use regex::Regex;

pub const PYTHON_VERSION_RE: &str = r#"VERSION\s*=\s*["'](?P<version>[0-9.^"']+)["']"#;

/// Which kind of project is this?
#[derive(Debug, PartialEq, Eq)]
pub enum ProjectType {
    Rust(PathBuf),
    Python(PathBuf),
    Javascript(PathBuf),
    Nofile,
}

fn is_rust<T: AsRef<Path>>(dir: T) -> Option<PathBuf> {
    let cargo_toml = dir.as_ref().join("Cargo.toml");

    cargo_toml.is_file().then_some(cargo_toml)
}

fn declares_version_python(file_path: &Path) -> Result<bool> {
    if !file_path.is_file() {
        return Ok(false);
    }

    let file = File::open(file_path)?;
    let buf_reader = BufReader::new(file);

    let version_re = Regex::new(PYTHON_VERSION_RE).unwrap();

    for line in buf_reader.lines().filter_map(|l| l.ok()) {
        if version_re.is_match(&line) {
            return Ok(true);
        }
    }

    Ok(false)
}

fn is_python<T: AsRef<Path>>(dir: T) -> Result<Option<PathBuf>> {
    let pyproject = dir.as_ref().join("pyproject.toml");

    if !pyproject.is_file() {
        return Ok(None);
    }

    for entry in read_dir(dir)?.filter_map(|e| e.ok()).map(|e| e.path()).filter(|p| p.is_dir()) {
        let version_txt = entry.join("__init__.py");

        if declares_version_python(&version_txt)? {
            return Ok(Some(version_txt));
        }
    }

    Ok(None)
}

fn is_javascript<T: AsRef<Path>>(dir: T) -> Option<PathBuf> {
    let package_json = dir.as_ref().join("package.json");

    package_json.is_file().then_some(package_json)
}

pub fn detect_project_type<T: AsRef<Path>>(dir: T) -> anyhow::Result<ProjectType> {
    if let Some(cargo_toml) = is_rust(dir.as_ref()) {
        return Ok(ProjectType::Rust(cargo_toml));
    }

    if let Some(version_file) = is_python(dir.as_ref())? {
        return Ok(ProjectType::Python(version_file));
    }

    if let Some(package_json) = is_javascript(dir.as_ref()) {
        return Ok(ProjectType::Javascript(package_json));
    }

    Ok(ProjectType::Nofile)
}

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

    #[test]
    fn directories_are_properly_detected() {
        assert_eq!(detect_project_type("./assets/test/rust_dir").unwrap(), ProjectType::Rust("./assets/test/rust_dir/Cargo.toml".into()));
        assert_eq!(detect_project_type("./assets/test/python_dir").unwrap(), ProjectType::Python("./assets/test/python_dir/module/__init__.py".into()));
        assert_eq!(detect_project_type("./assets/test/javascript_dir").unwrap(), ProjectType::Javascript("./assets/test/javascript_dir/package.json".into()));
    }
}