use std::sync::Arc;
use serde::{Deserialize, Serialize};
use typed_builder::TypedBuilder;
use crate::ir::Span;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum Severity {
Trace,
Info,
Warn,
Error,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum LimitKind {
FileSize,
TotalFiles,
WalkDepth,
BlocksPerFile,
AttributeDepth,
TemplateParts,
IncludeDepth,
EvalIterations,
FuncArgs,
ListLength,
StringSize,
Expansion,
ConfigFileSize,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, TypedBuilder)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
#[builder(field_defaults(setter(into)))]
pub struct Diagnostic {
pub severity: Severity,
pub code: Arc<str>,
pub message: Arc<str>,
#[builder(default)]
pub span: Option<Span>,
#[builder(default)]
pub suggestion: Option<Arc<str>>,
#[builder(default)]
pub limit_kind: Option<LimitKind>,
}
impl Diagnostic {
#[must_use]
pub fn new(
severity: Severity,
code: impl Into<Arc<str>>,
message: impl Into<Arc<str>>,
) -> Self {
Self {
severity,
code: code.into(),
message: message.into(),
span: None,
suggestion: None,
limit_kind: None,
}
}
#[must_use]
pub fn limit(kind: LimitKind, code: impl Into<Arc<str>>, message: impl Into<Arc<str>>) -> Self {
Self {
severity: Severity::Warn,
code: code.into(),
message: message.into(),
span: None,
suggestion: None,
limit_kind: Some(kind),
}
}
#[must_use]
pub fn with_span(mut self, span: Span) -> Self {
self.span = Some(span);
self
}
#[must_use]
pub fn with_suggestion(mut self, suggestion: impl Into<Arc<str>>) -> Self {
self.suggestion = Some(suggestion.into());
self
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_should_compose_diagnostic_with_builder_style() {
let d = Diagnostic::new(Severity::Warn, "TF1001", "skipped malformed file")
.with_suggestion("re-run with --verbose for the parse error");
assert_eq!(d.severity, Severity::Warn);
assert_eq!(&*d.code, "TF1001");
assert!(d.suggestion.is_some());
}
#[test]
fn test_should_carry_limit_kind_when_constructed_via_limit() {
let d = Diagnostic::limit(LimitKind::BlocksPerFile, "TF1200", "exceeded");
assert_eq!(d.severity, Severity::Warn);
assert_eq!(d.limit_kind, Some(LimitKind::BlocksPerFile));
}
#[test]
fn test_should_serde_round_trip_diagnostic() {
let d = Diagnostic::new(Severity::Error, "TG2003", "include cycle detected");
let json = serde_json::to_string(&d).unwrap();
let back: Diagnostic = serde_json::from_str(&json).unwrap();
assert_eq!(d, back);
}
#[test]
fn test_should_serialize_severity_kebab_case() {
let json = serde_json::to_string(&Severity::Warn).unwrap();
assert_eq!(json, "\"warn\"");
}
#[test]
fn test_should_serialize_limit_kind_kebab_case() {
let json = serde_json::to_string(&LimitKind::IncludeDepth).unwrap();
assert_eq!(json, "\"include-depth\"");
}
#[test]
fn test_should_order_severities_naturally() {
assert!(Severity::Trace < Severity::Info);
assert!(Severity::Info < Severity::Warn);
assert!(Severity::Warn < Severity::Error);
}
}