Skip to main content

dockerfile_roast/
config.rs

1/// Project-level configuration loaded from `droast.toml`.
2///
3/// droast works great with zero configuration — this file is purely optional
4/// and exists for teams that want to commit project-level defaults into their
5/// repo (e.g. in CI/CD setups) rather than repeat flags on every invocation.
6///
7/// Discovery: droast searches for `droast.toml` starting from the current
8/// working directory and walking up until it finds the file, reaches a `.git`
9/// directory, or hits the filesystem root — whichever comes first.
10///
11/// Merge order (highest wins): CLI flag > droast.toml > built-in default.
12/// The one exception is `skip`: CLI and config are **unioned** so that the
13/// config can establish a project baseline without preventing developers from
14/// suppressing additional rules on the command line.
15
16use anyhow::Context;
17use serde::Deserialize;
18use std::path::Path;
19
20#[derive(Debug, Default, Deserialize)]
21#[serde(rename_all = "kebab-case", deny_unknown_fields)]
22pub struct DroastConfig {
23    /// Rule IDs to skip (merged with --skip on CLI).
24    pub skip: Option<Vec<String>>,
25
26    /// Minimum severity to report: "info", "warning", or "error".
27    pub min_severity: Option<String>,
28
29    /// Suppress roast messages; show technical descriptions only.
30    pub no_roast: Option<bool>,
31
32    /// Never exit with code 1 (advisory / non-blocking mode).
33    pub no_fail: Option<bool>,
34
35    /// Output format: "terminal", "json", "github", or "compact".
36    pub format: Option<String>,
37}
38
39impl DroastConfig {
40    /// Search for and load `droast.toml`, returning `Default` if none found.
41    pub fn load() -> Self {
42        if let Some(path) = Self::find() {
43            Self::load_from(&path).unwrap_or_default()
44        } else {
45            Self::default()
46        }
47    }
48
49    /// Walk up from cwd looking for `droast.toml`.
50    /// Stops at a `.git` boundary or the filesystem root.
51    fn find() -> Option<std::path::PathBuf> {
52        let cwd = std::env::current_dir().ok()?;
53        let mut dir: &Path = &cwd;
54        loop {
55            let candidate = dir.join("droast.toml");
56            if candidate.is_file() {
57                return Some(candidate);
58            }
59            // Stop at repository root so we don't cross project boundaries.
60            if dir.join(".git").exists() {
61                break;
62            }
63            match dir.parent() {
64                Some(parent) => dir = parent,
65                None => break,
66            }
67        }
68        None
69    }
70
71    /// Load configuration from an explicit path.
72    pub fn load_from(path: &Path) -> anyhow::Result<Self> {
73        let content = std::fs::read_to_string(path)
74            .with_context(|| format!("Failed to read config file '{}'", path.display()))?;
75        let cfg: DroastConfig = toml::from_str(&content)
76            .map_err(|e| anyhow::anyhow!("Invalid config file '{}': {e}", path.display()))?;
77        Ok(cfg)
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::DroastConfig;
84
85    #[test]
86    fn loads_config_from_explicit_path() {
87        let path = std::env::temp_dir().join(format!(
88            "droast-explicit-config-{}.toml",
89            std::process::id()
90        ));
91        std::fs::write(&path, "skip = [\"DF001\"]\nno-roast = true\n").unwrap();
92
93        let config = DroastConfig::load_from(&path).unwrap();
94
95        assert_eq!(config.skip.unwrap(), ["DF001"]);
96        assert_eq!(config.no_roast, Some(true));
97        std::fs::remove_file(path).unwrap();
98    }
99
100    #[test]
101    fn explicit_config_reports_invalid_toml() {
102        let path = std::env::temp_dir().join(format!(
103            "droast-invalid-config-{}.toml",
104            std::process::id()
105        ));
106        std::fs::write(&path, "skip = [\n").unwrap();
107
108        let error = DroastConfig::load_from(&path).unwrap_err().to_string();
109
110        assert!(error.contains("Invalid config file"));
111        std::fs::remove_file(path).unwrap();
112    }
113}