ficon/
lib.rs

1#[macro_use]
2extern crate serde_derive;
3extern crate regex;
4extern crate structopt;
5
6use exitfailure::ExitFailure;
7use failure::{Context, ResultExt};
8use glob::Pattern;
9use regex::Regex;
10use std::fs;
11use std::path::{Path, PathBuf};
12use structopt::StructOpt;
13
14#[derive(StructOpt, Debug)]
15#[structopt(name = "ficon")]
16pub struct CliOption {
17    /// Path to directory to check convention
18    #[structopt(name = "PATH", default_value = ".", parse(from_os_str))]
19    pub path: PathBuf,
20}
21
22#[derive(Deserialize)]
23pub struct Config {
24    default: SubConfig,
25    for_patterns: Option<Vec<SubConfigByPattern>>,
26}
27
28#[derive(Deserialize)]
29struct SubConfig {
30    convention: String,
31}
32
33#[derive(Deserialize, Debug)]
34struct SubConfigByPattern {
35    pattern: String,
36    convention: String,
37}
38
39pub struct Ficon {
40    option: CliOption,
41    config: Config,
42}
43
44impl Ficon {
45    const DEFAULT_CONFIG_FILE: &'static str = "Ficon.toml";
46
47    pub fn new() -> Result<Ficon, ExitFailure> {
48        let option: CliOption = CliOption::from_args();
49
50        let config_path = if option.path.is_dir() {
51            Ok(format!(
52                "{}/{}",
53                option.path.display(),
54                Ficon::DEFAULT_CONFIG_FILE
55            ))
56        } else {
57            Err(Context::new(format!(
58                "\"{}\" is not a directory",
59                option.path.display()
60            )))
61        }?;
62
63        let config = fs::read_to_string(&config_path)
64            .with_context(|_| format!("Config file is missing: {}", config_path.as_str()))?;
65
66        let config: Config = toml::from_str(config.as_str())
67            .with_context(|_| "Error while parsing configuration file")?;
68
69        Ok(Ficon { option, config })
70    }
71
72    pub fn target_dir(&self) -> &Path {
73        return self.option.path.as_ref();
74    }
75
76    pub fn check(&self, path: &Path) -> Result<bool, ExitFailure> {
77        let convention_str = self.config.convention_for(path);
78        let reg_pattern = Regex::new(r"/(.*)/").unwrap();
79
80        let convention_regex = match convention_str.as_str() {
81            "any" => Ficon::convention_from_regex(r".*"),
82            "kebab" => Ficon::convention_from_regex(r"^[a-z][a-z\-\d]*[a-z\d]$"),
83            "snake" => Ficon::convention_from_regex(r"^[a-z][a-z_\d]*[a-z\d]$"),
84            "upper_snake" => Ficon::convention_from_regex(r"^[A-Z][A-Z_\d]*$"),
85            "camel" => Ficon::convention_from_regex(r"^[a-z][A-Za-z\d]*$"),
86            "pascal" => Ficon::convention_from_regex(r"^[A-Z][A-Za-z\d]*$"),
87            convention => {
88                if reg_pattern.is_match(convention_str.as_str()) {
89                    let convention = reg_pattern.replace(convention, "$1").to_string();
90                    Regex::new(convention.as_str())
91                        .with_context(|_| format!("{} is not a valid regexp", convention))
92                } else {
93                    Err(Context::new(format!(
94                        "convention is not predefined or defined as regexp: {}",
95                        convention
96                    )))
97                }
98            }
99        };
100
101        let file_name = path
102            .file_stem()
103            .expect("file stem is missing")
104            .to_str()
105            .expect("can't cast file stem to string");
106
107        // ignore multiple extension by default
108        // TODO: make this configurable
109        let file_name = file_name.split(".").next().unwrap_or("");
110
111        let convention = convention_regex.with_context(|_| "fail to parse convention")?;
112
113        Ok(convention.is_match(file_name))
114    }
115
116    fn convention_from_regex(pattern: &str) -> Result<Regex, Context<String>> {
117        Regex::new(pattern).with_context(|_| format!("Invalid convention definition: {}", pattern))
118    }
119}
120
121impl Config {
122    fn convention_for(&self, path: &Path) -> String {
123        let pattern_configs = &self.for_patterns;
124
125        let empty_vec = vec![];
126        let pattern_configs = pattern_configs.as_ref().map_or(&empty_vec, |e| e);
127
128        let matched_formats: Vec<&SubConfigByPattern> = pattern_configs
129            .iter()
130            .filter(|conf| {
131                let pattern = Pattern::new(conf.pattern.as_str()).expect("invalid glob pattern");
132
133                pattern.matches_path(path)
134            })
135            .collect();
136
137        return matched_formats
138            .first()
139            .map(|e| e.convention.clone())
140            .unwrap_or(self.default.convention.clone());
141    }
142}