tfparser_core/
diagnostic.rs1use std::sync::Arc;
11
12use serde::{Deserialize, Serialize};
13use typed_builder::TypedBuilder;
14
15use crate::ir::Span;
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
24#[serde(rename_all = "kebab-case")]
25#[non_exhaustive]
26pub enum Severity {
27 Trace,
29 Info,
31 Warn,
33 Error,
35}
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
42#[serde(rename_all = "kebab-case")]
43#[non_exhaustive]
44pub enum LimitKind {
45 FileSize,
47 TotalFiles,
49 WalkDepth,
51 BlocksPerFile,
53 AttributeDepth,
55 TemplateParts,
57 IncludeDepth,
59 EvalIterations,
61 FuncArgs,
63 ListLength,
65 StringSize,
67 Expansion,
69 ConfigFileSize,
71}
72
73#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, TypedBuilder)]
81#[non_exhaustive]
82#[serde(rename_all = "camelCase")]
83#[builder(field_defaults(setter(into)))]
84pub struct Diagnostic {
85 pub severity: Severity,
87 pub code: Arc<str>,
89 pub message: Arc<str>,
91 #[builder(default)]
93 pub span: Option<Span>,
94 #[builder(default)]
96 pub suggestion: Option<Arc<str>>,
97 #[builder(default)]
101 pub limit_kind: Option<LimitKind>,
102}
103
104impl Diagnostic {
105 #[must_use]
107 pub fn new(
108 severity: Severity,
109 code: impl Into<Arc<str>>,
110 message: impl Into<Arc<str>>,
111 ) -> Self {
112 Self {
113 severity,
114 code: code.into(),
115 message: message.into(),
116 span: None,
117 suggestion: None,
118 limit_kind: None,
119 }
120 }
121
122 #[must_use]
128 pub fn limit(kind: LimitKind, code: impl Into<Arc<str>>, message: impl Into<Arc<str>>) -> Self {
129 Self {
130 severity: Severity::Warn,
131 code: code.into(),
132 message: message.into(),
133 span: None,
134 suggestion: None,
135 limit_kind: Some(kind),
136 }
137 }
138
139 #[must_use]
141 pub fn with_span(mut self, span: Span) -> Self {
142 self.span = Some(span);
143 self
144 }
145
146 #[must_use]
148 pub fn with_suggestion(mut self, suggestion: impl Into<Arc<str>>) -> Self {
149 self.suggestion = Some(suggestion.into());
150 self
151 }
152}
153
154#[cfg(test)]
155#[allow(clippy::unwrap_used)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn test_should_compose_diagnostic_with_builder_style() {
161 let d = Diagnostic::new(Severity::Warn, "TF1001", "skipped malformed file")
162 .with_suggestion("re-run with --verbose for the parse error");
163 assert_eq!(d.severity, Severity::Warn);
164 assert_eq!(&*d.code, "TF1001");
165 assert!(d.suggestion.is_some());
166 }
167
168 #[test]
169 fn test_should_carry_limit_kind_when_constructed_via_limit() {
170 let d = Diagnostic::limit(LimitKind::BlocksPerFile, "TF1200", "exceeded");
171 assert_eq!(d.severity, Severity::Warn);
172 assert_eq!(d.limit_kind, Some(LimitKind::BlocksPerFile));
173 }
174
175 #[test]
176 fn test_should_serde_round_trip_diagnostic() {
177 let d = Diagnostic::new(Severity::Error, "TG2003", "include cycle detected");
178 let json = serde_json::to_string(&d).unwrap();
179 let back: Diagnostic = serde_json::from_str(&json).unwrap();
180 assert_eq!(d, back);
181 }
182
183 #[test]
184 fn test_should_serialize_severity_kebab_case() {
185 let json = serde_json::to_string(&Severity::Warn).unwrap();
186 assert_eq!(json, "\"warn\"");
187 }
188
189 #[test]
190 fn test_should_serialize_limit_kind_kebab_case() {
191 let json = serde_json::to_string(&LimitKind::IncludeDepth).unwrap();
192 assert_eq!(json, "\"include-depth\"");
193 }
194
195 #[test]
196 fn test_should_order_severities_naturally() {
197 assert!(Severity::Trace < Severity::Info);
198 assert!(Severity::Info < Severity::Warn);
199 assert!(Severity::Warn < Severity::Error);
200 }
201}