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
49const NULLABLE_SENTINEL: &str = "\0__nullable__";
54
55pub fn is_nullable_sentinel(value: &toml::Value) -> bool {
57 matches!(value, toml::Value::String(s) if s == NULLABLE_SENTINEL)
58}
59
60pub fn config_schema_table<T: RuleConfig>(config: &T) -> Option<toml::map::Map<String, toml::Value>> {
66 let json_value = serde_json::to_value(config).ok()?;
67 let obj = json_value.as_object()?;
68 let mut table = toml::map::Map::new();
69 for (k, v) in obj {
70 if v.is_null() {
71 table.insert(k.clone(), toml::Value::String(NULLABLE_SENTINEL.to_string()));
72 } else {
73 let toml_v = json_to_toml_value(v).unwrap_or_else(|| toml::Value::String(NULLABLE_SENTINEL.to_string()));
76 table.insert(k.clone(), toml_v);
77 }
78 }
79 Some(table)
80}
81
82pub fn json_to_toml_value(json_val: &serde_json::Value) -> Option<toml::Value> {
84 match json_val {
85 serde_json::Value::Null => None,
86 serde_json::Value::Bool(b) => Some(toml::Value::Boolean(*b)),
87 serde_json::Value::Number(n) => {
88 if let Some(i) = n.as_i64() {
89 Some(toml::Value::Integer(i))
90 } else {
91 n.as_f64().map(toml::Value::Float)
92 }
93 }
94 serde_json::Value::String(s) => Some(toml::Value::String(s.clone())),
95 serde_json::Value::Array(arr) => {
96 let toml_arr: Vec<_> = arr.iter().filter_map(json_to_toml_value).collect();
97 Some(toml::Value::Array(toml_arr))
98 }
99 serde_json::Value::Object(obj) => {
100 let mut toml_table = toml::map::Map::new();
101 for (k, v) in obj {
102 if let Some(toml_v) = json_to_toml_value(v) {
103 toml_table.insert(k.clone(), toml_v);
104 }
105 }
106 Some(toml::Value::Table(toml_table))
107 }
108 }
109}
110
111pub fn is_rule_name(name: &str) -> bool {
115 let upper = name.to_ascii_uppercase();
116 upper.starts_with("MD") && upper.len() >= 4 && upper[2..].chars().all(|c| c.is_ascii_digit())
117}
118
119#[derive(Debug, Default)]
121pub struct RuleConfigConversion {
122 pub config: Option<crate::config::RuleConfig>,
124 pub warnings: Vec<String>,
126}
127
128pub fn json_to_rule_config(json_value: &serde_json::Value) -> Option<crate::config::RuleConfig> {
136 json_to_rule_config_with_warnings(json_value).config
137}
138
139pub fn json_to_rule_config_with_warnings(json_value: &serde_json::Value) -> RuleConfigConversion {
144 use std::collections::BTreeMap;
145
146 let mut result = RuleConfigConversion::default();
147
148 let Some(obj) = json_value.as_object() else {
149 result.warnings.push(format!(
150 "Expected object for rule config, got {}",
151 json_type_name(json_value)
152 ));
153 return result;
154 };
155
156 let mut values = BTreeMap::new();
157 let mut severity = None;
158
159 for (key, val) in obj {
160 if key == "severity" {
162 if let Some(s) = val.as_str() {
163 match s.to_lowercase().as_str() {
164 "error" => severity = Some(crate::rule::Severity::Error),
165 "warning" => severity = Some(crate::rule::Severity::Warning),
166 "info" => severity = Some(crate::rule::Severity::Info),
167 _ => {
168 result.warnings.push(format!(
169 "Invalid severity '{s}', expected 'error', 'warning', or 'info'"
170 ));
171 }
172 };
173 } else {
174 result
175 .warnings
176 .push(format!("Severity must be a string, got {}", json_type_name(val)));
177 }
178 continue;
179 }
180
181 if let Some(toml_val) = json_to_toml_value(val) {
183 values.insert(key.clone(), toml_val);
184 } else if !val.is_null() {
185 result
186 .warnings
187 .push(format!("Could not convert '{key}' value to config format"));
188 }
189 }
190
191 result.config = Some(crate::config::RuleConfig { severity, values });
192 result
193}
194
195fn json_type_name(val: &serde_json::Value) -> &'static str {
197 match val {
198 serde_json::Value::Null => "null",
199 serde_json::Value::Bool(_) => "boolean",
200 serde_json::Value::Number(_) => "number",
201 serde_json::Value::String(_) => "string",
202 serde_json::Value::Array(_) => "array",
203 serde_json::Value::Object(_) => "object",
204 }
205}
206
207pub fn toml_value_to_json(toml_val: &toml::Value) -> Option<serde_json::Value> {
209 match toml_val {
210 toml::Value::String(s) => Some(serde_json::Value::String(s.clone())),
211 toml::Value::Integer(i) => Some(serde_json::json!(i)),
212 toml::Value::Float(f) => Some(serde_json::json!(f)),
213 toml::Value::Boolean(b) => Some(serde_json::Value::Bool(*b)),
214 toml::Value::Array(arr) => {
215 let json_arr: Vec<_> = arr.iter().filter_map(toml_value_to_json).collect();
216 Some(serde_json::Value::Array(json_arr))
217 }
218 toml::Value::Table(table) => {
219 let mut json_obj = serde_json::Map::new();
220 for (k, v) in table {
221 if let Some(json_v) = toml_value_to_json(v) {
222 json_obj.insert(k.clone(), json_v);
223 }
224 }
225 Some(serde_json::Value::Object(json_obj))
226 }
227 toml::Value::Datetime(_) => None, }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use serde::{Deserialize, Serialize};
235 use std::collections::BTreeMap;
236
237 #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
239 #[serde(default)]
240 struct TestRuleConfig {
241 #[serde(default)]
242 enabled: bool,
243 #[serde(default)]
244 indent: i64,
245 #[serde(default)]
246 style: String,
247 #[serde(default)]
248 items: Vec<String>,
249 }
250
251 impl RuleConfig for TestRuleConfig {
252 const RULE_NAME: &'static str = "TEST001";
253 }
254
255 #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
259 #[serde(default)]
260 struct NullableTestConfig {
261 #[serde(default)]
262 enabled: bool,
263 #[serde(default, alias = "key-order")]
264 key_order: Option<Vec<String>>,
265 #[serde(default, alias = "title-pattern")]
266 title_pattern: Option<String>,
267 }
268
269 impl RuleConfig for NullableTestConfig {
270 const RULE_NAME: &'static str = "TEST_NULLABLE";
271 }
272
273 #[test]
274 fn test_is_nullable_sentinel() {
275 let sentinel = toml::Value::String(NULLABLE_SENTINEL.to_string());
276 assert!(is_nullable_sentinel(&sentinel));
277
278 let regular = toml::Value::String("normal".to_string());
279 assert!(!is_nullable_sentinel(®ular));
280
281 let integer = toml::Value::Integer(42);
282 assert!(!is_nullable_sentinel(&integer));
283 }
284
285 #[test]
286 fn test_config_schema_table_preserves_nullable_keys() {
287 let config = NullableTestConfig::default();
288 let table = config_schema_table(&config).unwrap();
289
290 assert!(table.contains_key("enabled"), "enabled key missing");
292 assert!(table.contains_key("key_order"), "key_order key missing");
293 assert!(table.contains_key("title_pattern"), "title_pattern key missing");
294
295 assert!(is_nullable_sentinel(table.get("key_order").unwrap()));
297 assert!(is_nullable_sentinel(table.get("title_pattern").unwrap()));
298
299 assert_eq!(table.get("enabled"), Some(&toml::Value::Boolean(false)));
301 }
302
303 #[test]
304 fn test_config_schema_table_non_null_option_uses_real_value() {
305 let config = NullableTestConfig {
306 enabled: true,
307 key_order: Some(vec!["title".to_string(), "date".to_string()]),
308 title_pattern: Some("pattern".to_string()),
309 };
310 let table = config_schema_table(&config).unwrap();
311
312 let key_order = table.get("key_order").unwrap();
314 assert!(!is_nullable_sentinel(key_order));
315 assert!(matches!(key_order, toml::Value::Array(_)));
316
317 let title_pattern = table.get("title_pattern").unwrap();
318 assert!(!is_nullable_sentinel(title_pattern));
319 assert_eq!(title_pattern, &toml::Value::String("pattern".to_string()));
320 }
321
322 #[test]
323 fn test_json_to_toml_value_still_drops_null() {
324 assert!(json_to_toml_value(&serde_json::Value::Null).is_none());
326 }
327
328 #[test]
329 fn test_config_schema_table_all_keys_present() {
330 let config = NullableTestConfig::default();
331 let table = config_schema_table(&config).unwrap();
332 assert_eq!(table.len(), 3, "Expected 3 keys: enabled, key_order, title_pattern");
333 }
334
335 #[test]
336 fn test_config_schema_table_never_drops_keys() {
337 let mut obj = serde_json::Map::new();
341 obj.insert("real_key".to_string(), serde_json::json!(42));
342 obj.insert("null_key".to_string(), serde_json::Value::Null);
343 let json = serde_json::Value::Object(obj);
344
345 let obj = json.as_object().unwrap();
347 let mut table = toml::map::Map::new();
348 for (k, v) in obj {
349 if v.is_null() {
350 table.insert(k.clone(), toml::Value::String(NULLABLE_SENTINEL.to_string()));
351 } else {
352 let toml_v =
353 json_to_toml_value(v).unwrap_or_else(|| toml::Value::String(NULLABLE_SENTINEL.to_string()));
354 table.insert(k.clone(), toml_v);
355 }
356 }
357
358 assert_eq!(table.len(), 2, "Both keys must be present");
359 assert!(table.contains_key("real_key"));
360 assert!(table.contains_key("null_key"));
361 }
362
363 #[test]
364 fn test_toml_value_to_json_basic_types() {
365 let toml_str = toml::Value::String("hello".to_string());
367 let json_str = toml_value_to_json(&toml_str).unwrap();
368 assert_eq!(json_str, serde_json::Value::String("hello".to_string()));
369
370 let toml_int = toml::Value::Integer(42);
372 let json_int = toml_value_to_json(&toml_int).unwrap();
373 assert_eq!(json_int, serde_json::json!(42));
374
375 let toml_float = toml::Value::Float(1.234);
377 let json_float = toml_value_to_json(&toml_float).unwrap();
378 assert_eq!(json_float, serde_json::json!(1.234));
379
380 let toml_bool = toml::Value::Boolean(true);
382 let json_bool = toml_value_to_json(&toml_bool).unwrap();
383 assert_eq!(json_bool, serde_json::Value::Bool(true));
384 }
385
386 #[test]
387 fn test_toml_value_to_json_complex_types() {
388 let toml_arr = toml::Value::Array(vec![
390 toml::Value::String("a".to_string()),
391 toml::Value::String("b".to_string()),
392 ]);
393 let json_arr = toml_value_to_json(&toml_arr).unwrap();
394 assert_eq!(json_arr, serde_json::json!(["a", "b"]));
395
396 let mut toml_table = toml::map::Map::new();
398 toml_table.insert("key1".to_string(), toml::Value::String("value1".to_string()));
399 toml_table.insert("key2".to_string(), toml::Value::Integer(123));
400 let toml_tbl = toml::Value::Table(toml_table);
401 let json_tbl = toml_value_to_json(&toml_tbl).unwrap();
402
403 let expected = serde_json::json!({
404 "key1": "value1",
405 "key2": 123
406 });
407 assert_eq!(json_tbl, expected);
408 }
409
410 #[test]
411 fn test_toml_value_to_json_datetime() {
412 let toml_dt = toml::Value::Datetime("2023-01-01T00:00:00Z".parse().unwrap());
414 assert!(toml_value_to_json(&toml_dt).is_none());
415 }
416
417 #[test]
418 fn test_json_to_toml_value_basic_types() {
419 assert!(json_to_toml_value(&serde_json::Value::Null).is_none());
421
422 let json_bool = serde_json::Value::Bool(false);
424 let toml_bool = json_to_toml_value(&json_bool).unwrap();
425 assert_eq!(toml_bool, toml::Value::Boolean(false));
426
427 let json_int = serde_json::json!(42);
429 let toml_int = json_to_toml_value(&json_int).unwrap();
430 assert_eq!(toml_int, toml::Value::Integer(42));
431
432 let json_float = serde_json::json!(1.234);
434 let toml_float = json_to_toml_value(&json_float).unwrap();
435 assert_eq!(toml_float, toml::Value::Float(1.234));
436
437 let json_str = serde_json::Value::String("test".to_string());
439 let toml_str = json_to_toml_value(&json_str).unwrap();
440 assert_eq!(toml_str, toml::Value::String("test".to_string()));
441 }
442
443 #[test]
444 fn test_json_to_toml_value_complex_types() {
445 let json_arr = serde_json::json!(["x", "y", "z"]);
447 let toml_arr = json_to_toml_value(&json_arr).unwrap();
448 if let toml::Value::Array(arr) = toml_arr {
449 assert_eq!(arr.len(), 3);
450 assert_eq!(arr[0], toml::Value::String("x".to_string()));
451 assert_eq!(arr[1], toml::Value::String("y".to_string()));
452 assert_eq!(arr[2], toml::Value::String("z".to_string()));
453 } else {
454 panic!("Expected array");
455 }
456
457 let json_obj = serde_json::json!({
459 "name": "test",
460 "count": 10,
461 "active": true
462 });
463 let toml_obj = json_to_toml_value(&json_obj).unwrap();
464 if let toml::Value::Table(table) = toml_obj {
465 assert_eq!(table.get("name"), Some(&toml::Value::String("test".to_string())));
466 assert_eq!(table.get("count"), Some(&toml::Value::Integer(10)));
467 assert_eq!(table.get("active"), Some(&toml::Value::Boolean(true)));
468 } else {
469 panic!("Expected table");
470 }
471 }
472
473 #[test]
474 fn test_load_rule_config_default() {
475 let config = crate::config::Config::default();
477
478 let rule_config: TestRuleConfig = load_rule_config(&config);
480 assert_eq!(rule_config, TestRuleConfig::default());
481 }
482
483 #[test]
484 fn test_load_rule_config_with_values() {
485 let mut config = crate::config::Config::default();
487 let mut rule_values = BTreeMap::new();
488 rule_values.insert("enabled".to_string(), toml::Value::Boolean(true));
489 rule_values.insert("indent".to_string(), toml::Value::Integer(4));
490 rule_values.insert("style".to_string(), toml::Value::String("consistent".to_string()));
491 rule_values.insert(
492 "items".to_string(),
493 toml::Value::Array(vec![
494 toml::Value::String("item1".to_string()),
495 toml::Value::String("item2".to_string()),
496 ]),
497 );
498
499 config.rules.insert(
500 "TEST001".to_string(),
501 crate::config::RuleConfig {
502 severity: None,
503 values: rule_values,
504 },
505 );
506
507 let rule_config: TestRuleConfig = load_rule_config(&config);
509 assert!(rule_config.enabled);
510 assert_eq!(rule_config.indent, 4);
511 assert_eq!(rule_config.style, "consistent");
512 assert_eq!(rule_config.items, vec!["item1", "item2"]);
513 }
514
515 #[test]
516 fn test_load_rule_config_partial() {
517 let mut config = crate::config::Config::default();
519 let mut rule_values = BTreeMap::new();
520 rule_values.insert("enabled".to_string(), toml::Value::Boolean(true));
521 rule_values.insert("style".to_string(), toml::Value::String("custom".to_string()));
522
523 config.rules.insert(
524 "TEST001".to_string(),
525 crate::config::RuleConfig {
526 severity: None,
527 values: rule_values,
528 },
529 );
530
531 let rule_config: TestRuleConfig = load_rule_config(&config);
533 assert!(rule_config.enabled); assert_eq!(rule_config.indent, 0); assert_eq!(rule_config.style, "custom"); assert_eq!(rule_config.items, Vec::<String>::new()); }
538
539 #[test]
540 fn test_conversion_roundtrip() {
541 let original = toml::Value::Table({
543 let mut table = toml::map::Map::new();
544 table.insert("string".to_string(), toml::Value::String("test".to_string()));
545 table.insert("number".to_string(), toml::Value::Integer(42));
546 table.insert("bool".to_string(), toml::Value::Boolean(true));
547 table.insert(
548 "array".to_string(),
549 toml::Value::Array(vec![
550 toml::Value::String("a".to_string()),
551 toml::Value::String("b".to_string()),
552 ]),
553 );
554 table
555 });
556
557 let json = toml_value_to_json(&original).unwrap();
558 let back_to_toml = json_to_toml_value(&json).unwrap();
559
560 assert_eq!(original, back_to_toml);
561 }
562
563 #[test]
564 fn test_edge_cases() {
565 let empty_arr = toml::Value::Array(vec![]);
567 let json_arr = toml_value_to_json(&empty_arr).unwrap();
568 assert_eq!(json_arr, serde_json::json!([]));
569
570 let empty_table = toml::Value::Table(toml::map::Map::new());
572 let json_table = toml_value_to_json(&empty_table).unwrap();
573 assert_eq!(json_table, serde_json::json!({}));
574
575 let nested = toml::Value::Table({
577 let mut outer = toml::map::Map::new();
578 outer.insert(
579 "inner".to_string(),
580 toml::Value::Table({
581 let mut inner = toml::map::Map::new();
582 inner.insert("value".to_string(), toml::Value::Integer(123));
583 inner
584 }),
585 );
586 outer
587 });
588 let json_nested = toml_value_to_json(&nested).unwrap();
589 assert_eq!(
590 json_nested,
591 serde_json::json!({
592 "inner": {
593 "value": 123
594 }
595 })
596 );
597 }
598
599 #[test]
600 fn test_float_edge_cases() {
601 let nan = serde_json::Number::from_f64(f64::NAN);
603 assert!(nan.is_none());
604
605 let inf = serde_json::Number::from_f64(f64::INFINITY);
606 assert!(inf.is_none());
607
608 let valid_float = toml::Value::Float(1.23);
610 let json_float = toml_value_to_json(&valid_float).unwrap();
611 assert_eq!(json_float, serde_json::json!(1.23));
612 }
613
614 #[test]
615 fn test_invalid_config_returns_default() {
616 let mut config = crate::config::Config::default();
618 let mut rule_values = BTreeMap::new();
619 rule_values.insert("unknown_field".to_string(), toml::Value::Boolean(true));
620 rule_values.insert("items".to_string(), toml::Value::Table(toml::map::Map::new()));
622
623 config.rules.insert(
624 "TEST001".to_string(),
625 crate::config::RuleConfig {
626 severity: None,
627 values: rule_values,
628 },
629 );
630
631 let rule_config: TestRuleConfig = load_rule_config(&config);
633 assert_eq!(rule_config, TestRuleConfig::default());
635 }
636
637 #[test]
638 fn test_invalid_field_type() {
639 let mut config = crate::config::Config::default();
641 let mut rule_values = BTreeMap::new();
642 rule_values.insert("indent".to_string(), toml::Value::String("not_a_number".to_string()));
644
645 config.rules.insert(
646 "TEST001".to_string(),
647 crate::config::RuleConfig {
648 severity: None,
649 values: rule_values,
650 },
651 );
652
653 let rule_config: TestRuleConfig = load_rule_config(&config);
655 assert_eq!(rule_config, TestRuleConfig::default());
656 }
657
658 #[test]
661 fn test_is_rule_name_valid() {
662 assert!(is_rule_name("MD001"));
664 assert!(is_rule_name("MD060"));
665 assert!(is_rule_name("MD123"));
666 assert!(is_rule_name("MD999"));
667
668 assert!(is_rule_name("md001"));
670 assert!(is_rule_name("Md060"));
671 assert!(is_rule_name("mD123"));
672
673 assert!(is_rule_name("MD0001"));
675 assert!(is_rule_name("MD12345"));
676 }
677
678 #[test]
679 fn test_is_rule_name_invalid() {
680 assert!(!is_rule_name("MD"));
682 assert!(!is_rule_name("MD1"));
683 assert!(!is_rule_name("M"));
684 assert!(!is_rule_name(""));
685
686 assert!(!is_rule_name("disable"));
688 assert!(!is_rule_name("enable"));
689 assert!(!is_rule_name("flavor"));
690 assert!(!is_rule_name("line-length"));
691 assert!(!is_rule_name("global"));
692
693 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")); }
700
701 #[test]
704 fn test_json_to_rule_config_simple() {
705 let json = serde_json::json!({
706 "enabled": true,
707 "style": "aligned"
708 });
709
710 let rule_config = json_to_rule_config(&json).unwrap();
711
712 assert_eq!(rule_config.values.get("enabled"), Some(&toml::Value::Boolean(true)));
713 assert_eq!(
714 rule_config.values.get("style"),
715 Some(&toml::Value::String("aligned".to_string()))
716 );
717 assert!(rule_config.severity.is_none());
718 }
719
720 #[test]
721 fn test_json_to_rule_config_with_numbers() {
722 let json = serde_json::json!({
723 "line-length": 120,
724 "max-width": 0,
725 "indent": 4
726 });
727
728 let rule_config = json_to_rule_config(&json).unwrap();
729
730 assert_eq!(rule_config.values.get("line-length"), Some(&toml::Value::Integer(120)));
731 assert_eq!(rule_config.values.get("max-width"), Some(&toml::Value::Integer(0)));
732 assert_eq!(rule_config.values.get("indent"), Some(&toml::Value::Integer(4)));
733 }
734
735 #[test]
736 fn test_json_to_rule_config_with_arrays() {
737 let json = serde_json::json!({
738 "names": ["JavaScript", "TypeScript", "React"],
739 "exclude-patterns": ["*.test.md", "draft-*"]
740 });
741
742 let rule_config = json_to_rule_config(&json).unwrap();
743
744 let expected_names = toml::Value::Array(vec![
745 toml::Value::String("JavaScript".to_string()),
746 toml::Value::String("TypeScript".to_string()),
747 toml::Value::String("React".to_string()),
748 ]);
749 assert_eq!(rule_config.values.get("names"), Some(&expected_names));
750
751 let expected_patterns = toml::Value::Array(vec![
752 toml::Value::String("*.test.md".to_string()),
753 toml::Value::String("draft-*".to_string()),
754 ]);
755 assert_eq!(rule_config.values.get("exclude-patterns"), Some(&expected_patterns));
756 }
757
758 #[test]
759 fn test_json_to_rule_config_with_severity() {
760 let json = serde_json::json!({
762 "severity": "error",
763 "style": "aligned"
764 });
765 let rule_config = json_to_rule_config(&json).unwrap();
766 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Error));
767 assert!(!rule_config.values.contains_key("severity")); let json = serde_json::json!({
771 "severity": "warning",
772 "enabled": true
773 });
774 let rule_config = json_to_rule_config(&json).unwrap();
775 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Warning));
776
777 let json = serde_json::json!({
779 "severity": "info"
780 });
781 let rule_config = json_to_rule_config(&json).unwrap();
782 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Info));
783
784 let json = serde_json::json!({
786 "severity": "ERROR"
787 });
788 let rule_config = json_to_rule_config(&json).unwrap();
789 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Error));
790 }
791
792 #[test]
793 fn test_json_to_rule_config_invalid_severity() {
794 let json = serde_json::json!({
796 "severity": "critical",
797 "style": "aligned"
798 });
799 let rule_config = json_to_rule_config(&json).unwrap();
800 assert!(rule_config.severity.is_none()); assert_eq!(
802 rule_config.values.get("style"),
803 Some(&toml::Value::String("aligned".to_string()))
804 );
805
806 let json = serde_json::json!({
808 "severity": 1,
809 "enabled": true
810 });
811 let rule_config = json_to_rule_config(&json).unwrap();
812 assert!(rule_config.severity.is_none()); }
814
815 #[test]
816 fn test_json_to_rule_config_non_object() {
817 assert!(json_to_rule_config(&serde_json::json!(42)).is_none());
819 assert!(json_to_rule_config(&serde_json::json!("string")).is_none());
820 assert!(json_to_rule_config(&serde_json::json!(true)).is_none());
821 assert!(json_to_rule_config(&serde_json::json!([1, 2, 3])).is_none());
822 assert!(json_to_rule_config(&serde_json::Value::Null).is_none());
823 }
824
825 #[test]
826 fn test_json_to_rule_config_empty_object() {
827 let json = serde_json::json!({});
828 let rule_config = json_to_rule_config(&json).unwrap();
829 assert!(rule_config.values.is_empty());
830 assert!(rule_config.severity.is_none());
831 }
832
833 #[test]
834 fn test_json_to_rule_config_nested_objects() {
835 let json = serde_json::json!({
837 "options": {
838 "nested-key": "nested-value",
839 "nested-number": 42
840 }
841 });
842
843 let rule_config = json_to_rule_config(&json).unwrap();
844
845 let options = rule_config.values.get("options").unwrap();
846 if let toml::Value::Table(table) = options {
847 assert_eq!(
848 table.get("nested-key"),
849 Some(&toml::Value::String("nested-value".to_string()))
850 );
851 assert_eq!(table.get("nested-number"), Some(&toml::Value::Integer(42)));
852 } else {
853 panic!("options should be a table");
854 }
855 }
856
857 #[test]
858 fn test_json_to_rule_config_md060_example() {
859 let json = serde_json::json!({
861 "enabled": true,
862 "style": "aligned",
863 "max-width": 120,
864 "column-align": "auto",
865 "loose-last-column": false
866 });
867
868 let rule_config = json_to_rule_config(&json).unwrap();
869
870 assert_eq!(rule_config.values.get("enabled"), Some(&toml::Value::Boolean(true)));
871 assert_eq!(
872 rule_config.values.get("style"),
873 Some(&toml::Value::String("aligned".to_string()))
874 );
875 assert_eq!(rule_config.values.get("max-width"), Some(&toml::Value::Integer(120)));
876 assert_eq!(
877 rule_config.values.get("column-align"),
878 Some(&toml::Value::String("auto".to_string()))
879 );
880 assert_eq!(
881 rule_config.values.get("loose-last-column"),
882 Some(&toml::Value::Boolean(false))
883 );
884 }
885
886 #[test]
887 fn test_json_to_rule_config_md044_example() {
888 let json = serde_json::json!({
890 "names": ["JavaScript", "TypeScript", "GitHub", "macOS"],
891 "code-blocks": false,
892 "html-elements": false
893 });
894
895 let rule_config = json_to_rule_config(&json).unwrap();
896
897 let expected_names = toml::Value::Array(vec![
898 toml::Value::String("JavaScript".to_string()),
899 toml::Value::String("TypeScript".to_string()),
900 toml::Value::String("GitHub".to_string()),
901 toml::Value::String("macOS".to_string()),
902 ]);
903 assert_eq!(rule_config.values.get("names"), Some(&expected_names));
904 assert_eq!(
905 rule_config.values.get("code-blocks"),
906 Some(&toml::Value::Boolean(false))
907 );
908 assert_eq!(
909 rule_config.values.get("html-elements"),
910 Some(&toml::Value::Boolean(false))
911 );
912 }
913
914 #[test]
917 fn test_json_to_rule_config_with_warnings_valid() {
918 let json = serde_json::json!({
919 "severity": "error",
920 "enabled": true
921 });
922
923 let result = json_to_rule_config_with_warnings(&json);
924
925 assert!(result.config.is_some());
926 assert!(
927 result.warnings.is_empty(),
928 "Expected no warnings, got: {:?}",
929 result.warnings
930 );
931 assert_eq!(result.config.unwrap().severity, Some(crate::rule::Severity::Error));
932 }
933
934 #[test]
935 fn test_json_to_rule_config_with_warnings_invalid_severity() {
936 let json = serde_json::json!({
937 "severity": "critical",
938 "style": "aligned"
939 });
940
941 let result = json_to_rule_config_with_warnings(&json);
942
943 assert!(result.config.is_some());
944 assert_eq!(result.warnings.len(), 1);
945 assert!(result.warnings[0].contains("Invalid severity 'critical'"));
946 assert!(result.config.unwrap().severity.is_none());
948 }
949
950 #[test]
951 fn test_json_to_rule_config_with_warnings_wrong_severity_type() {
952 let json = serde_json::json!({
953 "severity": 123,
954 "enabled": true
955 });
956
957 let result = json_to_rule_config_with_warnings(&json);
958
959 assert!(result.config.is_some());
960 assert_eq!(result.warnings.len(), 1);
961 assert!(result.warnings[0].contains("Severity must be a string"));
962 }
963
964 #[test]
965 fn test_json_to_rule_config_with_warnings_non_object() {
966 let json = serde_json::json!("not an object");
967
968 let result = json_to_rule_config_with_warnings(&json);
969
970 assert!(result.config.is_none());
971 assert_eq!(result.warnings.len(), 1);
972 assert!(result.warnings[0].contains("Expected object"));
973 }
974
975 #[test]
978 fn test_rule_config_integration_with_config() {
979 let mut config = crate::config::Config::default();
981
982 let md060_json = serde_json::json!({
984 "enabled": true,
985 "style": "aligned",
986 "max-width": 120
987 });
988 let md013_json = serde_json::json!({
989 "line-length": 100,
990 "code-blocks": false
991 });
992
993 if let Some(md060_config) = json_to_rule_config(&md060_json) {
994 config.rules.insert("MD060".to_string(), md060_config);
995 }
996 if let Some(md013_config) = json_to_rule_config(&md013_json) {
997 config.rules.insert("MD013".to_string(), md013_config);
998 }
999
1000 assert!(config.rules.contains_key("MD060"));
1002 assert!(config.rules.contains_key("MD013"));
1003
1004 let md060 = config.rules.get("MD060").unwrap();
1006 assert_eq!(md060.values.get("enabled"), Some(&toml::Value::Boolean(true)));
1007 assert_eq!(
1008 md060.values.get("style"),
1009 Some(&toml::Value::String("aligned".to_string()))
1010 );
1011 assert_eq!(md060.values.get("max-width"), Some(&toml::Value::Integer(120)));
1012 }
1013
1014 #[test]
1015 fn test_rule_config_integration_with_severity() {
1016 let mut config = crate::config::Config::default();
1017
1018 let json = serde_json::json!({
1019 "severity": "error",
1020 "enabled": true
1021 });
1022
1023 if let Some(rule_config) = json_to_rule_config(&json) {
1024 config.rules.insert("MD041".to_string(), rule_config);
1025 }
1026
1027 let md041 = config.rules.get("MD041").unwrap();
1028 assert_eq!(md041.severity, Some(crate::rule::Severity::Error));
1029 }
1030
1031 #[test]
1032 fn test_rule_config_integration_case_normalization() {
1033 let mut config = crate::config::Config::default();
1035
1036 let json = serde_json::json!({ "enabled": true });
1037
1038 for rule_name in ["md060", "MD060", "Md060"] {
1040 if is_rule_name(rule_name)
1041 && let Some(rule_config) = json_to_rule_config(&json)
1042 {
1043 config.rules.insert(rule_name.to_ascii_uppercase(), rule_config);
1044 }
1045 }
1046
1047 assert!(config.rules.contains_key("MD060"));
1049 assert_eq!(config.rules.len(), 1); }
1051
1052 #[test]
1053 fn test_rule_config_integration_filters_non_rules() {
1054 let keys = ["MD060", "disable", "enable", "flavor", "line-length", "global"];
1056
1057 let rule_keys: Vec<_> = keys.iter().filter(|k| is_rule_name(k)).collect();
1058
1059 assert_eq!(rule_keys, vec![&"MD060"]);
1060 }
1061
1062 #[test]
1063 fn test_multiple_rule_configs_with_mixed_validity() {
1064 let rules = vec![
1066 ("MD060", serde_json::json!({ "severity": "error", "style": "aligned" })),
1067 (
1068 "MD013",
1069 serde_json::json!({ "severity": "invalid", "line-length": 100 }),
1070 ),
1071 ("MD041", serde_json::json!({ "enabled": true })),
1072 ];
1073
1074 let mut config = crate::config::Config::default();
1075 let mut all_warnings = Vec::new();
1076
1077 for (name, json) in rules {
1078 let result = json_to_rule_config_with_warnings(&json);
1079 all_warnings.extend(result.warnings);
1080 if let Some(rule_config) = result.config {
1081 config.rules.insert(name.to_string(), rule_config);
1082 }
1083 }
1084
1085 assert_eq!(config.rules.len(), 3);
1087
1088 assert_eq!(all_warnings.len(), 1);
1090 assert!(all_warnings[0].contains("Invalid severity"));
1091
1092 assert_eq!(
1094 config.rules.get("MD060").unwrap().severity,
1095 Some(crate::rule::Severity::Error)
1096 );
1097 assert!(config.rules.get("MD013").unwrap().severity.is_none());
1098 }
1099
1100 #[test]
1104 fn test_end_to_end_md013_line_length_config() {
1105 let content = "# Test\n\nThis is a line that is exactly 50 characters long.\n";
1107
1108 let mut config = crate::config::Config::default();
1110 let json = serde_json::json!({
1111 "line-length": 40
1112 });
1113 if let Some(rule_config) = json_to_rule_config(&json) {
1114 config.rules.insert("MD013".to_string(), rule_config);
1115 }
1116
1117 config.global.enable = vec!["MD013".to_string()];
1119
1120 let rules = crate::rules::all_rules(&config);
1121 let filtered = crate::rules::filter_rules(&rules, &config.global);
1122
1123 let result = crate::lint(
1124 content,
1125 &filtered,
1126 false,
1127 crate::config::MarkdownFlavor::Standard,
1128 Some(&config),
1129 );
1130
1131 let warnings = result.expect("Linting should succeed");
1132
1133 let has_md013 = warnings.iter().any(|w| w.rule_name.as_deref() == Some("MD013"));
1135 assert!(has_md013, "Should have MD013 warning with line-length=40");
1136 }
1137
1138 #[test]
1139 fn test_end_to_end_md013_line_length_no_warning() {
1140 let content = "# Test\n\nThis is a line that is exactly 50 characters long.\n";
1142
1143 let mut config = crate::config::Config::default();
1145 let json = serde_json::json!({
1146 "line-length": 100
1147 });
1148 if let Some(rule_config) = json_to_rule_config(&json) {
1149 config.rules.insert("MD013".to_string(), rule_config);
1150 }
1151
1152 config.global.enable = vec!["MD013".to_string()];
1154
1155 let rules = crate::rules::all_rules(&config);
1156 let filtered = crate::rules::filter_rules(&rules, &config.global);
1157
1158 let result = crate::lint(
1159 content,
1160 &filtered,
1161 false,
1162 crate::config::MarkdownFlavor::Standard,
1163 Some(&config),
1164 );
1165
1166 let warnings = result.expect("Linting should succeed");
1167
1168 let has_md013 = warnings.iter().any(|w| w.rule_name.as_deref() == Some("MD013"));
1170 assert!(!has_md013, "Should NOT have MD013 warning with line-length=100");
1171 }
1172
1173 #[test]
1174 fn test_end_to_end_md044_proper_names() {
1175 let content = "# Test\n\nWe use javascript and typescript.\n";
1177
1178 let mut config = crate::config::Config::default();
1180 let json = serde_json::json!({
1181 "names": ["JavaScript", "TypeScript"],
1182 "code-blocks": false
1183 });
1184 if let Some(rule_config) = json_to_rule_config(&json) {
1185 config.rules.insert("MD044".to_string(), rule_config);
1186 }
1187
1188 config.global.enable = vec!["MD044".to_string()];
1190
1191 let rules = crate::rules::all_rules(&config);
1192 let filtered = crate::rules::filter_rules(&rules, &config.global);
1193
1194 let result = crate::lint(
1195 content,
1196 &filtered,
1197 false,
1198 crate::config::MarkdownFlavor::Standard,
1199 Some(&config),
1200 );
1201
1202 let warnings = result.expect("Linting should succeed");
1203
1204 let md044_warnings: Vec<_> = warnings
1206 .iter()
1207 .filter(|w| w.rule_name.as_deref() == Some("MD044"))
1208 .collect();
1209
1210 assert!(
1211 md044_warnings.len() >= 2,
1212 "Should have MD044 warnings for 'javascript' and 'typescript', got {}",
1213 md044_warnings.len()
1214 );
1215 }
1216
1217 #[test]
1218 fn test_end_to_end_severity_config() {
1219 let content = "test\n"; let mut config = crate::config::Config::default();
1223 let json = serde_json::json!({
1224 "severity": "info"
1225 });
1226 if let Some(rule_config) = json_to_rule_config(&json) {
1227 config.rules.insert("MD041".to_string(), rule_config);
1228 }
1229
1230 config.global.enable = vec!["MD041".to_string()];
1232
1233 let rules = crate::rules::all_rules(&config);
1234 let filtered = crate::rules::filter_rules(&rules, &config.global);
1235
1236 let result = crate::lint(
1237 content,
1238 &filtered,
1239 false,
1240 crate::config::MarkdownFlavor::Standard,
1241 Some(&config),
1242 );
1243
1244 let warnings = result.expect("Linting should succeed");
1245
1246 let md041 = warnings.iter().find(|w| w.rule_name.as_deref() == Some("MD041"));
1248 assert!(md041.is_some(), "Should have MD041 warning");
1249 assert_eq!(
1250 md041.unwrap().severity,
1251 crate::rule::Severity::Info,
1252 "MD041 should have Info severity from config"
1253 );
1254 }
1255}