Skip to main content

merman_analysis/
payload.rs

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}