crab_hooks/
yml_parser.rs

1use glob::Pattern;
2use std::{
3    fmt,
4    fs::File,
5    path::{Path, PathBuf},
6};
7use which::which;
8
9#[derive(Debug)]
10pub struct ValidationError {
11    pub hook_name: String,
12    pub field: String,
13    pub problem: String,
14}
15
16impl fmt::Display for ValidationError {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        write!(
19            f,
20            "hook `{}`: field `{}` — {}",
21            self.hook_name, self.field, self.problem
22        )
23    }
24}
25
26use crate::git_hook::GitHook;
27
28pub fn read_file(config_file: PathBuf) -> Result<Vec<GitHook>, Box<dyn std::error::Error>> {
29    let f: File;
30    if Path::new(&config_file).exists() {
31        f = std::fs::File::open(config_file)?;
32    } else if Path::new("./config.yml").exists() {
33        f = std::fs::File::open("./config.yml")?;
34    } else {
35        return Err(format!(
36            "Cannot locate a config.yml file, please make one here: {:?}",
37            config_file
38        )
39        .into());
40    }
41
42    let hooks: Vec<GitHook> = serde_yaml::from_reader(f)?;
43    Ok(hooks)
44}
45
46pub fn test_config(config_file: PathBuf) -> Result<(), Box<dyn std::error::Error>> {
47    let hooks = read_file(config_file)?;
48
49    let mut errors = Vec::new();
50
51    for hook in hooks {
52        // --- name
53        if hook.name.trim().is_empty() {
54            errors.push(ValidationError {
55                hook_name: hook.name.clone(),
56                field: "name".into(),
57                problem: "must not be empty".into(),
58            });
59        }
60
61        // --- command.cmd must be non‐empty
62        let cmd = hook.command.cmd.trim();
63        if cmd.is_empty() {
64            errors.push(ValidationError {
65                hook_name: hook.name.clone(),
66                field: "command.cmd".into(),
67                problem: "must not be empty".into(),
68            });
69        } else {
70            // --- check existence without executing
71            if let Some(dir) = &hook.command.directory {
72                // Build the path to <directory>/<cmd>
73                let candidate = dir.join(cmd);
74
75                // Try finding it globally on PATH as well
76                let found_globally = which(cmd).is_ok();
77
78                if !candidate.exists() && !found_globally {
79                    errors.push(ValidationError {
80                        hook_name: hook.name.clone(),
81                        field: "command.cmd".into(),
82                        problem: format!(
83                            "could not find executable `{}` in directory {:?} or on PATH",
84                            cmd, dir
85                        ),
86                    });
87                }
88            } else {
89                // No directory specified → must find it globally
90                if which(cmd).is_err() {
91                    errors.push(ValidationError {
92                        hook_name: hook.name.clone(),
93                        field: "command.cmd".into(),
94                        problem: format!("could not locate `{}` on PATH", cmd),
95                    });
96                }
97            }
98        }
99
100        // --- directory
101        if let Some(dir) = &hook.command.directory {
102            if dir.as_os_str().is_empty() {
103                errors.push(ValidationError {
104                    hook_name: hook.name.clone(),
105                    field: "command.directory".into(),
106                    problem: "path is empty".into(),
107                });
108            }
109        }
110
111        // --- glob patterns
112        if hook.glob_pattern.is_empty() {
113            errors.push(ValidationError {
114                hook_name: hook.name.clone(),
115                field: "glob_pattern".into(),
116                problem: "must contain at least one pattern".into(),
117            });
118        } else {
119            for pat in &hook.glob_pattern {
120                if let Err(e) = Pattern::new(pat) {
121                    errors.push(ValidationError {
122                        hook_name: hook.name.clone(),
123                        field: "glob_pattern".into(),
124                        problem: format!("invalid glob `{}`: {}", pat, e),
125                    });
126                }
127            }
128        }
129    }
130
131    if errors.is_empty() {
132        Ok(())
133    } else {
134        for e in errors {
135            eprintln!("Config error: {}", e);
136        }
137        Err("Config error".into())
138    }
139}
140
141pub fn display_hooks(config_file: PathBuf) {
142    let hooks_result = read_file(config_file);
143    match hooks_result {
144        Ok(hooks) => {
145            for hook in hooks.iter() {
146                println!("{}", hook)
147            }
148        }
149        Err(e) => println!("Some error occured in finding the config file: [{}]", e),
150    }
151}