1use nvim_oxi::Dictionary;
4use serde::Deserialize;
5
6use crate::diagnostics::DiagnosticSeverity;
7
8pub fn dict() -> Dictionary {
10 dict! {
11 "sqruff": dict! {
12 "parser": fn_from!(parser)
13 },
14 }
15}
16
17#[allow(clippy::needless_pass_by_value)]
19fn parser(maybe_output: Option<nvim_oxi::String>) -> Vec<Dictionary> {
20 let Some(output) = &maybe_output else {
21 ytil_noxi::notify::warn(format!("sqruff output missing output={maybe_output:?}"));
22 return vec![];
23 };
24 let output = output.to_string_lossy();
25
26 if output.trim().is_empty() {
27 ytil_noxi::notify::warn(format!("sqruff output is an empty string output={maybe_output:?}"));
28 return vec![];
29 }
30
31 let parsed_output = match serde_json::from_str::<SqruffOutput>(&output) {
32 Ok(parsed_output) => parsed_output,
33 Err(err) => {
34 ytil_noxi::notify::error(format!(
35 "error parsing sqruff output | output={output:?} error={err:#?}"
36 ));
37 return vec![];
38 }
39 };
40
41 parsed_output
42 .messages
43 .into_iter()
44 .map(diagnostic_dict_from_msg)
45 .collect()
46}
47
48#[derive(Debug, Deserialize)]
50#[cfg_attr(test, derive(Eq, PartialEq))]
51struct SqruffOutput {
52 #[serde(rename = "<string>", default)]
53 messages: Vec<SqruffMessage>,
54}
55
56#[derive(Debug, Deserialize)]
58#[cfg_attr(test, derive(Eq, PartialEq))]
59struct SqruffMessage {
60 code: Option<String>,
61 message: String,
62 range: Range,
63 severity: DiagnosticSeverity,
64 source: String,
65}
66
67#[derive(Debug, Deserialize)]
69#[cfg_attr(test, derive(Eq, PartialEq))]
70struct Range {
71 start: Position,
72 end: Position,
73}
74
75#[derive(Debug, Deserialize)]
77#[cfg_attr(test, derive(Eq, PartialEq))]
78struct Position {
79 character: u32,
80 line: u32,
81}
82
83fn diagnostic_dict_from_msg(msg: SqruffMessage) -> Dictionary {
85 dict! {
86 "lnum": msg.range.start.line.saturating_sub(1),
87 "end_lnum": msg.range.end.line.saturating_sub(1),
88 "col": msg.range.start.character.saturating_sub(1),
89 "end_col": msg.range.end.character.saturating_sub(1),
90 "message": msg.message,
91 "code": msg.code.map_or_else(nvim_oxi::Object::nil, nvim_oxi::Object::from),
92 "source": msg.source,
93 "severity": msg.severity.to_number(),
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use nvim_oxi::Object;
100
101 use super::*;
102
103 #[test]
104 fn diagnostic_dict_from_msg_returns_the_expected_dict_from_msg() {
105 let msg = SqruffMessage {
106 code: Some("R001".to_string()),
107 message: "Example message".to_string(),
108 range: Range {
109 start: Position { line: 3, character: 7 },
110 end: Position { line: 4, character: 10 },
111 },
112 severity: DiagnosticSeverity::Warn,
113 source: "sqruff".to_string(),
114 };
115
116 let res = diagnostic_dict_from_msg(msg);
117
118 let expected = dict! {
119 "lnum": 2,
120 "end_lnum": 3,
121 "col": 6,
122 "end_col": 9,
123 "message": "Example message".to_string(),
124 "code": Object::from(nvim_oxi::String::from("R001")),
125 "source": "sqruff".to_string(),
126 "severity": DiagnosticSeverity::Warn.to_number(),
127 };
128 pretty_assertions::assert_eq!(res, expected);
129 }
130
131 #[test]
132 fn sqruff_output_deserializes_empty_messages() {
133 let value = serde_json::json!({
134 "<string>": []
135 });
136
137 assert2::assert!(let Ok(parsed) = serde_json::from_value::<SqruffOutput>(value));
138 pretty_assertions::assert_eq!(parsed, SqruffOutput { messages: vec![] });
139 }
140
141 #[test]
142 fn sqruff_output_deserializes_single_message_with_code() {
143 let value = serde_json::json!({
144 "<string>": [
145 {
146 "code": "R001",
147 "message": "Msg",
148 "range": {"start": {"line": 2, "character": 5}, "end": {"line": 2, "character": 10}},
149 "severity": "2",
150 "source": "sqruff"
151 }
152 ]
153 });
154
155 assert2::assert!(let Ok(res) = serde_json::from_value::<SqruffOutput>(value));
156 pretty_assertions::assert_eq!(
157 res,
158 SqruffOutput {
159 messages: vec![SqruffMessage {
160 code: Some("R001".into()),
161 message: "Msg".into(),
162 range: Range {
163 start: Position { line: 2, character: 5 },
164 end: Position { line: 2, character: 10 },
165 },
166 severity: DiagnosticSeverity::Warn,
167 source: "sqruff".into(),
168 }],
169 }
170 );
171 }
172
173 #[test]
174 fn sqruff_output_deserializes_multiple_messages_mixed_code() {
175 let value = serde_json::json!({
176 "<string>": [
177 {
178 "code": "R001",
179 "message": "HasCode",
180 "range": {"start": {"line": 3, "character": 7}, "end": {"line": 3, "character": 12}},
181 "severity": "2",
182 "source": "sqruff"
183 },
184 {
185 "code": null,
186 "message": "NoCode",
187 "range": {"start": {"line": 1, "character": 1}, "end": {"line": 1, "character": 2}},
188 "severity": "1",
189 "source": "sqruff"
190 }
191 ]
192 });
193
194 assert2::assert!(let Ok(res) = serde_json::from_value::<SqruffOutput>(value));
195 pretty_assertions::assert_eq!(
196 res,
197 SqruffOutput {
198 messages: vec![
199 SqruffMessage {
200 code: Some("R001".into()),
201 message: "HasCode".into(),
202 range: Range {
203 start: Position { line: 3, character: 7 },
204 end: Position { line: 3, character: 12 },
205 },
206 severity: DiagnosticSeverity::Warn,
207 source: "sqruff".into(),
208 },
209 SqruffMessage {
210 code: None,
211 message: "NoCode".into(),
212 range: Range {
213 start: Position { line: 1, character: 1 },
214 end: Position { line: 1, character: 2 },
215 },
216 severity: DiagnosticSeverity::Error,
217 source: "sqruff".into(),
218 },
219 ],
220 }
221 );
222 }
223}