use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum Severity {
Error,
Warning,
Info,
}
impl Severity {
pub fn as_str(self) -> &'static str {
match self {
Severity::Error => "error",
Severity::Warning => "warning",
Severity::Info => "info",
}
}
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Finding {
pub path: String,
pub line: Option<usize>,
pub severity: Severity,
pub message: String,
pub authority: Option<String>,
}
impl Finding {
pub fn new(severity: Severity, path: impl Into<String>, message: impl Into<String>) -> Self {
Finding {
path: path.into(),
line: None,
severity,
message: message.into(),
authority: None,
}
}
pub fn error(path: impl Into<String>, message: impl Into<String>) -> Self {
Self::new(Severity::Error, path, message)
}
pub fn warning(path: impl Into<String>, message: impl Into<String>) -> Self {
Self::new(Severity::Warning, path, message)
}
pub fn info(path: impl Into<String>, message: impl Into<String>) -> Self {
Self::new(Severity::Info, path, message)
}
#[must_use]
pub fn line(mut self, line: usize) -> Self {
self.line = Some(line);
self
}
#[must_use]
pub fn authority(mut self, authority: impl Into<String>) -> Self {
self.authority = Some(authority.into());
self
}
pub fn full_message(&self) -> String {
match &self.authority {
Some(a) => format!("{} ({a})", self.message),
None => self.message.clone(),
}
}
pub fn to_line(&self) -> String {
let level = self.severity.as_str();
match self.line {
Some(line) => format!("{}:{line}: {level}: {}", self.path, self.full_message()),
None => format!("{}: {level}: {}", self.path, self.full_message()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_line_carries_the_level_and_authority() {
let f = Finding::error("a.yaml", "bad").line(3).authority("CFG-1");
assert_eq!(f.to_line(), "a.yaml:3: error: bad (CFG-1)");
let f = Finding::warning("a.yaml", "odd");
assert_eq!(f.to_line(), "a.yaml: warning: odd");
let f = Finding::info("c.toml", "project wins")
.line(1)
.authority("CLI-3");
assert_eq!(f.severity, Severity::Info);
assert_eq!(f.to_line(), "c.toml:1: info: project wins (CLI-3)");
}
#[test]
fn findings_round_trip_through_serde() {
let f = Finding::error("a.md", "bad").line(3).authority("spec §8");
let json = serde_json::to_string(&f).unwrap();
assert!(json.contains("\"severity\":\"error\""), "{json}");
let back: Finding = serde_json::from_str(&json).unwrap();
assert_eq!(back, f);
}
}