use std::{error::Error, fmt};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InvalidDiagnosticCode;
impl fmt::Display for InvalidDiagnosticCode {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("diagnostic code must contain only uppercase ASCII letters and digits")
}
}
impl Error for InvalidDiagnosticCode {}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct DiagnosticCode(String);
impl DiagnosticCode {
pub fn new(value: impl Into<String>) -> Result<Self, InvalidDiagnosticCode> {
let value = value.into();
if value.is_empty()
|| !value
.bytes()
.all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit())
{
return Err(InvalidDiagnosticCode);
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Severity {
Error,
Warning,
Note,
}
#[derive(Clone, Eq, PartialEq)]
#[non_exhaustive]
pub enum DiagnosticValue {
Plain(String),
Sensitive(String),
}
impl DiagnosticValue {
#[must_use]
pub fn plain(value: impl Into<String>) -> Self {
Self::Plain(value.into())
}
#[must_use]
pub fn sensitive(value: impl Into<String>) -> Self {
Self::Sensitive(value.into())
}
#[must_use]
pub const fn is_sensitive(&self) -> bool {
matches!(self, Self::Sensitive(_))
}
#[must_use]
pub fn expose(&self) -> &str {
match self {
Self::Plain(value) | Self::Sensitive(value) => value,
}
}
#[must_use]
pub fn redacted(&self) -> &str {
match self {
Self::Plain(value) => value,
Self::Sensitive(_) => "[REDACTED]",
}
}
}
impl fmt::Debug for DiagnosticValue {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_tuple("DiagnosticValue")
.field(&self.redacted())
.finish()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DiagnosticField {
name: String,
value: DiagnosticValue,
}
impl DiagnosticField {
#[must_use]
pub fn new(name: impl Into<String>, value: DiagnosticValue) -> Self {
Self {
name: name.into(),
value,
}
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub const fn value(&self) -> &DiagnosticValue {
&self.value
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Diagnostic {
code: DiagnosticCode,
severity: Severity,
summary: String,
fields: Vec<DiagnosticField>,
}
impl Diagnostic {
#[must_use]
pub fn new(code: DiagnosticCode, severity: Severity, summary: impl Into<String>) -> Self {
Self {
code,
severity,
summary: summary.into(),
fields: Vec::new(),
}
}
#[must_use]
pub fn with_field(mut self, field: DiagnosticField) -> Self {
self.fields.push(field);
self
}
#[must_use]
pub const fn code(&self) -> &DiagnosticCode {
&self.code
}
#[must_use]
pub const fn severity(&self) -> Severity {
self.severity
}
#[must_use]
pub fn summary(&self) -> &str {
&self.summary
}
#[must_use]
pub fn fields(&self) -> &[DiagnosticField] {
&self.fields
}
}
impl fmt::Display for Diagnostic {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}: {}", self.code.as_str(), self.summary)?;
for field in &self.fields {
write!(formatter, " {}={}", field.name(), field.value().redacted())?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{Diagnostic, DiagnosticCode, DiagnosticField, DiagnosticValue, Severity};
#[test]
fn sensitive_fields_are_redacted_from_debug_and_display() -> Result<(), String> {
let diagnostic = Diagnostic::new(code("BFE0001")?, Severity::Warning, "value was adjusted").with_field(
DiagnosticField::new("value", DiagnosticValue::sensitive("never-print-this")),
);
for rendered in [format!("{diagnostic:?}"), diagnostic.to_string()] {
assert!(!rendered.contains("never-print-this"));
assert!(rendered.contains("[REDACTED]"));
}
Ok(())
}
fn code(value: &str) -> Result<DiagnosticCode, String> {
DiagnosticCode::new(value).map_err(|error| error.to_string())
}
}