1use serde::{Deserialize, Serialize};
2
3pub const ANALYSIS_PAYLOAD_VERSION: u32 = 1;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7pub enum SourceKind {
8 Diagram,
9 Markdown,
10 Mdx,
11}
12
13impl SourceKind {
14 pub const fn as_str(self) -> &'static str {
15 match self {
16 Self::Diagram => "diagram",
17 Self::Markdown => "markdown",
18 Self::Mdx => "mdx",
19 }
20 }
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24pub struct SourceDescriptor {
25 pub kind: SourceKind,
26 pub path: Option<String>,
27 pub diagram_index: Option<usize>,
28 pub language: String,
29}
30
31impl SourceDescriptor {
32 pub fn diagram() -> Self {
33 Self {
34 kind: SourceKind::Diagram,
35 path: None,
36 diagram_index: None,
37 language: "mermaid".to_string(),
38 }
39 }
40
41 pub fn with_path(mut self, path: impl Into<String>) -> Self {
42 self.path = Some(path.into());
43 self
44 }
45
46 pub fn with_diagram_index(mut self, diagram_index: usize) -> Self {
47 self.diagram_index = Some(diagram_index);
48 self
49 }
50
51 pub fn with_language(mut self, language: impl Into<String>) -> Self {
52 self.language = language.into();
53 self
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(rename_all = "snake_case")]
59pub enum DiagnosticSeverity {
60 Error,
61 Warning,
62 Info,
63 Hint,
64}
65
66impl DiagnosticSeverity {
67 pub const fn as_str(self) -> &'static str {
68 match self {
69 Self::Error => "error",
70 Self::Warning => "warning",
71 Self::Info => "info",
72 Self::Hint => "hint",
73 }
74 }
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
78#[serde(rename_all = "snake_case")]
79pub enum DiagnosticCategory {
80 Parse,
81 Semantic,
82 Config,
83 Resource,
84 Compatibility,
85 Layout,
86 Render,
87 Internal,
88}
89
90impl DiagnosticCategory {
91 pub const fn as_str(self) -> &'static str {
92 match self {
93 Self::Parse => "parse",
94 Self::Semantic => "semantic",
95 Self::Config => "config",
96 Self::Resource => "resource",
97 Self::Compatibility => "compatibility",
98 Self::Layout => "layout",
99 Self::Render => "render",
100 Self::Internal => "internal",
101 }
102 }
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
106pub struct Utf16Position {
107 pub line: usize,
108 pub character: usize,
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
112pub struct LspRange {
113 pub start: Utf16Position,
114 pub end: Utf16Position,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
118pub struct DiagnosticSpan {
119 pub byte_start: usize,
120 pub byte_end: usize,
121 pub line: usize,
122 pub column: usize,
123 pub end_line: usize,
124 pub end_column: usize,
125 pub lsp_range: LspRange,
126}
127
128impl DiagnosticSpan {
129 pub const fn new(
130 byte_start: usize,
131 byte_end: usize,
132 line: usize,
133 column: usize,
134 end_line: usize,
135 end_column: usize,
136 lsp_start: Utf16Position,
137 lsp_end: Utf16Position,
138 ) -> Self {
139 Self {
140 byte_start,
141 byte_end,
142 line,
143 column,
144 end_line,
145 end_column,
146 lsp_range: LspRange {
147 start: lsp_start,
148 end: lsp_end,
149 },
150 }
151 }
152}
153
154#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
155pub struct DiagnosticRelated {
156 pub message: String,
157 pub span: Option<DiagnosticSpan>,
158}
159
160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
161pub struct DiagnosticFixEdit {
162 pub span: DiagnosticSpan,
163 pub replacement: String,
164}
165
166impl DiagnosticFixEdit {
167 pub fn new(span: DiagnosticSpan, replacement: impl Into<String>) -> Self {
168 Self {
169 span,
170 replacement: replacement.into(),
171 }
172 }
173}
174
175#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
176pub struct DiagnosticFix {
177 pub title: String,
178 pub edits: Vec<DiagnosticFixEdit>,
179 #[serde(default, skip_serializing_if = "is_false")]
180 pub is_preferred: bool,
181}
182
183impl DiagnosticFix {
184 pub fn new(title: impl Into<String>, edits: Vec<DiagnosticFixEdit>) -> Self {
185 Self {
186 title: title.into(),
187 edits,
188 is_preferred: false,
189 }
190 }
191
192 pub fn preferred(mut self) -> Self {
193 self.is_preferred = true;
194 self
195 }
196}
197
198const fn is_false(value: &bool) -> bool {
199 !*value
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
203pub struct AnalysisDiagnostic {
204 pub id: String,
205 pub severity: DiagnosticSeverity,
206 pub category: DiagnosticCategory,
207 pub message: String,
208 pub code: Option<i32>,
209 pub code_name: Option<String>,
210 pub diagram_type: Option<String>,
211 pub span: Option<DiagnosticSpan>,
212 #[serde(default)]
213 pub related: Vec<DiagnosticRelated>,
214 pub help: Option<String>,
215 #[serde(default, skip_serializing_if = "Vec::is_empty")]
216 pub fixes: Vec<DiagnosticFix>,
217}
218
219impl AnalysisDiagnostic {
220 pub fn new(
221 id: impl Into<String>,
222 severity: DiagnosticSeverity,
223 category: DiagnosticCategory,
224 message: impl Into<String>,
225 ) -> Self {
226 Self {
227 id: id.into(),
228 severity,
229 category,
230 message: message.into(),
231 code: None,
232 code_name: None,
233 diagram_type: None,
234 span: None,
235 related: Vec::new(),
236 help: None,
237 fixes: Vec::new(),
238 }
239 }
240
241 pub fn error(
242 id: impl Into<String>,
243 category: DiagnosticCategory,
244 message: impl Into<String>,
245 ) -> Self {
246 Self::new(id, DiagnosticSeverity::Error, category, message)
247 }
248
249 pub fn with_code(mut self, code: i32, code_name: impl Into<String>) -> Self {
250 self.code = Some(code);
251 self.code_name = Some(code_name.into());
252 self
253 }
254
255 pub fn with_diagram_type(mut self, diagram_type: impl Into<String>) -> Self {
256 self.diagram_type = Some(diagram_type.into());
257 self
258 }
259
260 pub const fn with_span(mut self, span: DiagnosticSpan) -> Self {
261 self.span = Some(span);
262 self
263 }
264
265 pub fn with_help(mut self, help: impl Into<String>) -> Self {
266 self.help = Some(help.into());
267 self
268 }
269
270 pub fn with_fix(mut self, fix: DiagnosticFix) -> Self {
271 self.fixes.push(fix);
272 self
273 }
274
275 pub fn with_fixes(mut self, fixes: impl IntoIterator<Item = DiagnosticFix>) -> Self {
276 self.fixes.extend(fixes);
277 self
278 }
279}
280
281#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
282pub struct Summary {
283 pub errors: usize,
284 pub warnings: usize,
285 pub infos: usize,
286 pub hints: usize,
287}
288
289impl Summary {
290 pub fn from_diagnostics(diagnostics: &[AnalysisDiagnostic]) -> Self {
291 diagnostics
292 .iter()
293 .fold(Self::default(), |mut summary, diagnostic| {
294 match diagnostic.severity {
295 DiagnosticSeverity::Error => summary.errors += 1,
296 DiagnosticSeverity::Warning => summary.warnings += 1,
297 DiagnosticSeverity::Info => summary.infos += 1,
298 DiagnosticSeverity::Hint => summary.hints += 1,
299 }
300 summary
301 })
302 }
303}
304
305#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
306pub struct AnalysisPayload {
307 pub version: u32,
308 pub valid: bool,
309 pub summary: Summary,
310 pub source: SourceDescriptor,
311 pub diagnostics: Vec<AnalysisDiagnostic>,
312}
313
314impl AnalysisPayload {
315 pub fn new(source: SourceDescriptor, diagnostics: Vec<AnalysisDiagnostic>) -> Self {
316 let summary = Summary::from_diagnostics(&diagnostics);
317 Self {
318 version: ANALYSIS_PAYLOAD_VERSION,
319 valid: summary.errors == 0,
320 summary,
321 source,
322 diagnostics,
323 }
324 }
325
326 pub fn valid(source: SourceDescriptor) -> Self {
327 Self::new(source, Vec::new())
328 }
329
330 pub fn to_json_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
331 serde_json::to_vec(self)
332 }
333
334 pub fn to_pretty_json_string(&self) -> Result<String, serde_json::Error> {
335 serde_json::to_string_pretty(self)
336 }
337}