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_usize(value.len());
191 hasher.write(value.as_bytes())
192 }
193 ConfigKeyValue::Number(value) => {
194 hasher.write_u8(1);
195 hasher.write_i32(*value)
196 }
197 ConfigKeyValue::Bool(value) => {
198 hasher.write_u8(2);
199 hasher.write_u8(if *value { 1 } else { 0 })
200 }
201 ConfigKeyValue::Array(values) => {
202 hasher.write_u8(3);
203 hasher.write_usize(values.len());
204 for value in values {
205 value.hash(hasher);
206 }
207 }
208 ConfigKeyValue::Object(key_values) => {
209 hasher.write_u8(4);
210 hasher.write_usize(key_values.len());
211 for (key, value) in key_values {
212 hasher.write_usize(key.len());
213 hasher.write(key.as_bytes());
214 value.hash(hasher);
215 }
216 }
217 ConfigKeyValue::Null => {
218 hasher.write_u8(5);
219 }
220 }
221 }
222
223 pub fn from_i32(value: i32) -> ConfigKeyValue {
224 ConfigKeyValue::Number(value)
225 }
226
227 #[allow(clippy::should_implement_trait)]
228 pub fn from_str(value: &str) -> ConfigKeyValue {
229 ConfigKeyValue::String(value.to_string())
230 }
231
232 pub fn from_bool(value: bool) -> ConfigKeyValue {
233 ConfigKeyValue::Bool(value)
234 }
235}
236
237impl From<i32> for ConfigKeyValue {
238 fn from(item: i32) -> Self {
239 ConfigKeyValue::from_i32(item)
240 }
241}
242
243impl From<bool> for ConfigKeyValue {
244 fn from(item: bool) -> Self {
245 ConfigKeyValue::from_bool(item)
246 }
247}
248
249impl From<String> for ConfigKeyValue {
250 fn from(item: String) -> Self {
251 ConfigKeyValue::from_str(&item)
252 }
253}
254
255impl From<&str> for ConfigKeyValue {
256 fn from(item: &str) -> Self {
257 ConfigKeyValue::from_str(item)
258 }
259}
260
261#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug, Default, Hash)]
262#[serde(rename_all = "camelCase")]
263pub struct GlobalConfiguration {
264 pub line_width: Option<u32>,
265 pub use_tabs: Option<bool>,
266 pub indent_width: Option<u8>,
267 pub new_line_kind: Option<NewLineKind>,
268}
269
270pub const RECOMMENDED_GLOBAL_CONFIGURATION: RecommendedGlobalConfiguration = RecommendedGlobalConfiguration {
271 line_width: 120,
272 indent_width: 2,
273 use_tabs: false,
274 new_line_kind: NewLineKind::LineFeed,
275};
276
277pub struct RecommendedGlobalConfiguration {
278 pub line_width: u32,
279 pub use_tabs: bool,
280 pub indent_width: u8,
281 pub new_line_kind: NewLineKind,
282}
283
284impl From<RecommendedGlobalConfiguration> for GlobalConfiguration {
285 fn from(config: RecommendedGlobalConfiguration) -> Self {
286 Self {
287 line_width: Some(config.line_width),
288 use_tabs: Some(config.use_tabs),
289 indent_width: Some(config.indent_width),
290 new_line_kind: Some(config.new_line_kind),
291 }
292 }
293}
294
295#[derive(Clone, Serialize)]
296#[serde(rename_all = "camelCase")]
297pub struct ResolveConfigurationResult<T>
298where
299 T: Clone + Serialize,
300{
301 pub diagnostics: Vec<ConfigurationDiagnostic>,
303
304 pub config: T,
307}
308
309pub fn resolve_global_config(config: &mut ConfigKeyMap) -> ResolveConfigurationResult<GlobalConfiguration> {
311 let mut diagnostics = Vec::new();
312
313 let raw_new_line_kind = get_nullable_value::<RawNewLineKind>(config, "newLineKind", &mut diagnostics);
314
315 let resolved_config = GlobalConfiguration {
316 line_width: get_nullable_value(config, "lineWidth", &mut diagnostics),
317 use_tabs: get_nullable_value(config, "useTabs", &mut diagnostics),
318 indent_width: get_nullable_value(config, "indentWidth", &mut diagnostics),
319 new_line_kind: raw_new_line_kind.map(|kind| match kind {
320 RawNewLineKind::Auto => NewLineKind::Auto,
321 RawNewLineKind::LineFeed => NewLineKind::LineFeed,
322 RawNewLineKind::CarriageReturnLineFeed => NewLineKind::CarriageReturnLineFeed,
323 RawNewLineKind::System => {
324 if cfg!(windows) {
325 NewLineKind::CarriageReturnLineFeed
326 } else {
327 NewLineKind::LineFeed
328 }
329 }
330 }),
331 };
332
333 ResolveConfigurationResult {
334 config: resolved_config,
335 diagnostics,
336 }
337}
338
339pub fn get_value<T>(config: &mut ConfigKeyMap, key: &str, default_value: T, diagnostics: &mut Vec<ConfigurationDiagnostic>) -> T
343where
344 T: std::str::FromStr,
345 <T as std::str::FromStr>::Err: std::fmt::Display,
346{
347 get_nullable_value(config, key, diagnostics).unwrap_or(default_value)
348}
349
350pub fn get_nullable_value<T>(config: &mut ConfigKeyMap, key: &str, diagnostics: &mut Vec<ConfigurationDiagnostic>) -> Option<T>
354where
355 T: std::str::FromStr,
356 <T as std::str::FromStr>::Err: std::fmt::Display,
357{
358 if let Some(raw_value) = config.shift_remove(key) {
359 let parsed_value = match raw_value {
361 ConfigKeyValue::Bool(value) => value.to_string().parse::<T>().map_err(|e| e.to_string()),
362 ConfigKeyValue::Number(value) => value.to_string().parse::<T>().map_err(|e| e.to_string()),
363 ConfigKeyValue::String(value) => value.parse::<T>().map_err(|e| e.to_string()),
364 ConfigKeyValue::Object(_) | ConfigKeyValue::Array(_) => Err("Arrays and objects are not supported for this value".to_string()),
365 ConfigKeyValue::Null => return None,
366 };
367 match parsed_value {
368 Ok(parsed_value) => Some(parsed_value),
369 Err(message) => {
370 diagnostics.push(ConfigurationDiagnostic {
371 property_name: key.to_string(),
372 message,
373 });
374 None
375 }
376 }
377 } else {
378 None
379 }
380}
381
382pub fn get_nullable_vec<T: std::str::FromStr>(
383 config: &mut ConfigKeyMap,
384 key: &str,
385 get_nullable_value: impl Fn(ConfigKeyValue, usize, &mut Vec<ConfigurationDiagnostic>) -> Option<T>,
386 diagnostics: &mut Vec<ConfigurationDiagnostic>,
387) -> Option<Vec<T>> {
388 match config.shift_remove(key) {
389 Some(value) => match value {
390 ConfigKeyValue::Array(values) => {
391 let mut result = Vec::with_capacity(values.len());
392 for (i, value) in values.into_iter().enumerate() {
393 if let Some(value) = get_nullable_value(value, i, diagnostics) {
394 result.push(value);
395 }
396 }
397 Some(result)
398 }
399 _ => {
400 diagnostics.push(ConfigurationDiagnostic {
401 property_name: key.to_string(),
402 message: "Expected an array.".to_string(),
403 });
404 None
405 }
406 },
407 None => None,
408 }
409}
410
411pub fn handle_renamed_config_property(config: &mut ConfigKeyMap, old_key: &str, new_key: &str, diagnostics: &mut Vec<ConfigurationDiagnostic>) {
414 if let Some(raw_value) = config.shift_remove(old_key) {
415 if !config.contains_key(new_key) {
416 config.insert(new_key.to_string(), raw_value);
417 }
418 diagnostics.push(ConfigurationDiagnostic {
419 property_name: old_key.to_string(),
420 message: format!("The configuration key was renamed to '{}'", new_key),
421 });
422 }
423}
424
425pub fn resolve_new_line_kind(file_text: &str, new_line_kind: NewLineKind) -> &'static str {
427 match new_line_kind {
428 NewLineKind::LineFeed => "\n",
429 NewLineKind::CarriageReturnLineFeed => "\r\n",
430 NewLineKind::Auto => {
431 let mut found_slash_n = false;
432 for c in file_text.as_bytes().iter().rev() {
433 if found_slash_n {
434 if c == &(b'\r') {
435 return "\r\n";
436 } else {
437 return "\n";
438 }
439 }
440
441 if c == &(b'\n') {
442 found_slash_n = true;
443 }
444 }
445
446 "\n"
447 }
448 }
449}
450
451pub fn get_unknown_property_diagnostics(config: ConfigKeyMap) -> Vec<ConfigurationDiagnostic> {
455 let mut diagnostics = Vec::new();
456 for (key, _) in config {
457 diagnostics.push(ConfigurationDiagnostic {
458 property_name: key.to_string(),
459 message: "Unknown property in configuration".to_string(),
460 });
461 }
462 diagnostics
463}
464
465#[cfg(test)]
466mod test {
467 use super::*;
468
469 #[test]
470 fn config_key_value_hash_includes_lengths() {
471 fn get_hash(value: ConfigKeyValue) -> u64 {
472 use std::hash::Hasher;
473 let mut hasher = std::collections::hash_map::DefaultHasher::new();
474 value.hash(&mut hasher);
475 hasher.finish()
476 }
477
478 fn string(value: &str) -> ConfigKeyValue {
479 ConfigKeyValue::String(value.to_string())
480 }
481
482 fn object(key_values: Vec<(&str, ConfigKeyValue)>) -> ConfigKeyValue {
483 ConfigKeyValue::Object(key_values.into_iter().map(|(key, value)| (key.to_string(), value)).collect())
484 }
485
486 let number = ConfigKeyValue::Number;
487 let array = ConfigKeyValue::Array;
488
489 assert_ne!(
491 get_hash(array(vec![array(vec![number(1)]), number(2)])),
492 get_hash(array(vec![array(vec![number(1), number(2)])]))
493 );
494 assert_ne!(get_hash(array(vec![string("a"), string("b")])), get_hash(array(vec![string("a\0b")])));
496 assert_ne!(
498 get_hash(array(vec![object(vec![("a", number(1))]), number(2)])),
499 get_hash(array(vec![object(vec![("a", number(1)), ("\u{1}\u{2}\0\0", number(0))])]))
500 );
501 assert_ne!(
503 get_hash(object(vec![("a", string("b"))])),
504 get_hash(object(vec![("a\0\u{1}\0\0\0\0\0\0\0b", ConfigKeyValue::Null)]))
505 );
506
507 assert_eq!(
508 get_hash(array(vec![string("a"), object(vec![("b", number(1))])])),
509 get_hash(array(vec![string("a"), object(vec![("b", number(1))])]))
510 );
511 }
512
513 #[test]
514 fn get_default_config_when_empty() {
515 let config_result = resolve_global_config(&mut ConfigKeyMap::new());
516 let config = config_result.config;
517 assert_eq!(config_result.diagnostics.len(), 0);
518 assert_eq!(config.line_width, None);
519 assert_eq!(config.indent_width, None);
520 assert!(config.new_line_kind.is_none());
521 assert_eq!(config.use_tabs, None);
522 }
523
524 #[test]
525 fn get_values_when_filled() {
526 let mut global_config = ConfigKeyMap::from([
527 (String::from("lineWidth"), ConfigKeyValue::from_i32(80)),
528 (String::from("indentWidth"), ConfigKeyValue::from_i32(8)),
529 (String::from("newLineKind"), ConfigKeyValue::from_str("crlf")),
530 (String::from("useTabs"), ConfigKeyValue::from_bool(true)),
531 ]);
532 let config_result = resolve_global_config(&mut global_config);
533 let config = config_result.config;
534 assert_eq!(config_result.diagnostics.len(), 0);
535 assert_eq!(config.line_width, Some(80));
536 assert_eq!(config.indent_width, Some(8));
537 assert_eq!(config.new_line_kind, Some(NewLineKind::CarriageReturnLineFeed));
538 assert_eq!(config.use_tabs, Some(true));
539 }
540
541 #[test]
542 fn get_diagnostic_for_invalid_enum_config() {
543 let mut global_config = ConfigKeyMap::from([(String::from("newLineKind"), ConfigKeyValue::from_str("something"))]);
544 let diagnostics = resolve_global_config(&mut global_config).diagnostics;
545 assert_eq!(diagnostics.len(), 1);
546 assert_eq!(diagnostics[0].message, "Found invalid value 'something'.");
547 assert_eq!(diagnostics[0].property_name, "newLineKind");
548 }
549
550 #[test]
551 fn get_diagnostic_for_invalid_primitive() {
552 let mut global_config = ConfigKeyMap::from([(String::from("useTabs"), ConfigKeyValue::from_str("something"))]);
553 let diagnostics = resolve_global_config(&mut global_config).diagnostics;
554 assert_eq!(diagnostics.len(), 1);
555 assert_eq!(diagnostics[0].message, "provided string was not `true` or `false`");
556 assert_eq!(diagnostics[0].property_name, "useTabs");
557 }
558
559 #[test]
560 fn get_diagnostic_for_excess_property() {
561 let global_config = ConfigKeyMap::from([(String::from("something"), ConfigKeyValue::from_str("value"))]);
562 let diagnostics = get_unknown_property_diagnostics(global_config);
563 assert_eq!(diagnostics.len(), 1);
564 assert_eq!(diagnostics[0].message, "Unknown property in configuration");
565 assert_eq!(diagnostics[0].property_name, "something");
566 }
567
568 #[test]
569 fn add_diagnostic_for_renamed_property() {
570 let mut config = ConfigKeyMap::new();
571 let mut diagnostics = Vec::new();
572 config.insert("oldProp".to_string(), ConfigKeyValue::from_str("value"));
573 handle_renamed_config_property(&mut config, "oldProp", "newProp", &mut diagnostics);
574 assert_eq!(config.len(), 1);
575 assert_eq!(config.shift_remove("newProp").unwrap(), ConfigKeyValue::from_str("value"));
576 assert_eq!(diagnostics.len(), 1);
577 assert_eq!(diagnostics[0].message, "The configuration key was renamed to 'newProp'");
578 assert_eq!(diagnostics[0].property_name, "oldProp");
579 }
580
581 #[test]
582 fn add_diagnostic_for_renamed_property_when_already_exists() {
583 let mut config = ConfigKeyMap::new();
584 let mut diagnostics = Vec::new();
585 config.insert("oldProp".to_string(), ConfigKeyValue::from_str("new_value"));
586 config.insert("newProp".to_string(), ConfigKeyValue::from_str("value"));
587 handle_renamed_config_property(&mut config, "oldProp", "newProp", &mut diagnostics);
588 assert_eq!(config.len(), 1);
589 assert_eq!(config.shift_remove("newProp").unwrap(), ConfigKeyValue::from_str("value"));
590 assert_eq!(diagnostics.len(), 1);
591 assert_eq!(diagnostics[0].message, "The configuration key was renamed to 'newProp'");
592 assert_eq!(diagnostics[0].property_name, "oldProp");
593 }
594}