1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
#[macro_use]
extern crate serde_derive;
extern crate regex;
extern crate structopt;

use exitfailure::ExitFailure;
use failure::{Context, ResultExt};
use glob::Pattern;
use regex::Regex;
use std::fs;
use std::path::{Path, PathBuf};
use structopt::StructOpt;

#[derive(StructOpt, Debug)]
#[structopt(name = "ficon")]
pub struct CliOption {
    /// Path to directory to check convention
    #[structopt(name = "PATH", default_value = ".", parse(from_os_str))]
    pub path: PathBuf,
}

#[derive(Deserialize)]
pub struct Config {
    default: SubConfig,
    for_patterns: Option<Vec<SubConfigByPattern>>,
}

#[derive(Deserialize)]
struct SubConfig {
    convention: String,
}

#[derive(Deserialize, Debug)]
struct SubConfigByPattern {
    pattern: String,
    convention: String,
}

pub struct Ficon {
    option: CliOption,
    config: Config,
}

impl Ficon {
    const DEFAULT_CONFIG_FILE: &'static str = "Ficon.toml";

    pub fn new() -> Result<Ficon, ExitFailure> {
        let option: CliOption = CliOption::from_args();

        let config_path = if option.path.is_dir() {
            Ok(format!(
                "{}/{}",
                option.path.display(),
                Ficon::DEFAULT_CONFIG_FILE
            ))
        } else {
            Err(Context::new(format!(
                "\"{}\" is not a directory",
                option.path.display()
            )))
        }?;

        let config = fs::read_to_string(&config_path)
            .with_context(|_| format!("Config file is missing: {}", config_path.as_str()))?;

        let config: Config = toml::from_str(config.as_str())
            .with_context(|_| "Error while parsing configuration file")?;

        Ok(Ficon { option, config })
    }

    pub fn target_dir(&self) -> &Path {
        return self.option.path.as_ref();
    }

    pub fn check(&self, path: &Path) -> Result<bool, ExitFailure> {
        let convention_str = self.config.convention_for(path);
        let reg_pattern = Regex::new(r"/(.*)/").unwrap();

        let convention_regex = match convention_str.as_str() {
            "any" => Ficon::convention_from_regex(r".*"),
            "kebab" => Ficon::convention_from_regex(r"^[a-z][a-z\-\d]*[a-z\d]$"),
            "snake" => Ficon::convention_from_regex(r"^[a-z][a-z_\d]*[a-z\d]$"),
            "upper_snake" => Ficon::convention_from_regex(r"^[A-Z][A-Z_\d]*$"),
            "camel" => Ficon::convention_from_regex(r"^[a-z][A-Za-z\d]*$"),
            "pascal" => Ficon::convention_from_regex(r"^[A-Z][A-Za-z\d]*$"),
            convention => {
                if reg_pattern.is_match(convention_str.as_str()) {
                    let convention = reg_pattern.replace(convention, "$1").to_string();
                    Regex::new(convention.as_str())
                        .with_context(|_| format!("{} is not a valid regexp", convention))
                } else {
                    Err(Context::new(format!(
                        "convention is not predefined or defined as regexp: {}",
                        convention
                    )))
                }
            }
        };

        let file_name = path
            .file_stem()
            .expect("file stem is missing")
            .to_str()
            .expect("can't cast file stem to string");

        // ignore multiple extension by default
        // TODO: make this configurable
        let file_name = file_name.split(".").next().unwrap_or("");

        let convention = convention_regex.with_context(|_| "fail to parse convention")?;

        Ok(convention.is_match(file_name))
    }

    fn convention_from_regex(pattern: &str) -> Result<Regex, Context<String>> {
        Regex::new(pattern).with_context(|_| format!("Invalid convention definition: {}", pattern))
    }
}

impl Config {
    fn convention_for(&self, path: &Path) -> String {
        let pattern_configs = &self.for_patterns;

        let empty_vec = vec![];
        let pattern_configs = pattern_configs.as_ref().map_or(&empty_vec, |e| e);

        let matched_formats: Vec<&SubConfigByPattern> = pattern_configs
            .iter()
            .filter(|conf| {
                let pattern = Pattern::new(conf.pattern.as_str()).expect("invalid glob pattern");

                pattern.matches_path(path)
            })
            .collect();

        return matched_formats
            .first()
            .map(|e| e.convention.clone())
            .unwrap_or(self.default.convention.clone());
    }
}