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 #[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 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}