1use serde::{Deserialize, Serialize};
5use std::fmt;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum DiagnosticCode {
11 InvalidJson,
12 DuplicateMember,
13 NestingTooDeep,
14 InvalidType,
15 MissingMember,
16 UnknownMember,
17 UnknownNode,
18 AmbiguousShorthand,
19 InvalidName,
20 InvalidPath,
21 InvalidFqname,
22 InvalidLiteral,
23 InvalidAccess,
24 InvalidDistributionShape,
25 LegacySpelling,
26 MissingFormatVersion,
27 DuplicateFormatVersion,
28 InvalidFormatVersionType,
29 InvalidFormatVersionSyntax,
30 FormatVersionOutOfRange,
31 UnsupportedFormatVersionMajor,
32 UnsupportedFormatVersionMinor,
33 InvalidYaml,
34 UnsupportedYamlFeature,
35 VersionMismatch,
39}
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(rename_all = "lowercase")]
44pub enum DiagnosticStage {
45 Syntax,
46 Normalization,
47 Semantic,
48}
49
50#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
52pub struct Diagnostic {
53 pub code: DiagnosticCode,
54 pub stage: DiagnosticStage,
55 pub cursor: String,
56 pub message: String,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub line: Option<u32>,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub column: Option<u32>,
61}
62
63#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
65pub struct Warning {
66 pub code: DiagnosticCode,
67 pub cursor: String,
68}
69
70impl Diagnostic {
71 pub fn new(
72 code: DiagnosticCode,
73 stage: DiagnosticStage,
74 cursor: impl Into<String>,
75 message: impl Into<String>,
76 ) -> Self {
77 Self {
78 code,
79 stage,
80 cursor: cursor.into(),
81 message: message.into(),
82 line: None,
83 column: None,
84 }
85 }
86
87 pub fn normalization(
88 code: DiagnosticCode,
89 cursor: impl Into<String>,
90 message: impl Into<String>,
91 ) -> Self {
92 Self::new(code, DiagnosticStage::Normalization, cursor, message)
93 }
94
95 pub fn syntax(
96 code: DiagnosticCode,
97 cursor: impl Into<String>,
98 message: impl Into<String>,
99 ) -> Self {
100 Self::new(code, DiagnosticStage::Syntax, cursor, message)
101 }
102
103 pub fn from_serde_error<E: fmt::Display>(err: &E) -> Option<Diagnostic> {
113 let text = err.to_string();
114 let rest = text.strip_prefix(DIAGNOSTIC_MARKER)?;
115 let mut carried = serde_json::Deserializer::from_str(rest).into_iter::<Diagnostic>();
118 let diagnostic = carried.next()?.ok()?;
119 is_location_suffix(&rest[carried.byte_offset()..]).then_some(diagnostic)
120 }
121}
122
123fn is_location_suffix(text: &str) -> bool {
128 let text = text.trim_start();
129 if text.is_empty() {
130 return true;
131 }
132 let Some(rest) = text.strip_prefix("at line ") else {
133 return false;
134 };
135 let rest = rest.trim_start_matches(|c: char| c.is_ascii_digit());
136 let rest = rest.trim_start_matches(',').trim_start();
137 let Some(rest) = rest.strip_prefix("column ") else {
138 return false;
139 };
140 !rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit())
141}
142
143const DIAGNOSTIC_MARKER: &str = "@@morphir-diagnostic@@";
144
145#[derive(Clone, Debug, PartialEq, Eq)]
148pub struct DiagnosticError(pub Diagnostic);
149
150impl fmt::Display for DiagnosticError {
151 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152 let json = serde_json::to_string(&self.0).map_err(|_| fmt::Error)?;
153 write!(f, "{DIAGNOSTIC_MARKER}{json}")
154 }
155}
156
157impl std::error::Error for DiagnosticError {}