Skip to main content

depguard_settings/
resolve.rs

1use crate::{ValidationError, model::DepguardConfigV1, presets};
2use depguard_domain_core::policy::{CheckPolicy, EffectiveConfig, FailOn, Scope};
3use depguard_types::Severity;
4use globset::Glob;
5
6#[derive(Clone, Debug, Default)]
7pub struct Overrides {
8    pub profile: Option<String>,
9    pub scope: Option<String>,
10    pub max_findings: Option<u32>,
11    pub baseline: Option<String>,
12}
13
14#[derive(Clone, Debug)]
15pub struct ResolvedConfig {
16    pub effective: EffectiveConfig,
17    pub baseline_path: Option<String>,
18}
19
20pub fn resolve_config(
21    cfg: DepguardConfigV1,
22    overrides: Overrides,
23) -> anyhow::Result<ResolvedConfig> {
24    let profile = overrides
25        .profile
26        .clone()
27        .or(cfg.profile.clone())
28        .unwrap_or_else(|| "strict".to_string());
29
30    // Validate profile
31    validate_profile(&profile)?;
32
33    let mut effective = presets::preset(&profile);
34
35    // Scope
36    if let Some(scope_s) = overrides.scope.clone().or(cfg.scope.clone()) {
37        effective.scope = parse_scope(&scope_s)?;
38    }
39
40    // max findings
41    if let Some(mf) = overrides.max_findings.or(cfg.max_findings) {
42        if mf == 0 {
43            return Err(anyhow::Error::new(ValidationError::invalid_max_findings(
44                mf,
45            )));
46        }
47        effective.max_findings = mf as usize;
48    }
49
50    // per-check overrides
51    for (check_id, cc) in cfg.checks.iter() {
52        let entry = effective
53            .checks
54            .entry(check_id.clone())
55            .or_insert_with(CheckPolicy::disabled);
56
57        if let Some(enabled) = cc.enabled {
58            entry.enabled = enabled;
59        }
60        if let Some(sev) = cc.severity.as_deref() {
61            entry.severity = parse_severity(check_id, sev)?;
62        }
63        if !cc.allow.is_empty() {
64            validate_allowlist(check_id, &cc.allow)?;
65            entry.allow = cc.allow.clone();
66        }
67        if let Some(ignore_publish_false) = cc.ignore_publish_false {
68            // ignore_publish_false is only valid for deps.path_requires_version
69            if check_id != "deps.path_requires_version" {
70                return Err(anyhow::Error::new(
71                    ValidationError::ignore_publish_false_not_supported(check_id),
72                ));
73            }
74            entry.ignore_publish_false = ignore_publish_false;
75        }
76    }
77
78    // fail_on override from config
79    if let Some(fail_on_s) = cfg.fail_on.as_deref() {
80        effective.fail_on = parse_fail_on(fail_on_s)?;
81    }
82
83    let baseline_path = overrides.baseline.or(cfg.baseline);
84
85    Ok(ResolvedConfig {
86        effective,
87        baseline_path,
88    })
89}
90
91fn validate_allowlist(check_id: &str, patterns: &[String]) -> anyhow::Result<()> {
92    for pattern in patterns {
93        Glob::new(pattern).map_err(|e| {
94            anyhow::Error::new(ValidationError::invalid_allow_glob(
95                check_id,
96                pattern,
97                &e.to_string(),
98            ))
99        })?;
100    }
101    Ok(())
102}
103
104fn validate_profile(profile: &str) -> anyhow::Result<()> {
105    match profile {
106        "strict" | "warn" | "team" | "compat" | "oss" => Ok(()),
107        other => Err(anyhow::Error::new(ValidationError::unknown_profile(other))),
108    }
109}
110
111fn parse_scope(v: &str) -> anyhow::Result<Scope> {
112    match v {
113        "repo" => Ok(Scope::Repo),
114        "diff" => Ok(Scope::Diff),
115        other => Err(anyhow::Error::new(ValidationError::unknown_scope(other))),
116    }
117}
118
119fn parse_severity(check_id: &str, v: &str) -> anyhow::Result<Severity> {
120    match v {
121        "info" => Ok(Severity::Info),
122        "warning" | "warn" => Ok(Severity::Warning),
123        "error" => Ok(Severity::Error),
124        other => Err(anyhow::Error::new(ValidationError::unknown_severity(
125            check_id, other,
126        ))),
127    }
128}
129
130fn parse_fail_on(v: &str) -> anyhow::Result<FailOn> {
131    match v {
132        "error" => Ok(FailOn::Error),
133        "warning" | "warn" => Ok(FailOn::Warning),
134        other => Err(anyhow::Error::new(ValidationError::unknown_fail_on(other))),
135    }
136}