dnf_repofile/validate.rs
1//! Validation engine for checking DNF repository configuration consistency.
2//!
3//! Provides [`ValidationReport`] as the top-level result type, containing
4//! separate lists of errors and warnings. Each finding is a [`ValidationIssue`]
5//! with a severity level ([`IssueLevel`]), a location ([`IssueLocation`]), an
6//! optional field name, and a human-readable message.
7//!
8//! # Current validation rules
9//!
10//! - **Error**: repo missing URL source (no `baseurl`, `mirrorlist`, or `metalink`)
11//! - **Warning**: both `baseurl` and `mirrorlist`/`metalink` are set
12//! - **Warning**: `gpgkey` is set but neither `gpgcheck` nor `repo_gpgcheck` is enabled
13
14use crate::mainconfig::MainConfig;
15use crate::repo::Repo;
16use crate::types::{DnfBool, RepoId};
17
18/// Report containing validation errors and warnings.
19///
20/// Use [`is_ok()`](ValidationReport::is_ok) to check if the configuration is
21/// valid (no errors). Warnings alone do not indicate invalidity.
22///
23/// # Examples
24///
25/// ```
26/// use dnf_repofile::{Repo, RepoId, ValidationReport};
27///
28/// // A repo with no URL source is invalid
29/// let repo = Repo::new(RepoId::try_new("test").unwrap());
30/// let report = repo.validate();
31/// assert!(!report.is_ok());
32/// assert_eq!(report.errors.len(), 1);
33/// ```
34#[derive(Debug, Clone)]
35pub struct ValidationReport {
36 /// Issues classified as errors (configuration is invalid).
37 pub errors: Vec<ValidationIssue>,
38 /// Issues classified as warnings (advisory, non-fatal).
39 pub warnings: Vec<ValidationIssue>,
40}
41
42/// A single validation finding with severity level and location.
43///
44/// Each issue identifies where the problem was found, its severity, the
45/// specific field (if applicable), and a human-readable message.
46#[derive(Debug, Clone)]
47pub struct ValidationIssue {
48 /// Whether this is an error or a warning.
49 pub level: IssueLevel,
50 /// Where the issue was found (file, repo, or main section).
51 pub location: IssueLocation,
52 /// The specific option field name (e.g., `"baseurl"`, `"gpgkey"`), if applicable.
53 pub field: Option<String>,
54 /// A human-readable description of the issue.
55 pub message: String,
56}
57
58/// Severity level for a validation issue.
59///
60/// - [`Error`](IssueLevel::Error) — configuration is invalid and should not be used.
61/// - [`Warning`](IssueLevel::Warning) — advisory notice; configuration may still work.
62#[non_exhaustive]
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum IssueLevel {
65 /// A hard error: the configuration is invalid.
66 Error,
67 /// A soft warning: the configuration may still work but is suspicious.
68 Warning,
69}
70
71/// Identifies where a validation issue was found.
72#[non_exhaustive]
73#[derive(Debug, Clone)]
74pub enum IssueLocation {
75 /// The issue is in a specific `.repo` file on disk.
76 File(String),
77 /// The issue is in a specific repository section.
78 Repo(RepoId),
79 /// The issue is in the `[main]` section.
80 Main,
81}
82
83impl Default for ValidationReport {
84 fn default() -> Self {
85 Self::new()
86 }
87}
88
89impl ValidationReport {
90 /// Create a new empty validation report.
91 pub fn new() -> Self {
92 ValidationReport {
93 errors: Vec::new(),
94 warnings: Vec::new(),
95 }
96 }
97
98 /// Returns `true` if there are no errors (warnings are ignored).
99 ///
100 /// A report with only warnings is still considered valid.
101 ///
102 /// # Examples
103 ///
104 /// ```
105 /// use dnf_repofile::ValidationReport;
106 ///
107 /// let report = ValidationReport::new();
108 /// assert!(report.is_ok());
109 /// ```
110 #[must_use]
111 pub fn is_ok(&self) -> bool {
112 self.errors.is_empty()
113 }
114
115 /// Returns `true` if there are any issues (errors or warnings).
116 #[must_use]
117 pub fn has_issues(&self) -> bool {
118 !self.errors.is_empty() || !self.warnings.is_empty()
119 }
120}
121
122impl Repo {
123 /// Validate a single repository's configuration.
124 ///
125 /// Checks:
126 ///
127 /// - At least one URL source is present (`baseurl`, `mirrorlist`, or `metalink`).
128 /// - `baseurl` is not set alongside `mirrorlist` or `metalink` (warns of
129 /// potential ambiguity).
130 /// - `gpgkey` is set without `gpgcheck` or `repo_gpgcheck` being enabled (warns).
131 #[must_use]
132 pub fn validate(&self) -> ValidationReport {
133 let mut r = ValidationReport::new();
134
135 if self.baseurl.is_empty() && self.mirrorlist.is_none() && self.metalink.is_none() {
136 let issue = ValidationIssue {
137 level: IssueLevel::Error,
138 location: IssueLocation::Repo(self.id.clone()),
139 field: Some("baseurl".into()),
140 message:
141 "repo must have at least one URL source (baseurl, mirrorlist, or metalink)"
142 .into(),
143 };
144 r.errors.push(issue);
145 }
146
147 if !self.baseurl.is_empty() && (self.mirrorlist.is_some() || self.metalink.is_some()) {
148 r.warnings.push(ValidationIssue {
149 level: IssueLevel::Warning,
150 location: IssueLocation::Repo(self.id.clone()),
151 field: Some("baseurl".into()),
152 message:
153 "baseurl and mirrorlist/metalink both set; DNF may ignore mirrorlist/metalink"
154 .into(),
155 });
156 }
157
158 if !self.gpgkey.is_empty()
159 && self.gpgcheck != Some(DnfBool::True)
160 && self.repo_gpgcheck != Some(DnfBool::True)
161 {
162 r.warnings.push(ValidationIssue {
163 level: IssueLevel::Warning,
164 location: IssueLocation::Repo(self.id.clone()),
165 field: Some("gpgkey".into()),
166 message: "gpgkey is set but gpgcheck and repo_gpgcheck are not enabled".into(),
167 });
168 }
169
170 r
171 }
172}
173
174impl MainConfig {
175 /// Validate the `[main]` configuration.
176 ///
177 /// Currently returns an empty (valid) report. Future releases will add
178 /// validation rules for the `[main]` section.
179 #[must_use]
180 pub fn validate(&self) -> ValidationReport {
181 ValidationReport::new()
182 }
183}