boxferry_engine/
diagnostic.rs1use std::{error::Error, fmt};
4
5#[derive(Clone, Debug, Eq, PartialEq)]
7pub struct InvalidDiagnosticCode;
8
9impl fmt::Display for InvalidDiagnosticCode {
10 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
11 formatter.write_str("diagnostic code must contain only uppercase ASCII letters and digits")
12 }
13}
14
15impl Error for InvalidDiagnosticCode {}
16
17#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
19pub struct DiagnosticCode(String);
20
21impl DiagnosticCode {
22 pub fn new(value: impl Into<String>) -> Result<Self, InvalidDiagnosticCode> {
28 let value = value.into();
29 if value.is_empty()
30 || !value
31 .bytes()
32 .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit())
33 {
34 return Err(InvalidDiagnosticCode);
35 }
36 Ok(Self(value))
37 }
38
39 #[must_use]
41 pub fn as_str(&self) -> &str {
42 &self.0
43 }
44}
45
46#[derive(Clone, Copy, Debug, Eq, PartialEq)]
48#[non_exhaustive]
49pub enum Severity {
50 Error,
52 Warning,
54 Note,
56}
57
58#[derive(Clone, Eq, PartialEq)]
60#[non_exhaustive]
61pub enum DiagnosticValue {
62 Plain(String),
64 Sensitive(String),
66}
67
68impl DiagnosticValue {
69 #[must_use]
71 pub fn plain(value: impl Into<String>) -> Self {
72 Self::Plain(value.into())
73 }
74
75 #[must_use]
77 pub fn sensitive(value: impl Into<String>) -> Self {
78 Self::Sensitive(value.into())
79 }
80
81 #[must_use]
83 pub const fn is_sensitive(&self) -> bool {
84 matches!(self, Self::Sensitive(_))
85 }
86
87 #[must_use]
89 pub fn expose(&self) -> &str {
90 match self {
91 Self::Plain(value) | Self::Sensitive(value) => value,
92 }
93 }
94
95 #[must_use]
97 pub fn redacted(&self) -> &str {
98 match self {
99 Self::Plain(value) => value,
100 Self::Sensitive(_) => "[REDACTED]",
101 }
102 }
103}
104
105impl fmt::Debug for DiagnosticValue {
106 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
107 formatter
108 .debug_tuple("DiagnosticValue")
109 .field(&self.redacted())
110 .finish()
111 }
112}
113
114#[derive(Clone, Debug, Eq, PartialEq)]
116pub struct DiagnosticField {
117 name: String,
118 value: DiagnosticValue,
119}
120
121impl DiagnosticField {
122 #[must_use]
124 pub fn new(name: impl Into<String>, value: DiagnosticValue) -> Self {
125 Self {
126 name: name.into(),
127 value,
128 }
129 }
130
131 #[must_use]
133 pub fn name(&self) -> &str {
134 &self.name
135 }
136
137 #[must_use]
139 pub const fn value(&self) -> &DiagnosticValue {
140 &self.value
141 }
142}
143
144#[derive(Clone, Debug, Eq, PartialEq)]
146pub struct Diagnostic {
147 code: DiagnosticCode,
148 severity: Severity,
149 summary: String,
150 fields: Vec<DiagnosticField>,
151}
152
153impl Diagnostic {
154 #[must_use]
156 pub fn new(code: DiagnosticCode, severity: Severity, summary: impl Into<String>) -> Self {
157 Self {
158 code,
159 severity,
160 summary: summary.into(),
161 fields: Vec::new(),
162 }
163 }
164
165 #[must_use]
167 pub fn with_field(mut self, field: DiagnosticField) -> Self {
168 self.fields.push(field);
169 self
170 }
171
172 #[must_use]
174 pub const fn code(&self) -> &DiagnosticCode {
175 &self.code
176 }
177
178 #[must_use]
180 pub const fn severity(&self) -> Severity {
181 self.severity
182 }
183
184 #[must_use]
186 pub fn summary(&self) -> &str {
187 &self.summary
188 }
189
190 #[must_use]
192 pub fn fields(&self) -> &[DiagnosticField] {
193 &self.fields
194 }
195}
196
197impl fmt::Display for Diagnostic {
198 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
199 write!(formatter, "{}: {}", self.code.as_str(), self.summary)?;
200 for field in &self.fields {
201 write!(formatter, " {}={}", field.name(), field.value().redacted())?;
202 }
203 Ok(())
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::{Diagnostic, DiagnosticCode, DiagnosticField, DiagnosticValue, Severity};
210
211 #[test]
212 fn sensitive_fields_are_redacted_from_debug_and_display() -> Result<(), String> {
213 let diagnostic = Diagnostic::new(code("BFE0001")?, Severity::Warning, "value was adjusted").with_field(
214 DiagnosticField::new("value", DiagnosticValue::sensitive("never-print-this")),
215 );
216 for rendered in [format!("{diagnostic:?}"), diagnostic.to_string()] {
217 assert!(!rendered.contains("never-print-this"));
218 assert!(rendered.contains("[REDACTED]"));
219 }
220 Ok(())
221 }
222
223 fn code(value: &str) -> Result<DiagnosticCode, String> {
224 DiagnosticCode::new(value).map_err(|error| error.to_string())
225 }
226}