1use core::fmt;
7
8use nvim_oxi::Dictionary;
9use serde::de::Deserializer;
10use serde::de::Visitor;
11use strum::EnumCount;
12use strum::EnumIter;
13
14mod config;
15mod filter;
16mod filters;
17mod formatter;
18mod sorter;
19
20pub fn dict() -> Dictionary {
28 dict! {
29 "filter": fn_from!(filter::filter),
30 "sort": fn_from!(sorter::sort),
31 "format": fn_from!(formatter::format),
32 "config": config::get()
33 }
34}
35
36#[derive(Clone, Copy, Debug, EnumCount, EnumIter, Eq, Hash, PartialEq)]
45#[allow(clippy::upper_case_acronyms)]
46pub enum DiagnosticSeverity {
47 Error,
49 Warn,
51 Info,
53 Hint,
55 Other,
57}
58
59impl DiagnosticSeverity {
60 pub const VARIANT_COUNT: usize = <Self as strum::EnumCount>::COUNT;
62
63 pub const fn to_number(self) -> u8 {
79 match self {
80 Self::Error => 1,
81 Self::Warn => 2,
82 Self::Info => 3,
83 Self::Hint => 4,
84 Self::Other => 0,
85 }
86 }
87
88 pub const fn symbol(self) -> &'static str {
95 match self {
96 Self::Error => "x",
97 Self::Warn => "⏶",
98 Self::Info => "◆",
99 Self::Hint | Self::Other => "",
100 }
101 }
102}
103
104impl<'de> serde::Deserialize<'de> for DiagnosticSeverity {
110 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
111 where
112 D: Deserializer<'de>,
113 {
114 struct SevVisitor;
115
116 impl Visitor<'_> for SevVisitor {
117 type Value = DiagnosticSeverity;
118
119 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
120 write!(f, "a severity: 1-4 or (error|warn|info|hint) or short alias")
121 }
122
123 fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
124 where
125 E: serde::de::Error,
126 {
127 Ok(match v {
128 1 => DiagnosticSeverity::Error,
129 2 => DiagnosticSeverity::Warn,
130 3 => DiagnosticSeverity::Info,
131 4 => DiagnosticSeverity::Hint,
132 _ => DiagnosticSeverity::Other,
133 })
134 }
135
136 fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
137 where
138 E: serde::de::Error,
139 {
140 if v < 0 {
141 return Ok(DiagnosticSeverity::Other);
142 }
143 self.visit_u64(v.cast_unsigned())
144 }
145
146 fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
147 where
148 E: serde::de::Error,
149 {
150 let norm = s.trim().to_ascii_lowercase();
151 if let Ok(n) = norm.parse::<u64>() {
152 return self.visit_u64(n);
153 }
154 Ok(match norm.as_str() {
155 "error" | "err" | "e" => DiagnosticSeverity::Error,
156 "warn" | "warning" | "w" => DiagnosticSeverity::Warn,
157 "info" | "information" | "i" => DiagnosticSeverity::Info,
158 "hint" | "h" => DiagnosticSeverity::Hint,
159 _ => DiagnosticSeverity::Other,
160 })
161 }
162 }
163
164 deserializer.deserialize_any(SevVisitor)
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use rstest::rstest;
171 use strum::IntoEnumIterator;
172
173 use super::*;
174
175 #[rstest]
176 #[case("1", DiagnosticSeverity::Error)]
177 #[case("2", DiagnosticSeverity::Warn)]
178 #[case("3", DiagnosticSeverity::Info)]
179 #[case("4", DiagnosticSeverity::Hint)]
180 #[case("\"1\"", DiagnosticSeverity::Error)]
181 #[case("\"2\"", DiagnosticSeverity::Warn)]
182 #[case("\"3\"", DiagnosticSeverity::Info)]
183 #[case("\"4\"", DiagnosticSeverity::Hint)]
184 #[case("\"error\"", DiagnosticSeverity::Error)]
185 #[case("\"err\"", DiagnosticSeverity::Error)]
186 #[case("\"e\"", DiagnosticSeverity::Error)]
187 #[case("\"warn\"", DiagnosticSeverity::Warn)]
188 #[case("\"warning\"", DiagnosticSeverity::Warn)]
189 #[case("\"w\"", DiagnosticSeverity::Warn)]
190 #[case("\"info\"", DiagnosticSeverity::Info)]
191 #[case("\"information\"", DiagnosticSeverity::Info)]
192 #[case("\"i\"", DiagnosticSeverity::Info)]
193 #[case("\"hint\"", DiagnosticSeverity::Hint)]
194 #[case("\"h\"", DiagnosticSeverity::Hint)]
195 #[case("\" Error \"", DiagnosticSeverity::Error)]
196 #[case("\"WARNING\"", DiagnosticSeverity::Warn)]
197 #[case("\" Info \"", DiagnosticSeverity::Info)]
198 #[case("\"H\"", DiagnosticSeverity::Hint)]
199 #[case("-1", DiagnosticSeverity::Other)]
200 #[case("0", DiagnosticSeverity::Other)]
201 #[case("5", DiagnosticSeverity::Other)]
202 #[case("\"unknown\"", DiagnosticSeverity::Other)]
203 fn diagnostic_severity_deserializes_strings_as_expected(#[case] input: &str, #[case] expected: DiagnosticSeverity) {
204 assert2::let_assert!(Ok(sev) = serde_json::from_str::<DiagnosticSeverity>(input));
205 assert_eq!(sev, expected);
206 }
207
208 #[test]
209 fn diagnostic_severity_when_iterated_via_enumiter_yields_declared_order_and_matches_variant_count() {
210 let expected = [
211 DiagnosticSeverity::Error,
212 DiagnosticSeverity::Warn,
213 DiagnosticSeverity::Info,
214 DiagnosticSeverity::Hint,
215 DiagnosticSeverity::Other,
216 ];
217 let collected: Vec<DiagnosticSeverity> = DiagnosticSeverity::iter().collect();
218 pretty_assertions::assert_eq!(collected.as_slice(), expected.as_slice());
219 pretty_assertions::assert_eq!(collected.len(), DiagnosticSeverity::VARIANT_COUNT);
220 }
221
222 #[rstest]
223 #[case(1_i64, DiagnosticSeverity::Error)]
224 #[case(2_i64, DiagnosticSeverity::Warn)]
225 #[case(3_i64, DiagnosticSeverity::Info)]
226 #[case(4_i64, DiagnosticSeverity::Hint)]
227 #[case(-1_i64, DiagnosticSeverity::Other)]
228 #[case(0_i64, DiagnosticSeverity::Other)]
229 #[case(5_i64, DiagnosticSeverity::Other)]
230 fn diagnostic_severity_deserializes_numeric_values_as_expected(
231 #[case] input: i64,
232 #[case] expected: DiagnosticSeverity,
233 ) {
234 let json = input.to_string();
235 assert2::let_assert!(Ok(sev) = serde_json::from_str::<DiagnosticSeverity>(&json));
236 assert_eq!(sev, expected);
237 }
238
239 #[test]
240 fn diagnostic_severity_deserializes_invalid_json_errors() {
241 assert2::let_assert!(Err(err) = serde_json::from_str::<DiagnosticSeverity>("error"));
242 let msg = err.to_string();
243 assert!(msg.contains("expected value"), "unexpected error message: {msg}");
244 }
245}