1use std::hash::Hash;
2
3use indexmap::IndexMap;
4use serde::Deserialize;
5use serde::Serialize;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct ParseConfigurationError(pub String);
9
10impl std::fmt::Display for ParseConfigurationError {
11 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
12 format!("Found invalid value '{}'.", self.0).fmt(f)
13 }
14}
15
16#[macro_export]
17macro_rules! generate_str_to_from {
18 ($enum_name:ident, $([$member_name:ident, $string_value:expr]),* ) => {
19 impl std::str::FromStr for $enum_name {
20 type Err = ParseConfigurationError;
21
22 fn from_str(s: &str) -> Result<Self, Self::Err> {
23 match s {
24 $($string_value => Ok($enum_name::$member_name)),*,
25 _ => Err(ParseConfigurationError(String::from(s))),
26 }
27 }
28 }
29
30 impl std::fmt::Display for $enum_name {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 match self {
33 $($enum_name::$member_name => write!(f, $string_value)),*,
34 }
35 }
36 }
37 };
38}
39
40#[derive(Clone, PartialEq, Eq, Debug, Copy, Serialize, Deserialize, Hash)]
41pub enum RawNewLineKind {
42 #[serde(rename = "auto")]
44 Auto,
45 #[serde(rename = "lf")]
47 LineFeed,
48 #[serde(rename = "crlf")]
50 CarriageReturnLineFeed,
51 #[serde(rename = "system")]
53 System,
54}
55
56generate_str_to_from![
57 RawNewLineKind,
58 [Auto, "auto"],
59 [LineFeed, "lf"],
60 [CarriageReturnLineFeed, "crlf"],
61 [System, "system"]
62];
63
64#[derive(Clone, PartialEq, Eq, Debug, Copy, Serialize, Deserialize, Hash)]
65pub enum NewLineKind {
66 #[serde(rename = "auto")]
68 Auto,
69 #[serde(rename = "lf")]
71 LineFeed,
72 #[serde(rename = "crlf")]
74 CarriageReturnLineFeed,
75}
76
77generate_str_to_from![NewLineKind, [Auto, "auto"], [LineFeed, "lf"], [CarriageReturnLineFeed, "crlf"]];
78
79#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)]
81#[serde(rename_all = "camelCase")]
82pub struct ConfigurationDiagnostic {
83 pub property_name: String,
85 pub message: String,
87}
88
89impl std::fmt::Display for ConfigurationDiagnostic {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 write!(f, "{} ({})", self.message, self.property_name)
92 }
93}
94
95pub type ConfigKeyMap = IndexMap<String, ConfigKeyValue>;
96
97#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
98#[serde(untagged)]
99pub enum ConfigKeyValue {
100 String(String),
101 Number(i32),
102 Bool(bool),
103 Array(Vec<ConfigKeyValue>),
104 Object(ConfigKeyMap),
105 Null,
106}
107
108impl ConfigKeyValue {
109 pub fn as_string(&self) -> Option<&String> {
110 match self {
111 ConfigKeyValue::String(value) => Some(value),
112 _ => None,
113 }
114 }
115
116 pub fn as_number(&self) -> Option<i32> {
117 match self {
118 ConfigKeyValue::Number(value) => Some(*value),
119 _ => None,
120 }
121 }
122
123 pub fn as_bool(&self) -> Option<bool> {
124 match self {
125 ConfigKeyValue::Bool(value) => Some(*value),
126 _ => None,
127 }
128 }
129
130 pub fn as_array(&self) -> Option<&Vec<ConfigKeyValue>> {
131 match self {
132 ConfigKeyValue::Array(values) => Some(values),
133 _ => None,
134 }
135 }
136
137 pub fn as_object(&self) -> Option<&ConfigKeyMap> {
138 match self {
139 ConfigKeyValue::Object(values) => Some(values),
140 _ => None,
141 }
142 }
143
144 pub fn into_string(self) -> Option<String> {
145 match self {
146 ConfigKeyValue::String(value) => Some(value),
147 _ => None,
148 }
149 }
150
151 pub fn into_number(self) -> Option<i32> {
152 match self {
153 ConfigKeyValue::Number(value) => Some(value),
154 _ => None,
155 }
156 }
157
158 pub fn into_bool(self) -> Option<bool> {
159 match self {
160 ConfigKeyValue::Bool(value) => Some(value),
161 _ => None,
162 }
163 }
164
165 pub fn into_array(self) -> Option<Vec<ConfigKeyValue>> {
166 match self {
167 ConfigKeyValue::Array(values) => Some(values),
168 _ => None,
169 }
170 }
171
172 pub fn into_object(self) -> Option<ConfigKeyMap> {
173 match self {
174 ConfigKeyValue::Object(values) => Some(values),
175 _ => None,
176 }
177 }
178
179 pub fn is_null(&self) -> bool {
180 matches!(self, ConfigKeyValue::Null)
181 }
182
183 #[allow(clippy::should_implement_trait)]
186 pub fn hash(&self, hasher: &mut impl std::hash::Hasher) {
187 match self {
188 ConfigKeyValue::String(value) => {
189 hasher.write_u8(0);
190 hasher.write(value.as_bytes())
191 }
192 ConfigKeyValue::Number(value) => {
193 hasher.write_u8(1);
194 hasher.write_i32(*value)
195 }
196 ConfigKeyValue::Bool(value) => {
197 hasher.write_u8(2);
198 hasher.write_u8(if *value { 1 } else { 0 })
199 }
200 ConfigKeyValue::Array(values) => {
201 hasher.write_u8(3);
202 for value in values {
203 value.hash(hasher);
204 }
205 }
206 ConfigKeyValue::Object(key_values) => {
207 hasher.write_u8(4);
208 for (key, value) in key_values {
209 hasher.write(key.as_bytes());
210 value.hash(hasher);
211 }
212 }
213 ConfigKeyValue::Null => {
214 hasher.write_u8(5);
215 }
216 }
217 }
218
219 pub fn from_i32(value: i32) -> ConfigKeyValue {
220 ConfigKeyValue::Number(value)
221 }
222
223 #[allow(clippy::should_implement_trait)]
224 pub fn from_str(value: &str) -> ConfigKeyValue {
225 ConfigKeyValue::String(value.to_string())
226 }
227
228 pub fn from_bool(value: bool) -> ConfigKeyValue {
229 ConfigKeyValue::Bool(value)
230 }
231}
232
233impl From<i32> for ConfigKeyValue {
234 fn from(item: i32) -> Self {
235 ConfigKeyValue::from_i32(item)
236 }
237}
238
239impl From<bool> for ConfigKeyValue {
240 fn from(item: bool) -> Self {
241 ConfigKeyValue::from_bool(item)
242 }
243}
244
245impl From<String> for ConfigKeyValue {
246 fn from(item: String) -> Self {
247 ConfigKeyValue::from_str(&item)
248 }
249}
250
251impl From<&str> for ConfigKeyValue {
252 fn from(item: &str) -> Self {
253 ConfigKeyValue::from_str(item)
254 }
255}
256
257#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug, Default, Hash)]
258#[serde(rename_all = "camelCase")]
259pub struct GlobalConfiguration {
260 pub line_width: Option<u32>,
261 pub use_tabs: Option<bool>,
262 pub indent_width: Option<u8>,
263 pub new_line_kind: Option<NewLineKind>,
264}
265
266pub const RECOMMENDED_GLOBAL_CONFIGURATION: RecommendedGlobalConfiguration = RecommendedGlobalConfiguration {
267 line_width: 120,
268 indent_width: 2,
269 use_tabs: false,
270 new_line_kind: NewLineKind::LineFeed,
271};
272
273pub struct RecommendedGlobalConfiguration {
274 pub line_width: u32,
275 pub use_tabs: bool,
276 pub indent_width: u8,
277 pub new_line_kind: NewLineKind,
278}
279
280impl From<RecommendedGlobalConfiguration> for GlobalConfiguration {
281 fn from(config: RecommendedGlobalConfiguration) -> Self {
282 Self {
283 line_width: Some(config.line_width),
284 use_tabs: Some(config.use_tabs),
285 indent_width: Some(config.indent_width),
286 new_line_kind: Some(config.new_line_kind),
287 }
288 }
289}
290
291#[derive(Clone, Serialize)]
292#[serde(rename_all = "camelCase")]
293pub struct ResolveConfigurationResult<T>
294where
295 T: Clone + Serialize,
296{
297 pub diagnostics: Vec<ConfigurationDiagnostic>,
299
300 pub config: T,
303}
304
305pub fn resolve_global_config(config: &mut ConfigKeyMap) -> ResolveConfigurationResult<GlobalConfiguration> {
307 let mut diagnostics = Vec::new();
308
309 let raw_new_line_kind = get_nullable_value::<RawNewLineKind>(config, "newLineKind", &mut diagnostics);
310
311 let resolved_config = GlobalConfiguration {
312 line_width: get_nullable_value(config, "lineWidth", &mut diagnostics),
313 use_tabs: get_nullable_value(config, "useTabs", &mut diagnostics),
314 indent_width: get_nullable_value(config, "indentWidth", &mut diagnostics),
315 new_line_kind: raw_new_line_kind.map(|kind| match kind {
316 RawNewLineKind::Auto => NewLineKind::Auto,
317 RawNewLineKind::LineFeed => NewLineKind::LineFeed,
318 RawNewLineKind::CarriageReturnLineFeed => NewLineKind::CarriageReturnLineFeed,
319 RawNewLineKind::System => {
320 if cfg!(windows) {
321 NewLineKind::CarriageReturnLineFeed
322 } else {
323 NewLineKind::LineFeed
324 }
325 }
326 }),
327 };
328
329 ResolveConfigurationResult {
330 config: resolved_config,
331 diagnostics,
332 }
333}
334
335pub fn get_value<T>(config: &mut ConfigKeyMap, key: &str, default_value: T, diagnostics: &mut Vec<ConfigurationDiagnostic>) -> T
339where
340 T: std::str::FromStr,
341 <T as std::str::FromStr>::Err: std::fmt::Display,
342{
343 get_nullable_value(config, key, diagnostics).unwrap_or(default_value)
344}
345
346pub fn get_nullable_value<T>(config: &mut ConfigKeyMap, key: &str, diagnostics: &mut Vec<ConfigurationDiagnostic>) -> Option<T>
350where
351 T: std::str::FromStr,
352 <T as std::str::FromStr>::Err: std::fmt::Display,
353{
354 if let Some(raw_value) = config.shift_remove(key) {
355 let parsed_value = match raw_value {
357 ConfigKeyValue::Bool(value) => value.to_string().parse::<T>().map_err(|e| e.to_string()),
358 ConfigKeyValue::Number(value) => value.to_string().parse::<T>().map_err(|e| e.to_string()),
359 ConfigKeyValue::String(value) => value.parse::<T>().map_err(|e| e.to_string()),
360 ConfigKeyValue::Object(_) | ConfigKeyValue::Array(_) => Err("Arrays and objects are not supported for this value".to_string()),
361 ConfigKeyValue::Null => return None,
362 };
363 match parsed_value {
364 Ok(parsed_value) => Some(parsed_value),
365 Err(message) => {
366 diagnostics.push(ConfigurationDiagnostic {
367 property_name: key.to_string(),
368 message,
369 });
370 None
371 }
372 }
373 } else {
374 None
375 }
376}
377
378pub fn get_nullable_vec<T: std::str::FromStr>(
379 config: &mut ConfigKeyMap,
380 key: &str,
381 get_nullable_value: impl Fn(ConfigKeyValue, usize, &mut Vec<ConfigurationDiagnostic>) -> Option<T>,
382 diagnostics: &mut Vec<ConfigurationDiagnostic>,
383) -> Option<Vec<T>> {
384 match config.shift_remove(key) {
385 Some(value) => match value {
386 ConfigKeyValue::Array(values) => {
387 let mut result = Vec::with_capacity(values.len());
388 for (i, value) in values.into_iter().enumerate() {
389 if let Some(value) = get_nullable_value(value, i, diagnostics) {
390 result.push(value);
391 }
392 }
393 Some(result)
394 }
395 _ => {
396 diagnostics.push(ConfigurationDiagnostic {
397 property_name: key.to_string(),
398 message: "Expected an array.".to_string(),
399 });
400 None
401 }
402 },
403 None => None,
404 }
405}
406
407pub fn handle_renamed_config_property(config: &mut ConfigKeyMap, old_key: &str, new_key: &str, diagnostics: &mut Vec<ConfigurationDiagnostic>) {
410 if let Some(raw_value) = config.shift_remove(old_key) {
411 if !config.contains_key(new_key) {
412 config.insert(new_key.to_string(), raw_value);
413 }
414 diagnostics.push(ConfigurationDiagnostic {
415 property_name: old_key.to_string(),
416 message: format!("The configuration key was renamed to '{}'", new_key),
417 });
418 }
419}
420
421pub fn resolve_new_line_kind(file_text: &str, new_line_kind: NewLineKind) -> &'static str {
423 match new_line_kind {
424 NewLineKind::LineFeed => "\n",
425 NewLineKind::CarriageReturnLineFeed => "\r\n",
426 NewLineKind::Auto => {
427 let mut found_slash_n = false;
428 for c in file_text.as_bytes().iter().rev() {
429 if found_slash_n {
430 if c == &(b'\r') {
431 return "\r\n";
432 } else {
433 return "\n";
434 }
435 }
436
437 if c == &(b'\n') {
438 found_slash_n = true;
439 }
440 }
441
442 "\n"
443 }
444 }
445}
446
447pub fn get_unknown_property_diagnostics(config: ConfigKeyMap) -> Vec<ConfigurationDiagnostic> {
451 let mut diagnostics = Vec::new();
452 for (key, _) in config {
453 diagnostics.push(ConfigurationDiagnostic {
454 property_name: key.to_string(),
455 message: "Unknown property in configuration".to_string(),
456 });
457 }
458 diagnostics
459}
460
461#[cfg(test)]
462mod test {
463 use super::*;
464
465 #[test]
466 fn get_default_config_when_empty() {
467 let config_result = resolve_global_config(&mut ConfigKeyMap::new());
468 let config = config_result.config;
469 assert_eq!(config_result.diagnostics.len(), 0);
470 assert_eq!(config.line_width, None);
471 assert_eq!(config.indent_width, None);
472 assert!(config.new_line_kind.is_none());
473 assert_eq!(config.use_tabs, None);
474 }
475
476 #[test]
477 fn get_values_when_filled() {
478 let mut global_config = ConfigKeyMap::from([
479 (String::from("lineWidth"), ConfigKeyValue::from_i32(80)),
480 (String::from("indentWidth"), ConfigKeyValue::from_i32(8)),
481 (String::from("newLineKind"), ConfigKeyValue::from_str("crlf")),
482 (String::from("useTabs"), ConfigKeyValue::from_bool(true)),
483 ]);
484 let config_result = resolve_global_config(&mut global_config);
485 let config = config_result.config;
486 assert_eq!(config_result.diagnostics.len(), 0);
487 assert_eq!(config.line_width, Some(80));
488 assert_eq!(config.indent_width, Some(8));
489 assert_eq!(config.new_line_kind, Some(NewLineKind::CarriageReturnLineFeed));
490 assert_eq!(config.use_tabs, Some(true));
491 }
492
493 #[test]
494 fn get_diagnostic_for_invalid_enum_config() {
495 let mut global_config = ConfigKeyMap::from([(String::from("newLineKind"), ConfigKeyValue::from_str("something"))]);
496 let diagnostics = resolve_global_config(&mut global_config).diagnostics;
497 assert_eq!(diagnostics.len(), 1);
498 assert_eq!(diagnostics[0].message, "Found invalid value 'something'.");
499 assert_eq!(diagnostics[0].property_name, "newLineKind");
500 }
501
502 #[test]
503 fn get_diagnostic_for_invalid_primitive() {
504 let mut global_config = ConfigKeyMap::from([(String::from("useTabs"), ConfigKeyValue::from_str("something"))]);
505 let diagnostics = resolve_global_config(&mut global_config).diagnostics;
506 assert_eq!(diagnostics.len(), 1);
507 assert_eq!(diagnostics[0].message, "provided string was not `true` or `false`");
508 assert_eq!(diagnostics[0].property_name, "useTabs");
509 }
510
511 #[test]
512 fn get_diagnostic_for_excess_property() {
513 let global_config = ConfigKeyMap::from([(String::from("something"), ConfigKeyValue::from_str("value"))]);
514 let diagnostics = get_unknown_property_diagnostics(global_config);
515 assert_eq!(diagnostics.len(), 1);
516 assert_eq!(diagnostics[0].message, "Unknown property in configuration");
517 assert_eq!(diagnostics[0].property_name, "something");
518 }
519
520 #[test]
521 fn add_diagnostic_for_renamed_property() {
522 let mut config = ConfigKeyMap::new();
523 let mut diagnostics = Vec::new();
524 config.insert("oldProp".to_string(), ConfigKeyValue::from_str("value"));
525 handle_renamed_config_property(&mut config, "oldProp", "newProp", &mut diagnostics);
526 assert_eq!(config.len(), 1);
527 assert_eq!(config.shift_remove("newProp").unwrap(), ConfigKeyValue::from_str("value"));
528 assert_eq!(diagnostics.len(), 1);
529 assert_eq!(diagnostics[0].message, "The configuration key was renamed to 'newProp'");
530 assert_eq!(diagnostics[0].property_name, "oldProp");
531 }
532
533 #[test]
534 fn add_diagnostic_for_renamed_property_when_already_exists() {
535 let mut config = ConfigKeyMap::new();
536 let mut diagnostics = Vec::new();
537 config.insert("oldProp".to_string(), ConfigKeyValue::from_str("new_value"));
538 config.insert("newProp".to_string(), ConfigKeyValue::from_str("value"));
539 handle_renamed_config_property(&mut config, "oldProp", "newProp", &mut diagnostics);
540 assert_eq!(config.len(), 1);
541 assert_eq!(config.shift_remove("newProp").unwrap(), ConfigKeyValue::from_str("value"));
542 assert_eq!(diagnostics.len(), 1);
543 assert_eq!(diagnostics[0].message, "The configuration key was renamed to 'newProp'");
544 assert_eq!(diagnostics[0].property_name, "oldProp");
545 }
546}