depguard_settings/
resolve.rs1use 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(&profile)?;
32
33 let mut effective = presets::preset(&profile);
34
35 if let Some(scope_s) = overrides.scope.clone().or(cfg.scope.clone()) {
37 effective.scope = parse_scope(&scope_s)?;
38 }
39
40 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 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 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 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}