1use serde::Serialize;
6use serde::de::DeserializeOwned;
7
8pub trait RuleConfig: Serialize + DeserializeOwned + Default + Clone {
10 const RULE_NAME: &'static str;
12}
13
14pub fn load_rule_config<T: RuleConfig>(config: &crate::config::Config) -> T {
19 config
20 .rules
21 .get(T::RULE_NAME)
22 .and_then(|rule_config| {
23 let mut table = toml::map::Map::new();
25
26 for (k, v) in rule_config.values.iter() {
27 table.insert(k.clone(), v.clone());
29 }
30
31 let toml_table = toml::Value::Table(table);
32
33 match toml_table.try_into::<T>() {
35 Ok(config) => Some(config),
36 Err(e) => {
37 eprintln!("Warning: Invalid configuration for rule {}: {}", T::RULE_NAME, e);
39 eprintln!("Using default values for rule {}.", T::RULE_NAME);
40 eprintln!("Hint: Check the documentation for valid configuration values.");
41
42 None
43 }
44 }
45 })
46 .unwrap_or_default()
47}
48
49pub fn json_to_toml_value(json_val: &serde_json::Value) -> Option<toml::Value> {
51 match json_val {
52 serde_json::Value::Null => None,
53 serde_json::Value::Bool(b) => Some(toml::Value::Boolean(*b)),
54 serde_json::Value::Number(n) => {
55 if let Some(i) = n.as_i64() {
56 Some(toml::Value::Integer(i))
57 } else {
58 n.as_f64().map(toml::Value::Float)
59 }
60 }
61 serde_json::Value::String(s) => Some(toml::Value::String(s.clone())),
62 serde_json::Value::Array(arr) => {
63 let toml_arr: Vec<_> = arr.iter().filter_map(json_to_toml_value).collect();
64 Some(toml::Value::Array(toml_arr))
65 }
66 serde_json::Value::Object(obj) => {
67 let mut toml_table = toml::map::Map::new();
68 for (k, v) in obj {
69 if let Some(toml_v) = json_to_toml_value(v) {
70 toml_table.insert(k.clone(), toml_v);
71 }
72 }
73 Some(toml::Value::Table(toml_table))
74 }
75 }
76}
77
78pub fn is_rule_name(name: &str) -> bool {
82 let upper = name.to_ascii_uppercase();
83 upper.starts_with("MD") && upper.len() >= 4 && upper[2..].chars().all(|c| c.is_ascii_digit())
84}
85
86#[derive(Debug, Default)]
88pub struct RuleConfigConversion {
89 pub config: Option<crate::config::RuleConfig>,
91 pub warnings: Vec<String>,
93}
94
95pub fn json_to_rule_config(json_value: &serde_json::Value) -> Option<crate::config::RuleConfig> {
103 json_to_rule_config_with_warnings(json_value).config
104}
105
106pub fn json_to_rule_config_with_warnings(json_value: &serde_json::Value) -> RuleConfigConversion {
111 use std::collections::BTreeMap;
112
113 let mut result = RuleConfigConversion::default();
114
115 let Some(obj) = json_value.as_object() else {
116 result.warnings.push(format!(
117 "Expected object for rule config, got {}",
118 json_type_name(json_value)
119 ));
120 return result;
121 };
122
123 let mut values = BTreeMap::new();
124 let mut severity = None;
125
126 for (key, val) in obj {
127 if key == "severity" {
129 if let Some(s) = val.as_str() {
130 match s.to_lowercase().as_str() {
131 "error" => severity = Some(crate::rule::Severity::Error),
132 "warning" => severity = Some(crate::rule::Severity::Warning),
133 "info" => severity = Some(crate::rule::Severity::Info),
134 _ => {
135 result.warnings.push(format!(
136 "Invalid severity '{s}', expected 'error', 'warning', or 'info'"
137 ));
138 }
139 };
140 } else {
141 result
142 .warnings
143 .push(format!("Severity must be a string, got {}", json_type_name(val)));
144 }
145 continue;
146 }
147
148 if let Some(toml_val) = json_to_toml_value(val) {
150 values.insert(key.clone(), toml_val);
151 } else if !val.is_null() {
152 result
153 .warnings
154 .push(format!("Could not convert '{key}' value to config format"));
155 }
156 }
157
158 result.config = Some(crate::config::RuleConfig { severity, values });
159 result
160}
161
162fn json_type_name(val: &serde_json::Value) -> &'static str {
164 match val {
165 serde_json::Value::Null => "null",
166 serde_json::Value::Bool(_) => "boolean",
167 serde_json::Value::Number(_) => "number",
168 serde_json::Value::String(_) => "string",
169 serde_json::Value::Array(_) => "array",
170 serde_json::Value::Object(_) => "object",
171 }
172}
173
174pub fn toml_value_to_json(toml_val: &toml::Value) -> Option<serde_json::Value> {
176 match toml_val {
177 toml::Value::String(s) => Some(serde_json::Value::String(s.clone())),
178 toml::Value::Integer(i) => Some(serde_json::json!(i)),
179 toml::Value::Float(f) => Some(serde_json::json!(f)),
180 toml::Value::Boolean(b) => Some(serde_json::Value::Bool(*b)),
181 toml::Value::Array(arr) => {
182 let json_arr: Vec<_> = arr.iter().filter_map(toml_value_to_json).collect();
183 Some(serde_json::Value::Array(json_arr))
184 }
185 toml::Value::Table(table) => {
186 let mut json_obj = serde_json::Map::new();
187 for (k, v) in table {
188 if let Some(json_v) = toml_value_to_json(v) {
189 json_obj.insert(k.clone(), json_v);
190 }
191 }
192 Some(serde_json::Value::Object(json_obj))
193 }
194 toml::Value::Datetime(_) => None, }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use serde::{Deserialize, Serialize};
202 use std::collections::BTreeMap;
203
204 #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
206 #[serde(default)]
207 struct TestRuleConfig {
208 #[serde(default)]
209 enabled: bool,
210 #[serde(default)]
211 indent: i64,
212 #[serde(default)]
213 style: String,
214 #[serde(default)]
215 items: Vec<String>,
216 }
217
218 impl RuleConfig for TestRuleConfig {
219 const RULE_NAME: &'static str = "TEST001";
220 }
221
222 #[test]
223 fn test_toml_value_to_json_basic_types() {
224 let toml_str = toml::Value::String("hello".to_string());
226 let json_str = toml_value_to_json(&toml_str).unwrap();
227 assert_eq!(json_str, serde_json::Value::String("hello".to_string()));
228
229 let toml_int = toml::Value::Integer(42);
231 let json_int = toml_value_to_json(&toml_int).unwrap();
232 assert_eq!(json_int, serde_json::json!(42));
233
234 let toml_float = toml::Value::Float(1.234);
236 let json_float = toml_value_to_json(&toml_float).unwrap();
237 assert_eq!(json_float, serde_json::json!(1.234));
238
239 let toml_bool = toml::Value::Boolean(true);
241 let json_bool = toml_value_to_json(&toml_bool).unwrap();
242 assert_eq!(json_bool, serde_json::Value::Bool(true));
243 }
244
245 #[test]
246 fn test_toml_value_to_json_complex_types() {
247 let toml_arr = toml::Value::Array(vec![
249 toml::Value::String("a".to_string()),
250 toml::Value::String("b".to_string()),
251 ]);
252 let json_arr = toml_value_to_json(&toml_arr).unwrap();
253 assert_eq!(json_arr, serde_json::json!(["a", "b"]));
254
255 let mut toml_table = toml::map::Map::new();
257 toml_table.insert("key1".to_string(), toml::Value::String("value1".to_string()));
258 toml_table.insert("key2".to_string(), toml::Value::Integer(123));
259 let toml_tbl = toml::Value::Table(toml_table);
260 let json_tbl = toml_value_to_json(&toml_tbl).unwrap();
261
262 let expected = serde_json::json!({
263 "key1": "value1",
264 "key2": 123
265 });
266 assert_eq!(json_tbl, expected);
267 }
268
269 #[test]
270 fn test_toml_value_to_json_datetime() {
271 let toml_dt = toml::Value::Datetime("2023-01-01T00:00:00Z".parse().unwrap());
273 assert!(toml_value_to_json(&toml_dt).is_none());
274 }
275
276 #[test]
277 fn test_json_to_toml_value_basic_types() {
278 assert!(json_to_toml_value(&serde_json::Value::Null).is_none());
280
281 let json_bool = serde_json::Value::Bool(false);
283 let toml_bool = json_to_toml_value(&json_bool).unwrap();
284 assert_eq!(toml_bool, toml::Value::Boolean(false));
285
286 let json_int = serde_json::json!(42);
288 let toml_int = json_to_toml_value(&json_int).unwrap();
289 assert_eq!(toml_int, toml::Value::Integer(42));
290
291 let json_float = serde_json::json!(1.234);
293 let toml_float = json_to_toml_value(&json_float).unwrap();
294 assert_eq!(toml_float, toml::Value::Float(1.234));
295
296 let json_str = serde_json::Value::String("test".to_string());
298 let toml_str = json_to_toml_value(&json_str).unwrap();
299 assert_eq!(toml_str, toml::Value::String("test".to_string()));
300 }
301
302 #[test]
303 fn test_json_to_toml_value_complex_types() {
304 let json_arr = serde_json::json!(["x", "y", "z"]);
306 let toml_arr = json_to_toml_value(&json_arr).unwrap();
307 if let toml::Value::Array(arr) = toml_arr {
308 assert_eq!(arr.len(), 3);
309 assert_eq!(arr[0], toml::Value::String("x".to_string()));
310 assert_eq!(arr[1], toml::Value::String("y".to_string()));
311 assert_eq!(arr[2], toml::Value::String("z".to_string()));
312 } else {
313 panic!("Expected array");
314 }
315
316 let json_obj = serde_json::json!({
318 "name": "test",
319 "count": 10,
320 "active": true
321 });
322 let toml_obj = json_to_toml_value(&json_obj).unwrap();
323 if let toml::Value::Table(table) = toml_obj {
324 assert_eq!(table.get("name"), Some(&toml::Value::String("test".to_string())));
325 assert_eq!(table.get("count"), Some(&toml::Value::Integer(10)));
326 assert_eq!(table.get("active"), Some(&toml::Value::Boolean(true)));
327 } else {
328 panic!("Expected table");
329 }
330 }
331
332 #[test]
333 fn test_load_rule_config_default() {
334 let config = crate::config::Config::default();
336
337 let rule_config: TestRuleConfig = load_rule_config(&config);
339 assert_eq!(rule_config, TestRuleConfig::default());
340 }
341
342 #[test]
343 fn test_load_rule_config_with_values() {
344 let mut config = crate::config::Config::default();
346 let mut rule_values = BTreeMap::new();
347 rule_values.insert("enabled".to_string(), toml::Value::Boolean(true));
348 rule_values.insert("indent".to_string(), toml::Value::Integer(4));
349 rule_values.insert("style".to_string(), toml::Value::String("consistent".to_string()));
350 rule_values.insert(
351 "items".to_string(),
352 toml::Value::Array(vec![
353 toml::Value::String("item1".to_string()),
354 toml::Value::String("item2".to_string()),
355 ]),
356 );
357
358 config.rules.insert(
359 "TEST001".to_string(),
360 crate::config::RuleConfig {
361 severity: None,
362 values: rule_values,
363 },
364 );
365
366 let rule_config: TestRuleConfig = load_rule_config(&config);
368 assert!(rule_config.enabled);
369 assert_eq!(rule_config.indent, 4);
370 assert_eq!(rule_config.style, "consistent");
371 assert_eq!(rule_config.items, vec!["item1", "item2"]);
372 }
373
374 #[test]
375 fn test_load_rule_config_partial() {
376 let mut config = crate::config::Config::default();
378 let mut rule_values = BTreeMap::new();
379 rule_values.insert("enabled".to_string(), toml::Value::Boolean(true));
380 rule_values.insert("style".to_string(), toml::Value::String("custom".to_string()));
381
382 config.rules.insert(
383 "TEST001".to_string(),
384 crate::config::RuleConfig {
385 severity: None,
386 values: rule_values,
387 },
388 );
389
390 let rule_config: TestRuleConfig = load_rule_config(&config);
392 assert!(rule_config.enabled); assert_eq!(rule_config.indent, 0); assert_eq!(rule_config.style, "custom"); assert_eq!(rule_config.items, Vec::<String>::new()); }
397
398 #[test]
399 fn test_conversion_roundtrip() {
400 let original = toml::Value::Table({
402 let mut table = toml::map::Map::new();
403 table.insert("string".to_string(), toml::Value::String("test".to_string()));
404 table.insert("number".to_string(), toml::Value::Integer(42));
405 table.insert("bool".to_string(), toml::Value::Boolean(true));
406 table.insert(
407 "array".to_string(),
408 toml::Value::Array(vec![
409 toml::Value::String("a".to_string()),
410 toml::Value::String("b".to_string()),
411 ]),
412 );
413 table
414 });
415
416 let json = toml_value_to_json(&original).unwrap();
417 let back_to_toml = json_to_toml_value(&json).unwrap();
418
419 assert_eq!(original, back_to_toml);
420 }
421
422 #[test]
423 fn test_edge_cases() {
424 let empty_arr = toml::Value::Array(vec![]);
426 let json_arr = toml_value_to_json(&empty_arr).unwrap();
427 assert_eq!(json_arr, serde_json::json!([]));
428
429 let empty_table = toml::Value::Table(toml::map::Map::new());
431 let json_table = toml_value_to_json(&empty_table).unwrap();
432 assert_eq!(json_table, serde_json::json!({}));
433
434 let nested = toml::Value::Table({
436 let mut outer = toml::map::Map::new();
437 outer.insert(
438 "inner".to_string(),
439 toml::Value::Table({
440 let mut inner = toml::map::Map::new();
441 inner.insert("value".to_string(), toml::Value::Integer(123));
442 inner
443 }),
444 );
445 outer
446 });
447 let json_nested = toml_value_to_json(&nested).unwrap();
448 assert_eq!(
449 json_nested,
450 serde_json::json!({
451 "inner": {
452 "value": 123
453 }
454 })
455 );
456 }
457
458 #[test]
459 fn test_float_edge_cases() {
460 let nan = serde_json::Number::from_f64(f64::NAN);
462 assert!(nan.is_none());
463
464 let inf = serde_json::Number::from_f64(f64::INFINITY);
465 assert!(inf.is_none());
466
467 let valid_float = toml::Value::Float(1.23);
469 let json_float = toml_value_to_json(&valid_float).unwrap();
470 assert_eq!(json_float, serde_json::json!(1.23));
471 }
472
473 #[test]
474 fn test_invalid_config_returns_default() {
475 let mut config = crate::config::Config::default();
477 let mut rule_values = BTreeMap::new();
478 rule_values.insert("unknown_field".to_string(), toml::Value::Boolean(true));
479 rule_values.insert("items".to_string(), toml::Value::Table(toml::map::Map::new()));
481
482 config.rules.insert(
483 "TEST001".to_string(),
484 crate::config::RuleConfig {
485 severity: None,
486 values: rule_values,
487 },
488 );
489
490 let rule_config: TestRuleConfig = load_rule_config(&config);
492 assert_eq!(rule_config, TestRuleConfig::default());
494 }
495
496 #[test]
497 fn test_invalid_field_type() {
498 let mut config = crate::config::Config::default();
500 let mut rule_values = BTreeMap::new();
501 rule_values.insert("indent".to_string(), toml::Value::String("not_a_number".to_string()));
503
504 config.rules.insert(
505 "TEST001".to_string(),
506 crate::config::RuleConfig {
507 severity: None,
508 values: rule_values,
509 },
510 );
511
512 let rule_config: TestRuleConfig = load_rule_config(&config);
514 assert_eq!(rule_config, TestRuleConfig::default());
515 }
516
517 #[test]
520 fn test_is_rule_name_valid() {
521 assert!(is_rule_name("MD001"));
523 assert!(is_rule_name("MD060"));
524 assert!(is_rule_name("MD123"));
525 assert!(is_rule_name("MD999"));
526
527 assert!(is_rule_name("md001"));
529 assert!(is_rule_name("Md060"));
530 assert!(is_rule_name("mD123"));
531
532 assert!(is_rule_name("MD0001"));
534 assert!(is_rule_name("MD12345"));
535 }
536
537 #[test]
538 fn test_is_rule_name_invalid() {
539 assert!(!is_rule_name("MD"));
541 assert!(!is_rule_name("MD1"));
542 assert!(!is_rule_name("M"));
543 assert!(!is_rule_name(""));
544
545 assert!(!is_rule_name("disable"));
547 assert!(!is_rule_name("enable"));
548 assert!(!is_rule_name("flavor"));
549 assert!(!is_rule_name("line-length"));
550 assert!(!is_rule_name("global"));
551
552 assert!(!is_rule_name("MDA01")); assert!(!is_rule_name("XD001")); assert!(!is_rule_name("MD00A")); assert!(!is_rule_name("1MD001")); assert!(!is_rule_name("MD-001")); }
559
560 #[test]
563 fn test_json_to_rule_config_simple() {
564 let json = serde_json::json!({
565 "enabled": true,
566 "style": "aligned"
567 });
568
569 let rule_config = json_to_rule_config(&json).unwrap();
570
571 assert_eq!(rule_config.values.get("enabled"), Some(&toml::Value::Boolean(true)));
572 assert_eq!(
573 rule_config.values.get("style"),
574 Some(&toml::Value::String("aligned".to_string()))
575 );
576 assert!(rule_config.severity.is_none());
577 }
578
579 #[test]
580 fn test_json_to_rule_config_with_numbers() {
581 let json = serde_json::json!({
582 "line-length": 120,
583 "max-width": 0,
584 "indent": 4
585 });
586
587 let rule_config = json_to_rule_config(&json).unwrap();
588
589 assert_eq!(rule_config.values.get("line-length"), Some(&toml::Value::Integer(120)));
590 assert_eq!(rule_config.values.get("max-width"), Some(&toml::Value::Integer(0)));
591 assert_eq!(rule_config.values.get("indent"), Some(&toml::Value::Integer(4)));
592 }
593
594 #[test]
595 fn test_json_to_rule_config_with_arrays() {
596 let json = serde_json::json!({
597 "names": ["JavaScript", "TypeScript", "React"],
598 "exclude-patterns": ["*.test.md", "draft-*"]
599 });
600
601 let rule_config = json_to_rule_config(&json).unwrap();
602
603 let expected_names = toml::Value::Array(vec![
604 toml::Value::String("JavaScript".to_string()),
605 toml::Value::String("TypeScript".to_string()),
606 toml::Value::String("React".to_string()),
607 ]);
608 assert_eq!(rule_config.values.get("names"), Some(&expected_names));
609
610 let expected_patterns = toml::Value::Array(vec![
611 toml::Value::String("*.test.md".to_string()),
612 toml::Value::String("draft-*".to_string()),
613 ]);
614 assert_eq!(rule_config.values.get("exclude-patterns"), Some(&expected_patterns));
615 }
616
617 #[test]
618 fn test_json_to_rule_config_with_severity() {
619 let json = serde_json::json!({
621 "severity": "error",
622 "style": "aligned"
623 });
624 let rule_config = json_to_rule_config(&json).unwrap();
625 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Error));
626 assert!(!rule_config.values.contains_key("severity")); let json = serde_json::json!({
630 "severity": "warning",
631 "enabled": true
632 });
633 let rule_config = json_to_rule_config(&json).unwrap();
634 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Warning));
635
636 let json = serde_json::json!({
638 "severity": "info"
639 });
640 let rule_config = json_to_rule_config(&json).unwrap();
641 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Info));
642
643 let json = serde_json::json!({
645 "severity": "ERROR"
646 });
647 let rule_config = json_to_rule_config(&json).unwrap();
648 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Error));
649 }
650
651 #[test]
652 fn test_json_to_rule_config_invalid_severity() {
653 let json = serde_json::json!({
655 "severity": "critical",
656 "style": "aligned"
657 });
658 let rule_config = json_to_rule_config(&json).unwrap();
659 assert!(rule_config.severity.is_none()); assert_eq!(
661 rule_config.values.get("style"),
662 Some(&toml::Value::String("aligned".to_string()))
663 );
664
665 let json = serde_json::json!({
667 "severity": 1,
668 "enabled": true
669 });
670 let rule_config = json_to_rule_config(&json).unwrap();
671 assert!(rule_config.severity.is_none()); }
673
674 #[test]
675 fn test_json_to_rule_config_non_object() {
676 assert!(json_to_rule_config(&serde_json::json!(42)).is_none());
678 assert!(json_to_rule_config(&serde_json::json!("string")).is_none());
679 assert!(json_to_rule_config(&serde_json::json!(true)).is_none());
680 assert!(json_to_rule_config(&serde_json::json!([1, 2, 3])).is_none());
681 assert!(json_to_rule_config(&serde_json::Value::Null).is_none());
682 }
683
684 #[test]
685 fn test_json_to_rule_config_empty_object() {
686 let json = serde_json::json!({});
687 let rule_config = json_to_rule_config(&json).unwrap();
688 assert!(rule_config.values.is_empty());
689 assert!(rule_config.severity.is_none());
690 }
691
692 #[test]
693 fn test_json_to_rule_config_nested_objects() {
694 let json = serde_json::json!({
696 "options": {
697 "nested-key": "nested-value",
698 "nested-number": 42
699 }
700 });
701
702 let rule_config = json_to_rule_config(&json).unwrap();
703
704 let options = rule_config.values.get("options").unwrap();
705 if let toml::Value::Table(table) = options {
706 assert_eq!(
707 table.get("nested-key"),
708 Some(&toml::Value::String("nested-value".to_string()))
709 );
710 assert_eq!(table.get("nested-number"), Some(&toml::Value::Integer(42)));
711 } else {
712 panic!("options should be a table");
713 }
714 }
715
716 #[test]
717 fn test_json_to_rule_config_md060_example() {
718 let json = serde_json::json!({
720 "enabled": true,
721 "style": "aligned",
722 "max-width": 120,
723 "column-align": "auto",
724 "loose-last-column": false
725 });
726
727 let rule_config = json_to_rule_config(&json).unwrap();
728
729 assert_eq!(rule_config.values.get("enabled"), Some(&toml::Value::Boolean(true)));
730 assert_eq!(
731 rule_config.values.get("style"),
732 Some(&toml::Value::String("aligned".to_string()))
733 );
734 assert_eq!(rule_config.values.get("max-width"), Some(&toml::Value::Integer(120)));
735 assert_eq!(
736 rule_config.values.get("column-align"),
737 Some(&toml::Value::String("auto".to_string()))
738 );
739 assert_eq!(
740 rule_config.values.get("loose-last-column"),
741 Some(&toml::Value::Boolean(false))
742 );
743 }
744
745 #[test]
746 fn test_json_to_rule_config_md044_example() {
747 let json = serde_json::json!({
749 "names": ["JavaScript", "TypeScript", "GitHub", "macOS"],
750 "code-blocks": false,
751 "html-elements": false
752 });
753
754 let rule_config = json_to_rule_config(&json).unwrap();
755
756 let expected_names = toml::Value::Array(vec![
757 toml::Value::String("JavaScript".to_string()),
758 toml::Value::String("TypeScript".to_string()),
759 toml::Value::String("GitHub".to_string()),
760 toml::Value::String("macOS".to_string()),
761 ]);
762 assert_eq!(rule_config.values.get("names"), Some(&expected_names));
763 assert_eq!(
764 rule_config.values.get("code-blocks"),
765 Some(&toml::Value::Boolean(false))
766 );
767 assert_eq!(
768 rule_config.values.get("html-elements"),
769 Some(&toml::Value::Boolean(false))
770 );
771 }
772
773 #[test]
776 fn test_json_to_rule_config_with_warnings_valid() {
777 let json = serde_json::json!({
778 "severity": "error",
779 "enabled": true
780 });
781
782 let result = json_to_rule_config_with_warnings(&json);
783
784 assert!(result.config.is_some());
785 assert!(
786 result.warnings.is_empty(),
787 "Expected no warnings, got: {:?}",
788 result.warnings
789 );
790 assert_eq!(result.config.unwrap().severity, Some(crate::rule::Severity::Error));
791 }
792
793 #[test]
794 fn test_json_to_rule_config_with_warnings_invalid_severity() {
795 let json = serde_json::json!({
796 "severity": "critical",
797 "style": "aligned"
798 });
799
800 let result = json_to_rule_config_with_warnings(&json);
801
802 assert!(result.config.is_some());
803 assert_eq!(result.warnings.len(), 1);
804 assert!(result.warnings[0].contains("Invalid severity 'critical'"));
805 assert!(result.config.unwrap().severity.is_none());
807 }
808
809 #[test]
810 fn test_json_to_rule_config_with_warnings_wrong_severity_type() {
811 let json = serde_json::json!({
812 "severity": 123,
813 "enabled": true
814 });
815
816 let result = json_to_rule_config_with_warnings(&json);
817
818 assert!(result.config.is_some());
819 assert_eq!(result.warnings.len(), 1);
820 assert!(result.warnings[0].contains("Severity must be a string"));
821 }
822
823 #[test]
824 fn test_json_to_rule_config_with_warnings_non_object() {
825 let json = serde_json::json!("not an object");
826
827 let result = json_to_rule_config_with_warnings(&json);
828
829 assert!(result.config.is_none());
830 assert_eq!(result.warnings.len(), 1);
831 assert!(result.warnings[0].contains("Expected object"));
832 }
833
834 #[test]
837 fn test_rule_config_integration_with_config() {
838 let mut config = crate::config::Config::default();
840
841 let md060_json = serde_json::json!({
843 "enabled": true,
844 "style": "aligned",
845 "max-width": 120
846 });
847 let md013_json = serde_json::json!({
848 "line-length": 100,
849 "code-blocks": false
850 });
851
852 if let Some(md060_config) = json_to_rule_config(&md060_json) {
853 config.rules.insert("MD060".to_string(), md060_config);
854 }
855 if let Some(md013_config) = json_to_rule_config(&md013_json) {
856 config.rules.insert("MD013".to_string(), md013_config);
857 }
858
859 assert!(config.rules.contains_key("MD060"));
861 assert!(config.rules.contains_key("MD013"));
862
863 let md060 = config.rules.get("MD060").unwrap();
865 assert_eq!(md060.values.get("enabled"), Some(&toml::Value::Boolean(true)));
866 assert_eq!(
867 md060.values.get("style"),
868 Some(&toml::Value::String("aligned".to_string()))
869 );
870 assert_eq!(md060.values.get("max-width"), Some(&toml::Value::Integer(120)));
871 }
872
873 #[test]
874 fn test_rule_config_integration_with_severity() {
875 let mut config = crate::config::Config::default();
876
877 let json = serde_json::json!({
878 "severity": "error",
879 "enabled": true
880 });
881
882 if let Some(rule_config) = json_to_rule_config(&json) {
883 config.rules.insert("MD041".to_string(), rule_config);
884 }
885
886 let md041 = config.rules.get("MD041").unwrap();
887 assert_eq!(md041.severity, Some(crate::rule::Severity::Error));
888 }
889
890 #[test]
891 fn test_rule_config_integration_case_normalization() {
892 let mut config = crate::config::Config::default();
894
895 let json = serde_json::json!({ "enabled": true });
896
897 for rule_name in ["md060", "MD060", "Md060"] {
899 if is_rule_name(rule_name)
900 && let Some(rule_config) = json_to_rule_config(&json)
901 {
902 config.rules.insert(rule_name.to_ascii_uppercase(), rule_config);
903 }
904 }
905
906 assert!(config.rules.contains_key("MD060"));
908 assert_eq!(config.rules.len(), 1); }
910
911 #[test]
912 fn test_rule_config_integration_filters_non_rules() {
913 let keys = ["MD060", "disable", "enable", "flavor", "line-length", "global"];
915
916 let rule_keys: Vec<_> = keys.iter().filter(|k| is_rule_name(k)).collect();
917
918 assert_eq!(rule_keys, vec![&"MD060"]);
919 }
920
921 #[test]
922 fn test_multiple_rule_configs_with_mixed_validity() {
923 let rules = vec![
925 ("MD060", serde_json::json!({ "severity": "error", "style": "aligned" })),
926 (
927 "MD013",
928 serde_json::json!({ "severity": "invalid", "line-length": 100 }),
929 ),
930 ("MD041", serde_json::json!({ "enabled": true })),
931 ];
932
933 let mut config = crate::config::Config::default();
934 let mut all_warnings = Vec::new();
935
936 for (name, json) in rules {
937 let result = json_to_rule_config_with_warnings(&json);
938 all_warnings.extend(result.warnings);
939 if let Some(rule_config) = result.config {
940 config.rules.insert(name.to_string(), rule_config);
941 }
942 }
943
944 assert_eq!(config.rules.len(), 3);
946
947 assert_eq!(all_warnings.len(), 1);
949 assert!(all_warnings[0].contains("Invalid severity"));
950
951 assert_eq!(
953 config.rules.get("MD060").unwrap().severity,
954 Some(crate::rule::Severity::Error)
955 );
956 assert!(config.rules.get("MD013").unwrap().severity.is_none());
957 }
958
959 #[test]
963 fn test_end_to_end_md013_line_length_config() {
964 let content = "# Test\n\nThis is a line that is exactly 50 characters long.\n";
966
967 let mut config = crate::config::Config::default();
969 let json = serde_json::json!({
970 "line-length": 40
971 });
972 if let Some(rule_config) = json_to_rule_config(&json) {
973 config.rules.insert("MD013".to_string(), rule_config);
974 }
975
976 config.global.enable = vec!["MD013".to_string()];
978
979 let rules = crate::rules::all_rules(&config);
980 let filtered = crate::rules::filter_rules(&rules, &config.global);
981
982 let result = crate::lint(
983 content,
984 &filtered,
985 false,
986 crate::config::MarkdownFlavor::Standard,
987 Some(&config),
988 );
989
990 let warnings = result.expect("Linting should succeed");
991
992 let has_md013 = warnings.iter().any(|w| w.rule_name.as_deref() == Some("MD013"));
994 assert!(has_md013, "Should have MD013 warning with line-length=40");
995 }
996
997 #[test]
998 fn test_end_to_end_md013_line_length_no_warning() {
999 let content = "# Test\n\nThis is a line that is exactly 50 characters long.\n";
1001
1002 let mut config = crate::config::Config::default();
1004 let json = serde_json::json!({
1005 "line-length": 100
1006 });
1007 if let Some(rule_config) = json_to_rule_config(&json) {
1008 config.rules.insert("MD013".to_string(), rule_config);
1009 }
1010
1011 config.global.enable = vec!["MD013".to_string()];
1013
1014 let rules = crate::rules::all_rules(&config);
1015 let filtered = crate::rules::filter_rules(&rules, &config.global);
1016
1017 let result = crate::lint(
1018 content,
1019 &filtered,
1020 false,
1021 crate::config::MarkdownFlavor::Standard,
1022 Some(&config),
1023 );
1024
1025 let warnings = result.expect("Linting should succeed");
1026
1027 let has_md013 = warnings.iter().any(|w| w.rule_name.as_deref() == Some("MD013"));
1029 assert!(!has_md013, "Should NOT have MD013 warning with line-length=100");
1030 }
1031
1032 #[test]
1033 fn test_end_to_end_md044_proper_names() {
1034 let content = "# Test\n\nWe use javascript and typescript.\n";
1036
1037 let mut config = crate::config::Config::default();
1039 let json = serde_json::json!({
1040 "names": ["JavaScript", "TypeScript"],
1041 "code-blocks": false
1042 });
1043 if let Some(rule_config) = json_to_rule_config(&json) {
1044 config.rules.insert("MD044".to_string(), rule_config);
1045 }
1046
1047 config.global.enable = vec!["MD044".to_string()];
1049
1050 let rules = crate::rules::all_rules(&config);
1051 let filtered = crate::rules::filter_rules(&rules, &config.global);
1052
1053 let result = crate::lint(
1054 content,
1055 &filtered,
1056 false,
1057 crate::config::MarkdownFlavor::Standard,
1058 Some(&config),
1059 );
1060
1061 let warnings = result.expect("Linting should succeed");
1062
1063 let md044_warnings: Vec<_> = warnings
1065 .iter()
1066 .filter(|w| w.rule_name.as_deref() == Some("MD044"))
1067 .collect();
1068
1069 assert!(
1070 md044_warnings.len() >= 2,
1071 "Should have MD044 warnings for 'javascript' and 'typescript', got {}",
1072 md044_warnings.len()
1073 );
1074 }
1075
1076 #[test]
1077 fn test_end_to_end_severity_config() {
1078 let content = "test\n"; let mut config = crate::config::Config::default();
1082 let json = serde_json::json!({
1083 "severity": "info"
1084 });
1085 if let Some(rule_config) = json_to_rule_config(&json) {
1086 config.rules.insert("MD041".to_string(), rule_config);
1087 }
1088
1089 config.global.enable = vec!["MD041".to_string()];
1091
1092 let rules = crate::rules::all_rules(&config);
1093 let filtered = crate::rules::filter_rules(&rules, &config.global);
1094
1095 let result = crate::lint(
1096 content,
1097 &filtered,
1098 false,
1099 crate::config::MarkdownFlavor::Standard,
1100 Some(&config),
1101 );
1102
1103 let warnings = result.expect("Linting should succeed");
1104
1105 let md041 = warnings.iter().find(|w| w.rule_name.as_deref() == Some("MD041"));
1107 assert!(md041.is_some(), "Should have MD041 warning");
1108 assert_eq!(
1109 md041.unwrap().severity,
1110 crate::rule::Severity::Info,
1111 "MD041 should have Info severity from config"
1112 );
1113 }
1114}