1use nvim_oxi::Dictionary;
15use serde::Deserialize;
16
17use crate::diagnostics::DiagnosticSeverity;
18
19pub fn dict() -> Dictionary {
21 dict! {
22 "sqruff": dict! {
23 "parser": fn_from!(parser)
24 },
25 }
26}
27
28#[allow(clippy::needless_pass_by_value)]
35fn parser(maybe_output: Option<nvim_oxi::String>) -> Vec<Dictionary> {
36 let Some(output) = &maybe_output else {
37 ytil_noxi::notify::warn(format!("sqruff output missing output={maybe_output:?}"));
38 return vec![];
39 };
40 let output = output.to_string_lossy();
41
42 if output.trim().is_empty() {
43 ytil_noxi::notify::warn(format!("sqruff output is an empty string output={maybe_output:?}"));
44 return vec![];
45 }
46
47 let parsed_output = match serde_json::from_str::<SqruffOutput>(&output) {
48 Ok(parsed_output) => parsed_output,
49 Err(err) => {
50 ytil_noxi::notify::error(format!(
51 "error parsing sqruff output | output={output:?} error={err:#?}"
52 ));
53 return vec![];
54 }
55 };
56
57 parsed_output
58 .messages
59 .into_iter()
60 .map(diagnostic_dict_from_msg)
61 .collect()
62}
63
64#[derive(Debug, Deserialize)]
72#[cfg_attr(test, derive(Eq, PartialEq))]
73struct SqruffOutput {
74 #[serde(rename = "<string>", default)]
75 messages: Vec<SqruffMessage>,
76}
77
78#[derive(Debug, Deserialize)]
80#[cfg_attr(test, derive(Eq, PartialEq))]
81struct SqruffMessage {
82 code: Option<String>,
84 message: String,
86 range: Range,
88 severity: DiagnosticSeverity,
90 source: String,
92}
93
94#[derive(Debug, Deserialize)]
101#[cfg_attr(test, derive(Eq, PartialEq))]
102struct Range {
103 start: Position,
105 end: Position,
108}
109
110#[derive(Debug, Deserialize)]
116#[cfg_attr(test, derive(Eq, PartialEq))]
117struct Position {
118 character: u32,
120 line: u32,
122}
123
124fn diagnostic_dict_from_msg(msg: SqruffMessage) -> Dictionary {
126 dict! {
127 "lnum": msg.range.start.line.saturating_sub(1),
128 "end_lnum": msg.range.end.line.saturating_sub(1),
129 "col": msg.range.start.character.saturating_sub(1),
130 "end_col": msg.range.end.character.saturating_sub(1),
131 "message": msg.message,
132 "code": msg.code.map_or_else(nvim_oxi::Object::nil, nvim_oxi::Object::from),
133 "source": msg.source,
134 "severity": msg.severity.to_number(),
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use nvim_oxi::Object;
141
142 use super::*;
143
144 #[test]
145 fn diagnostic_dict_from_msg_returns_the_expected_dict_from_msg() {
146 let msg = SqruffMessage {
147 code: Some("R001".to_string()),
148 message: "Example message".to_string(),
149 range: Range {
150 start: Position { line: 3, character: 7 },
151 end: Position { line: 4, character: 10 },
152 },
153 severity: DiagnosticSeverity::Warn,
154 source: "sqruff".to_string(),
155 };
156
157 let res = diagnostic_dict_from_msg(msg);
158
159 let expected = dict! {
160 "lnum": 2,
161 "end_lnum": 3,
162 "col": 6,
163 "end_col": 9,
164 "message": "Example message".to_string(),
165 "code": Object::from(nvim_oxi::String::from("R001")),
166 "source": "sqruff".to_string(),
167 "severity": DiagnosticSeverity::Warn.to_number(),
168 };
169 pretty_assertions::assert_eq!(res, expected);
170 }
171
172 #[test]
173 fn sqruff_output_deserializes_empty_messages() {
174 let value = serde_json::json!({
175 "<string>": []
176 });
177
178 assert2::let_assert!(Ok(parsed) = serde_json::from_value::<SqruffOutput>(value));
179 pretty_assertions::assert_eq!(parsed, SqruffOutput { messages: vec![] });
180 }
181
182 #[test]
183 fn sqruff_output_deserializes_single_message_with_code() {
184 let value = serde_json::json!({
185 "<string>": [
186 {
187 "code": "R001",
188 "message": "Msg",
189 "range": {"start": {"line": 2, "character": 5}, "end": {"line": 2, "character": 10}},
190 "severity": "2",
191 "source": "sqruff"
192 }
193 ]
194 });
195
196 assert2::let_assert!(Ok(res) = serde_json::from_value::<SqruffOutput>(value));
197 pretty_assertions::assert_eq!(
198 res,
199 SqruffOutput {
200 messages: vec![SqruffMessage {
201 code: Some("R001".into()),
202 message: "Msg".into(),
203 range: Range {
204 start: Position { line: 2, character: 5 },
205 end: Position { line: 2, character: 10 },
206 },
207 severity: DiagnosticSeverity::Warn,
208 source: "sqruff".into(),
209 }],
210 }
211 );
212 }
213
214 #[test]
215 fn sqruff_output_deserializes_multiple_messages_mixed_code() {
216 let value = serde_json::json!({
217 "<string>": [
218 {
219 "code": "R001",
220 "message": "HasCode",
221 "range": {"start": {"line": 3, "character": 7}, "end": {"line": 3, "character": 12}},
222 "severity": "2",
223 "source": "sqruff"
224 },
225 {
226 "code": null,
227 "message": "NoCode",
228 "range": {"start": {"line": 1, "character": 1}, "end": {"line": 1, "character": 2}},
229 "severity": "1",
230 "source": "sqruff"
231 }
232 ]
233 });
234
235 assert2::let_assert!(Ok(res) = serde_json::from_value::<SqruffOutput>(value));
236 pretty_assertions::assert_eq!(
237 res,
238 SqruffOutput {
239 messages: vec![
240 SqruffMessage {
241 code: Some("R001".into()),
242 message: "HasCode".into(),
243 range: Range {
244 start: Position { line: 3, character: 7 },
245 end: Position { line: 3, character: 12 },
246 },
247 severity: DiagnosticSeverity::Warn,
248 source: "sqruff".into(),
249 },
250 SqruffMessage {
251 code: None,
252 message: "NoCode".into(),
253 range: Range {
254 start: Position { line: 1, character: 1 },
255 end: Position { line: 1, character: 2 },
256 },
257 severity: DiagnosticSeverity::Error,
258 source: "sqruff".into(),
259 },
260 ],
261 }
262 );
263 }
264}