Skip to main content

modum_core/
diagnostic.rs

1use std::path::PathBuf;
2
3use serde::{Serialize, Serializer, ser::SerializeStruct};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
6pub enum DiagnosticLevel {
7    Warning,
8    Error,
9}
10
11#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
12pub enum DiagnosticClass {
13    ToolError,
14    ToolWarning,
15    PolicyError { code: String },
16    PolicyWarning { code: String },
17    AdvisoryWarning { code: String },
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
21#[serde(rename_all = "snake_case")]
22pub enum DiagnosticFixKind {
23    ReplacePath,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
27pub struct DiagnosticFix {
28    pub kind: DiagnosticFixKind,
29    pub replacement: String,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
33pub struct Diagnostic {
34    pub class: DiagnosticClass,
35    pub file: Option<PathBuf>,
36    pub line: Option<usize>,
37    pub fix: Option<DiagnosticFix>,
38    pub message: String,
39}
40
41impl Diagnostic {
42    pub fn error(file: Option<PathBuf>, line: Option<usize>, message: impl Into<String>) -> Self {
43        Self {
44            class: DiagnosticClass::ToolError,
45            file,
46            line,
47            fix: None,
48            message: message.into(),
49        }
50    }
51
52    pub fn warning(file: Option<PathBuf>, line: Option<usize>, message: impl Into<String>) -> Self {
53        Self {
54            class: DiagnosticClass::ToolWarning,
55            file,
56            line,
57            fix: None,
58            message: message.into(),
59        }
60    }
61
62    pub fn policy(
63        file: Option<PathBuf>,
64        line: Option<usize>,
65        code: impl Into<String>,
66        message: impl Into<String>,
67    ) -> Self {
68        Self {
69            class: DiagnosticClass::PolicyWarning { code: code.into() },
70            file,
71            line,
72            fix: None,
73            message: message.into(),
74        }
75    }
76
77    pub fn policy_error(
78        file: Option<PathBuf>,
79        line: Option<usize>,
80        code: impl Into<String>,
81        message: impl Into<String>,
82    ) -> Self {
83        Self {
84            class: DiagnosticClass::PolicyError { code: code.into() },
85            file,
86            line,
87            fix: None,
88            message: message.into(),
89        }
90    }
91
92    pub fn advisory(
93        file: Option<PathBuf>,
94        line: Option<usize>,
95        code: impl Into<String>,
96        message: impl Into<String>,
97    ) -> Self {
98        Self {
99            class: DiagnosticClass::AdvisoryWarning { code: code.into() },
100            file,
101            line,
102            fix: None,
103            message: message.into(),
104        }
105    }
106
107    pub fn with_fix(mut self, fix: DiagnosticFix) -> Self {
108        self.fix = Some(fix);
109        self
110    }
111
112    pub fn level(&self) -> DiagnosticLevel {
113        match self.class {
114            DiagnosticClass::ToolError | DiagnosticClass::PolicyError { .. } => {
115                DiagnosticLevel::Error
116            }
117            DiagnosticClass::ToolWarning
118            | DiagnosticClass::PolicyWarning { .. }
119            | DiagnosticClass::AdvisoryWarning { .. } => DiagnosticLevel::Warning,
120        }
121    }
122
123    pub fn code(&self) -> Option<&str> {
124        match &self.class {
125            DiagnosticClass::PolicyError { code }
126            | DiagnosticClass::PolicyWarning { code }
127            | DiagnosticClass::AdvisoryWarning { code } => Some(code),
128            DiagnosticClass::ToolError | DiagnosticClass::ToolWarning => None,
129        }
130    }
131
132    pub fn is_error(&self) -> bool {
133        matches!(
134            self.class,
135            DiagnosticClass::ToolError | DiagnosticClass::PolicyError { .. }
136        )
137    }
138
139    pub fn is_policy_warning(&self) -> bool {
140        matches!(self.class, DiagnosticClass::PolicyWarning { .. })
141    }
142
143    pub fn is_advisory_warning(&self) -> bool {
144        matches!(
145            self.class,
146            DiagnosticClass::ToolWarning | DiagnosticClass::AdvisoryWarning { .. }
147        )
148    }
149
150    pub fn is_policy_violation(&self) -> bool {
151        matches!(
152            self.class,
153            DiagnosticClass::PolicyError { .. } | DiagnosticClass::PolicyWarning { .. }
154        )
155    }
156}
157
158impl Serialize for Diagnostic {
159    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
160    where
161        S: Serializer,
162    {
163        let mut state = serializer.serialize_struct("Diagnostic", 7)?;
164        state.serialize_field("level", &self.level())?;
165        state.serialize_field("file", &self.file)?;
166        state.serialize_field("line", &self.line)?;
167        state.serialize_field("code", &self.code())?;
168        state.serialize_field("policy", &self.is_policy_violation())?;
169        state.serialize_field("fix", &self.fix)?;
170        state.serialize_field("message", &self.message)?;
171        state.end()
172    }
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub enum DiagnosticSelection {
177    All,
178    Policy,
179    Advisory,
180}
181
182impl DiagnosticSelection {
183    pub fn includes(self, diagnostic: &Diagnostic) -> bool {
184        match self {
185            Self::All => true,
186            Self::Policy => diagnostic.is_error() || diagnostic.is_policy_violation(),
187            Self::Advisory => diagnostic.is_error() || diagnostic.is_advisory_warning(),
188        }
189    }
190
191    pub fn report_label(self) -> Option<&'static str> {
192        match self {
193            Self::All => None,
194            Self::Policy => Some("policy diagnostics and errors only"),
195            Self::Advisory => Some("advisory diagnostics and errors only"),
196        }
197    }
198}
199
200impl std::str::FromStr for DiagnosticSelection {
201    type Err = String;
202
203    fn from_str(raw: &str) -> Result<Self, Self::Err> {
204        match raw {
205            "all" => Ok(Self::All),
206            "policy" => Ok(Self::Policy),
207            "advisory" => Ok(Self::Advisory),
208            _ => Err(format!(
209                "invalid show mode `{raw}`; expected all|policy|advisory"
210            )),
211        }
212    }
213}