use std::fmt;
use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Error,
Warning,
Info,
}
#[derive(Debug, Clone, Serialize)]
pub struct Diagnostic {
pub severity: Severity,
pub code: &'static str,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub field: Option<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub suggestion: Option<String>,
}
impl Diagnostic {
#[must_use]
pub fn new(severity: Severity, code: &'static str, message: impl Into<String>) -> Self {
Self {
severity,
code,
message: message.into(),
field: None,
suggestion: None,
}
}
#[must_use]
pub fn with_field(mut self, field: &'static str) -> Self {
self.field = Some(field);
self
}
#[must_use]
pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
self.suggestion = Some(suggestion.into());
self
}
#[must_use]
pub fn is_error(&self) -> bool {
self.severity == Severity::Error
}
#[must_use]
pub fn is_warning(&self) -> bool {
self.severity == Severity::Warning
}
#[must_use]
pub fn is_info(&self) -> bool {
self.severity == Severity::Info
}
}
impl fmt::Display for Diagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.severity {
Severity::Error => write!(f, "{}", self.message),
Severity::Warning => write!(f, "warning: {}", self.message),
Severity::Info => write!(f, "info: {}", self.message),
}
}
}
pub const E000: &str = "E000";
pub const E001: &str = "E001";
pub const E002: &str = "E002";
pub const E003: &str = "E003";
pub const E004: &str = "E004";
pub const E005: &str = "E005";
pub const E006: &str = "E006";
pub const E007: &str = "E007";
pub const E008: &str = "E008";
pub const E009: &str = "E009";
pub const E010: &str = "E010";
pub const E011: &str = "E011";
pub const E012: &str = "E012";
pub const E013: &str = "E013";
pub const E014: &str = "E014";
pub const E015: &str = "E015";
pub const E016: &str = "E016";
pub const E017: &str = "E017";
pub const E018: &str = "E018";
pub const W001: &str = "W001";
pub const W002: &str = "W002";
pub const S001: &str = "S001";
pub const S002: &str = "S002";
pub const S003: &str = "S003";
pub const S004: &str = "S004";
pub const S005: &str = "S005";
pub const S006: &str = "S006";
pub const C001: &str = "C001";
pub const C002: &str = "C002";
pub const C003: &str = "C003";
pub const P001: &str = "P001";
pub const P002: &str = "P002";
pub const P003: &str = "P003";
pub const P004: &str = "P004";
pub const P005: &str = "P005";
pub const P006: &str = "P006";
pub const P007: &str = "P007";
pub const P008: &str = "P008";
pub const P009: &str = "P009";
pub const P010: &str = "P010";
pub const P011: &str = "P011";
pub const H001: &str = "H001";
pub const H002: &str = "H002";
pub const H003: &str = "H003";
pub const H004: &str = "H004";
pub const H005: &str = "H005";
pub const H006: &str = "H006";
pub const H007: &str = "H007";
pub const H008: &str = "H008";
pub const H009: &str = "H009";
pub const H010: &str = "H010";
pub const H011: &str = "H011";
pub const A001: &str = "A001";
pub const A002: &str = "A002";
pub const A003: &str = "A003";
pub const A004: &str = "A004";
pub const A005: &str = "A005";
pub const A006: &str = "A006";
pub const A007: &str = "A007";
pub const A008: &str = "A008";
pub const A009: &str = "A009";
pub const A010: &str = "A010";
pub const K001: &str = "K001";
pub const K002: &str = "K002";
pub const K003: &str = "K003";
pub const K004: &str = "K004";
pub const K005: &str = "K005";
pub const K006: &str = "K006";
pub const K007: &str = "K007";
pub const X001: &str = "X001";
pub const X002: &str = "X002";
pub const X003: &str = "X003";
pub const X004: &str = "X004";
pub const X005: &str = "X005";
pub const X006: &str = "X006";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ValidationTarget {
#[default]
Standard,
ClaudeCode,
Permissive,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_display_no_prefix() {
let d = Diagnostic::new(Severity::Error, E001, "name must not be empty");
assert_eq!(d.to_string(), "name must not be empty");
}
#[test]
fn warning_display_with_prefix() {
let d = Diagnostic::new(Severity::Warning, W001, "unexpected metadata field: 'foo'");
assert_eq!(d.to_string(), "warning: unexpected metadata field: 'foo'");
}
#[test]
fn info_display_with_prefix() {
let d = Diagnostic::new(Severity::Info, "I001", "description uses first person");
assert_eq!(d.to_string(), "info: description uses first person");
}
#[test]
fn is_error_true_for_errors() {
let d = Diagnostic::new(Severity::Error, E001, "test");
assert!(d.is_error());
assert!(!d.is_warning());
assert!(!d.is_info());
}
#[test]
fn is_warning_true_for_warnings() {
let d = Diagnostic::new(Severity::Warning, W001, "test");
assert!(!d.is_error());
assert!(d.is_warning());
assert!(!d.is_info());
}
#[test]
fn is_info_true_for_info() {
let d = Diagnostic::new(Severity::Info, "I001", "test");
assert!(!d.is_error());
assert!(!d.is_warning());
assert!(d.is_info());
}
#[test]
fn with_field_sets_field() {
let d = Diagnostic::new(Severity::Error, E001, "test").with_field("name");
assert_eq!(d.field, Some("name"));
}
#[test]
fn with_suggestion_sets_suggestion() {
let d = Diagnostic::new(Severity::Error, E003, "invalid character")
.with_suggestion("Use lowercase letters only");
assert_eq!(d.suggestion.as_deref(), Some("Use lowercase letters only"));
}
#[test]
fn new_has_no_field_or_suggestion() {
let d = Diagnostic::new(Severity::Error, E001, "test");
assert!(d.field.is_none());
assert!(d.suggestion.is_none());
}
#[test]
fn builder_pattern_chains() {
let d = Diagnostic::new(Severity::Error, E003, "invalid character: 'X'")
.with_field("name")
.with_suggestion("Use lowercase: 'x'");
assert_eq!(d.code, E003);
assert_eq!(d.field, Some("name"));
assert!(d.suggestion.is_some());
}
#[test]
fn serialize_json_error() {
let d = Diagnostic::new(Severity::Error, E001, "name must not be empty").with_field("name");
let json = serde_json::to_value(&d).unwrap();
assert_eq!(json["severity"], "error");
assert_eq!(json["code"], "E001");
assert_eq!(json["message"], "name must not be empty");
assert_eq!(json["field"], "name");
assert!(json.get("suggestion").is_none());
}
#[test]
fn serialize_json_warning_with_suggestion() {
let d = Diagnostic::new(Severity::Warning, W001, "unexpected field: 'foo'")
.with_field("metadata")
.with_suggestion("Remove the field");
let json = serde_json::to_value(&d).unwrap();
assert_eq!(json["severity"], "warning");
assert_eq!(json["suggestion"], "Remove the field");
}
#[test]
fn serialize_json_omits_none_fields() {
let d = Diagnostic::new(Severity::Error, E001, "test");
let json = serde_json::to_value(&d).unwrap();
assert!(json.get("field").is_none());
assert!(json.get("suggestion").is_none());
}
#[test]
fn error_codes_are_unique() {
let codes = [
E000, E001, E002, E003, E004, E005, E006, E007, E008, E009, E010, E011, E012, E013,
E014, E015, E016, E017, E018, W001, W002, S001, S002, S003, S004, S005, S006, C001,
C002, C003, P001, P002, P003, P004, P005, P006, P007, P008, P009, P010, P011, H001,
H002, H003, H004, H005, H006, H007, H008, H009, H010, H011, A001, A002, A003, A004,
A005, A006, A007, A008, A009, A010, K001, K002, K003, K004, K005, K006, K007, X001,
X002, X003, X004, X005, X006,
];
let mut seen = std::collections::HashSet::new();
for code in &codes {
assert!(seen.insert(code), "duplicate error code: {code}");
}
}
#[test]
fn validation_target_default_is_standard() {
let target = ValidationTarget::default();
assert_eq!(target, ValidationTarget::Standard);
}
}