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 {
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 let detail: &dyn std::fmt::Display = if config.withheld_rule_values.contains(T::RULE_NAME) {
43 &crate::config::WITHHELD
44 } else {
45 &e
46 };
47 eprintln!("Warning: Invalid configuration for rule {}: {detail}", T::RULE_NAME);
48 eprintln!("Using default values for rule {}.", T::RULE_NAME);
49 eprintln!("Hint: Check the documentation for valid configuration values.");
50
51 None
52 }
53 }
54 })
55 .unwrap_or_default()
56}
57
58pub fn compile_config_regex(pattern: &str, rule: &str, option: &str, values_withheld: bool) -> Option<regex::Regex> {
70 match regex::Regex::new(pattern) {
71 Ok(regex) => Some(regex),
72 Err(err) => {
73 let detail: &dyn std::fmt::Display = if values_withheld {
74 &crate::config::WITHHELD
75 } else {
76 &err
77 };
78 log::warn!("Invalid {option} for {rule}: {detail}. The option is ignored.");
79 None
80 }
81 }
82}
83
84const NULLABLE_SENTINEL: &str = "\0__nullable__";
89
90const POLYMORPHIC_SENTINEL: &str = "\0__polymorphic__";
97
98pub fn is_nullable_sentinel(value: &toml::Value) -> bool {
100 matches!(value, toml::Value::String(s) if s == NULLABLE_SENTINEL)
101}
102
103pub fn is_polymorphic_sentinel(value: &toml::Value) -> bool {
105 matches!(value, toml::Value::String(s) if s == POLYMORPHIC_SENTINEL)
106}
107
108pub fn polymorphic_sentinel_value() -> toml::Value {
114 toml::Value::String(POLYMORPHIC_SENTINEL.to_string())
115}
116
117pub fn config_schema_table<T: RuleConfig>(config: &T) -> Option<toml::map::Map<String, toml::Value>> {
123 let json_value = serde_json::to_value(config).ok()?;
124 let obj = json_value.as_object()?;
125 let mut table = toml::map::Map::new();
126 for (k, v) in obj {
127 if v.is_null() {
128 table.insert(k.clone(), toml::Value::String(NULLABLE_SENTINEL.to_string()));
129 } else {
130 let toml_v = json_to_toml_value(v).unwrap_or_else(|| toml::Value::String(NULLABLE_SENTINEL.to_string()));
133 table.insert(k.clone(), toml_v);
134 }
135 }
136 Some(table)
137}
138
139pub fn default_config_section_for<T: RuleConfig>() -> Option<(String, toml::Value)> {
146 let json_value = serde_json::to_value(T::default()).ok()?;
147 let toml_value = json_to_toml_value(&json_value)?;
148 match toml_value {
149 toml::Value::Table(table) if !table.is_empty() => Some((T::RULE_NAME.to_string(), toml::Value::Table(table))),
150 _ => None,
151 }
152}
153
154pub fn nullable_config_section_for<T: RuleConfig>() -> Option<(String, toml::Value)> {
158 let table = config_schema_table(&T::default())?;
159 if table.is_empty() {
160 return None;
161 }
162 Some((T::RULE_NAME.to_string(), toml::Value::Table(table)))
163}
164
165#[macro_export]
173macro_rules! impl_rule_config_methods {
174 ($config_ty:ty) => {
175 fn default_config_section(&self) -> Option<(String, toml::Value)> {
176 $crate::rule_config_serde::default_config_section_for::<$config_ty>()
177 }
178
179 fn from_config(config: &$crate::config::Config) -> Box<dyn $crate::rule::Rule>
180 where
181 Self: Sized,
182 {
183 Box::new(Self::from_config_struct(
184 $crate::rule_config_serde::load_rule_config::<$config_ty>(config),
185 ))
186 }
187 };
188 ($config_ty:ty, nullable) => {
189 fn default_config_section(&self) -> Option<(String, toml::Value)> {
190 $crate::rule_config_serde::nullable_config_section_for::<$config_ty>()
191 }
192
193 fn from_config(config: &$crate::config::Config) -> Box<dyn $crate::rule::Rule>
194 where
195 Self: Sized,
196 {
197 Box::new(Self::from_config_struct(
198 $crate::rule_config_serde::load_rule_config::<$config_ty>(config),
199 ))
200 }
201 };
202}
203
204pub fn json_to_toml_value(json_val: &serde_json::Value) -> Option<toml::Value> {
206 match json_val {
207 serde_json::Value::Null => None,
208 serde_json::Value::Bool(b) => Some(toml::Value::Boolean(*b)),
209 serde_json::Value::Number(n) => {
210 if let Some(i) = n.as_i64() {
211 Some(toml::Value::Integer(i))
212 } else {
213 n.as_f64().map(toml::Value::Float)
214 }
215 }
216 serde_json::Value::String(s) => Some(toml::Value::String(s.clone())),
217 serde_json::Value::Array(arr) => {
218 let toml_arr: Vec<_> = arr.iter().filter_map(json_to_toml_value).collect();
219 Some(toml::Value::Array(toml_arr))
220 }
221 serde_json::Value::Object(obj) => {
222 let mut toml_table = toml::map::Map::new();
223 for (k, v) in obj {
224 if let Some(toml_v) = json_to_toml_value(v) {
225 toml_table.insert(k.clone(), toml_v);
226 }
227 }
228 Some(toml::Value::Table(toml_table))
229 }
230 }
231}
232
233pub fn is_rule_name(name: &str) -> bool {
237 let upper = name.to_ascii_uppercase();
238 upper.starts_with("MD") && upper.len() >= 4 && upper[2..].chars().all(|c| c.is_ascii_digit())
239}
240
241#[derive(Debug, Default)]
243pub struct RuleConfigConversion {
244 pub config: Option<crate::config::RuleConfig>,
246 pub warnings: Vec<String>,
248}
249
250pub fn json_to_rule_config(json_value: &serde_json::Value) -> Option<crate::config::RuleConfig> {
258 json_to_rule_config_with_warnings(json_value).config
259}
260
261pub fn json_to_rule_config_with_warnings(json_value: &serde_json::Value) -> RuleConfigConversion {
266 use std::collections::BTreeMap;
267
268 let mut result = RuleConfigConversion::default();
269
270 let Some(obj) = json_value.as_object() else {
271 result.warnings.push(format!(
272 "Expected object for rule config, got {}",
273 json_type_name(json_value)
274 ));
275 return result;
276 };
277
278 let mut values = BTreeMap::new();
279 let mut severity = None;
280
281 for (key, val) in obj {
282 if key == "severity" {
284 if let Some(s) = val.as_str() {
285 match s.to_lowercase().as_str() {
286 "error" => severity = Some(crate::rule::Severity::Error),
287 "warning" => severity = Some(crate::rule::Severity::Warning),
288 "info" => severity = Some(crate::rule::Severity::Info),
289 _ => {
290 result.warnings.push(format!(
291 "Invalid severity '{s}', expected 'error', 'warning', or 'info'"
292 ));
293 }
294 }
295 } else {
296 result
297 .warnings
298 .push(format!("Severity must be a string, got {}", json_type_name(val)));
299 }
300 continue;
301 }
302
303 if let Some(toml_val) = json_to_toml_value(val) {
305 values.insert(key.clone(), toml_val);
306 } else if !val.is_null() {
307 result
308 .warnings
309 .push(format!("Could not convert '{key}' value to config format"));
310 }
311 }
312
313 result.config = Some(crate::config::RuleConfig { severity, values });
314 result
315}
316
317fn json_type_name(val: &serde_json::Value) -> &'static str {
319 match val {
320 serde_json::Value::Null => "null",
321 serde_json::Value::Bool(_) => "boolean",
322 serde_json::Value::Number(_) => "number",
323 serde_json::Value::String(_) => "string",
324 serde_json::Value::Array(_) => "array",
325 serde_json::Value::Object(_) => "object",
326 }
327}
328
329pub fn toml_value_to_json(toml_val: &toml::Value) -> Option<serde_json::Value> {
331 match toml_val {
332 toml::Value::String(s) => Some(serde_json::Value::String(s.clone())),
333 toml::Value::Integer(i) => Some(serde_json::json!(i)),
334 toml::Value::Float(f) => Some(serde_json::json!(f)),
335 toml::Value::Boolean(b) => Some(serde_json::Value::Bool(*b)),
336 toml::Value::Array(arr) => {
337 let json_arr: Vec<_> = arr.iter().filter_map(toml_value_to_json).collect();
338 Some(serde_json::Value::Array(json_arr))
339 }
340 toml::Value::Table(table) => {
341 let mut json_obj = serde_json::Map::new();
342 for (k, v) in table {
343 if let Some(json_v) = toml_value_to_json(v) {
344 json_obj.insert(k.clone(), json_v);
345 }
346 }
347 Some(serde_json::Value::Object(json_obj))
348 }
349 toml::Value::Datetime(_) => None, }
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356 use serde::{Deserialize, Serialize};
357 use std::collections::BTreeMap;
358
359 #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
361 #[serde(default)]
362 struct TestRuleConfig {
363 #[serde(default)]
364 enabled: bool,
365 #[serde(default)]
366 indent: i64,
367 #[serde(default)]
368 style: String,
369 #[serde(default)]
370 items: Vec<String>,
371 }
372
373 impl RuleConfig for TestRuleConfig {
374 const RULE_NAME: &'static str = "TEST001";
375 }
376
377 #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
381 #[serde(default)]
382 struct NullableTestConfig {
383 #[serde(default)]
384 enabled: bool,
385 #[serde(default, alias = "key-order")]
386 key_order: Option<Vec<String>>,
387 #[serde(default, alias = "title-pattern")]
388 title_pattern: Option<String>,
389 }
390
391 impl RuleConfig for NullableTestConfig {
392 const RULE_NAME: &'static str = "TEST_NULLABLE";
393 }
394
395 #[test]
396 fn test_is_nullable_sentinel() {
397 let sentinel = toml::Value::String(NULLABLE_SENTINEL.to_string());
398 assert!(is_nullable_sentinel(&sentinel));
399
400 let regular = toml::Value::String("normal".to_string());
401 assert!(!is_nullable_sentinel(®ular));
402
403 let integer = toml::Value::Integer(42);
404 assert!(!is_nullable_sentinel(&integer));
405 }
406
407 #[test]
408 fn test_is_polymorphic_sentinel() {
409 let sentinel = polymorphic_sentinel_value();
410 assert!(is_polymorphic_sentinel(&sentinel));
411
412 let nullable = toml::Value::String(NULLABLE_SENTINEL.to_string());
414 assert!(!is_polymorphic_sentinel(&nullable));
415 assert!(!is_nullable_sentinel(&sentinel));
416
417 let regular = toml::Value::String("normal".to_string());
418 assert!(!is_polymorphic_sentinel(®ular));
419 }
420
421 #[test]
422 fn test_config_schema_table_preserves_nullable_keys() {
423 let config = NullableTestConfig::default();
424 let table = config_schema_table(&config).unwrap();
425
426 assert!(table.contains_key("enabled"), "enabled key missing");
428 assert!(table.contains_key("key_order"), "key_order key missing");
429 assert!(table.contains_key("title_pattern"), "title_pattern key missing");
430
431 assert!(is_nullable_sentinel(table.get("key_order").unwrap()));
433 assert!(is_nullable_sentinel(table.get("title_pattern").unwrap()));
434
435 assert_eq!(table.get("enabled"), Some(&toml::Value::Boolean(false)));
437 }
438
439 #[test]
440 fn test_config_schema_table_non_null_option_uses_real_value() {
441 let config = NullableTestConfig {
442 enabled: true,
443 key_order: Some(vec!["title".to_string(), "date".to_string()]),
444 title_pattern: Some("pattern".to_string()),
445 };
446 let table = config_schema_table(&config).unwrap();
447
448 let key_order = table.get("key_order").unwrap();
450 assert!(!is_nullable_sentinel(key_order));
451 assert!(matches!(key_order, toml::Value::Array(_)));
452
453 let title_pattern = table.get("title_pattern").unwrap();
454 assert!(!is_nullable_sentinel(title_pattern));
455 assert_eq!(title_pattern, &toml::Value::String("pattern".to_string()));
456 }
457
458 #[test]
459 fn test_json_to_toml_value_still_drops_null() {
460 assert!(json_to_toml_value(&serde_json::Value::Null).is_none());
462 }
463
464 #[test]
465 fn test_config_schema_table_all_keys_present() {
466 let config = NullableTestConfig::default();
467 let table = config_schema_table(&config).unwrap();
468 assert_eq!(table.len(), 3, "Expected 3 keys: enabled, key_order, title_pattern");
469 }
470
471 #[test]
472 fn test_config_schema_table_never_drops_keys() {
473 let mut obj = serde_json::Map::new();
477 obj.insert("real_key".to_string(), serde_json::json!(42));
478 obj.insert("null_key".to_string(), serde_json::Value::Null);
479 let json = serde_json::Value::Object(obj);
480
481 let obj = json.as_object().unwrap();
483 let mut table = toml::map::Map::new();
484 for (k, v) in obj {
485 if v.is_null() {
486 table.insert(k.clone(), toml::Value::String(NULLABLE_SENTINEL.to_string()));
487 } else {
488 let toml_v =
489 json_to_toml_value(v).unwrap_or_else(|| toml::Value::String(NULLABLE_SENTINEL.to_string()));
490 table.insert(k.clone(), toml_v);
491 }
492 }
493
494 assert_eq!(table.len(), 2, "Both keys must be present");
495 assert!(table.contains_key("real_key"));
496 assert!(table.contains_key("null_key"));
497 }
498
499 #[test]
500 fn test_toml_value_to_json_basic_types() {
501 let toml_str = toml::Value::String("hello".to_string());
503 let json_str = toml_value_to_json(&toml_str).unwrap();
504 assert_eq!(json_str, serde_json::Value::String("hello".to_string()));
505
506 let toml_int = toml::Value::Integer(42);
508 let json_int = toml_value_to_json(&toml_int).unwrap();
509 assert_eq!(json_int, serde_json::json!(42));
510
511 let toml_float = toml::Value::Float(1.234);
513 let json_float = toml_value_to_json(&toml_float).unwrap();
514 assert_eq!(json_float, serde_json::json!(1.234));
515
516 let toml_bool = toml::Value::Boolean(true);
518 let json_bool = toml_value_to_json(&toml_bool).unwrap();
519 assert_eq!(json_bool, serde_json::Value::Bool(true));
520 }
521
522 #[test]
523 fn test_toml_value_to_json_complex_types() {
524 let toml_arr = toml::Value::Array(vec![
526 toml::Value::String("a".to_string()),
527 toml::Value::String("b".to_string()),
528 ]);
529 let json_arr = toml_value_to_json(&toml_arr).unwrap();
530 assert_eq!(json_arr, serde_json::json!(["a", "b"]));
531
532 let mut toml_table = toml::map::Map::new();
534 toml_table.insert("key1".to_string(), toml::Value::String("value1".to_string()));
535 toml_table.insert("key2".to_string(), toml::Value::Integer(123));
536 let toml_tbl = toml::Value::Table(toml_table);
537 let json_tbl = toml_value_to_json(&toml_tbl).unwrap();
538
539 let expected = serde_json::json!({
540 "key1": "value1",
541 "key2": 123
542 });
543 assert_eq!(json_tbl, expected);
544 }
545
546 #[test]
547 fn test_toml_value_to_json_datetime() {
548 let toml_dt = toml::Value::Datetime("2023-01-01T00:00:00Z".parse().unwrap());
550 assert!(toml_value_to_json(&toml_dt).is_none());
551 }
552
553 #[test]
554 fn test_json_to_toml_value_basic_types() {
555 assert!(json_to_toml_value(&serde_json::Value::Null).is_none());
557
558 let json_bool = serde_json::Value::Bool(false);
560 let toml_bool = json_to_toml_value(&json_bool).unwrap();
561 assert_eq!(toml_bool, toml::Value::Boolean(false));
562
563 let json_int = serde_json::json!(42);
565 let toml_int = json_to_toml_value(&json_int).unwrap();
566 assert_eq!(toml_int, toml::Value::Integer(42));
567
568 let json_float = serde_json::json!(1.234);
570 let toml_float = json_to_toml_value(&json_float).unwrap();
571 assert_eq!(toml_float, toml::Value::Float(1.234));
572
573 let json_str = serde_json::Value::String("test".to_string());
575 let toml_str = json_to_toml_value(&json_str).unwrap();
576 assert_eq!(toml_str, toml::Value::String("test".to_string()));
577 }
578
579 #[test]
580 fn test_json_to_toml_value_complex_types() {
581 let json_arr = serde_json::json!(["x", "y", "z"]);
583 let toml_arr = json_to_toml_value(&json_arr).unwrap();
584 if let toml::Value::Array(arr) = toml_arr {
585 assert_eq!(arr.len(), 3);
586 assert_eq!(arr[0], toml::Value::String("x".to_string()));
587 assert_eq!(arr[1], toml::Value::String("y".to_string()));
588 assert_eq!(arr[2], toml::Value::String("z".to_string()));
589 } else {
590 panic!("Expected array");
591 }
592
593 let json_obj = serde_json::json!({
595 "name": "test",
596 "count": 10,
597 "active": true
598 });
599 let toml_obj = json_to_toml_value(&json_obj).unwrap();
600 if let toml::Value::Table(table) = toml_obj {
601 assert_eq!(table.get("name"), Some(&toml::Value::String("test".to_string())));
602 assert_eq!(table.get("count"), Some(&toml::Value::Integer(10)));
603 assert_eq!(table.get("active"), Some(&toml::Value::Boolean(true)));
604 } else {
605 panic!("Expected table");
606 }
607 }
608
609 #[test]
610 fn test_load_rule_config_default() {
611 let config = crate::config::Config::default();
613
614 let rule_config: TestRuleConfig = load_rule_config(&config);
616 assert_eq!(rule_config, TestRuleConfig::default());
617 }
618
619 #[test]
620 fn test_load_rule_config_with_values() {
621 let mut config = crate::config::Config::default();
623 let mut rule_values = BTreeMap::new();
624 rule_values.insert("enabled".to_string(), toml::Value::Boolean(true));
625 rule_values.insert("indent".to_string(), toml::Value::Integer(4));
626 rule_values.insert("style".to_string(), toml::Value::String("consistent".to_string()));
627 rule_values.insert(
628 "items".to_string(),
629 toml::Value::Array(vec![
630 toml::Value::String("item1".to_string()),
631 toml::Value::String("item2".to_string()),
632 ]),
633 );
634
635 config.rules.insert(
636 "TEST001".to_string(),
637 crate::config::RuleConfig {
638 severity: None,
639 values: rule_values,
640 },
641 );
642
643 let rule_config: TestRuleConfig = load_rule_config(&config);
645 assert!(rule_config.enabled);
646 assert_eq!(rule_config.indent, 4);
647 assert_eq!(rule_config.style, "consistent");
648 assert_eq!(rule_config.items, vec!["item1", "item2"]);
649 }
650
651 #[test]
652 fn test_load_rule_config_partial() {
653 let mut config = crate::config::Config::default();
655 let mut rule_values = BTreeMap::new();
656 rule_values.insert("enabled".to_string(), toml::Value::Boolean(true));
657 rule_values.insert("style".to_string(), toml::Value::String("custom".to_string()));
658
659 config.rules.insert(
660 "TEST001".to_string(),
661 crate::config::RuleConfig {
662 severity: None,
663 values: rule_values,
664 },
665 );
666
667 let rule_config: TestRuleConfig = load_rule_config(&config);
669 assert!(rule_config.enabled); assert_eq!(rule_config.indent, 0); assert_eq!(rule_config.style, "custom"); assert_eq!(rule_config.items, Vec::<String>::new()); }
674
675 #[test]
676 fn test_conversion_roundtrip() {
677 let original = toml::Value::Table({
679 let mut table = toml::map::Map::new();
680 table.insert("string".to_string(), toml::Value::String("test".to_string()));
681 table.insert("number".to_string(), toml::Value::Integer(42));
682 table.insert("bool".to_string(), toml::Value::Boolean(true));
683 table.insert(
684 "array".to_string(),
685 toml::Value::Array(vec![
686 toml::Value::String("a".to_string()),
687 toml::Value::String("b".to_string()),
688 ]),
689 );
690 table
691 });
692
693 let json = toml_value_to_json(&original).unwrap();
694 let back_to_toml = json_to_toml_value(&json).unwrap();
695
696 assert_eq!(original, back_to_toml);
697 }
698
699 #[test]
700 fn test_edge_cases() {
701 let empty_arr = toml::Value::Array(vec![]);
703 let json_arr = toml_value_to_json(&empty_arr).unwrap();
704 assert_eq!(json_arr, serde_json::json!([]));
705
706 let empty_table = toml::Value::Table(toml::map::Map::new());
708 let json_table = toml_value_to_json(&empty_table).unwrap();
709 assert_eq!(json_table, serde_json::json!({}));
710
711 let nested = toml::Value::Table({
713 let mut outer = toml::map::Map::new();
714 outer.insert(
715 "inner".to_string(),
716 toml::Value::Table({
717 let mut inner = toml::map::Map::new();
718 inner.insert("value".to_string(), toml::Value::Integer(123));
719 inner
720 }),
721 );
722 outer
723 });
724 let json_nested = toml_value_to_json(&nested).unwrap();
725 assert_eq!(
726 json_nested,
727 serde_json::json!({
728 "inner": {
729 "value": 123
730 }
731 })
732 );
733 }
734
735 #[test]
736 fn test_float_edge_cases() {
737 let nan = serde_json::Number::from_f64(f64::NAN);
739 assert!(nan.is_none());
740
741 let inf = serde_json::Number::from_f64(f64::INFINITY);
742 assert!(inf.is_none());
743
744 let valid_float = toml::Value::Float(1.23);
746 let json_float = toml_value_to_json(&valid_float).unwrap();
747 assert_eq!(json_float, serde_json::json!(1.23));
748 }
749
750 #[test]
751 fn test_invalid_config_returns_default() {
752 let mut config = crate::config::Config::default();
754 let mut rule_values = BTreeMap::new();
755 rule_values.insert("unknown_field".to_string(), toml::Value::Boolean(true));
756 rule_values.insert("items".to_string(), toml::Value::Table(toml::map::Map::new()));
758
759 config.rules.insert(
760 "TEST001".to_string(),
761 crate::config::RuleConfig {
762 severity: None,
763 values: rule_values,
764 },
765 );
766
767 let rule_config: TestRuleConfig = load_rule_config(&config);
769 assert_eq!(rule_config, TestRuleConfig::default());
771 }
772
773 #[test]
774 fn test_invalid_field_type() {
775 let mut config = crate::config::Config::default();
777 let mut rule_values = BTreeMap::new();
778 rule_values.insert("indent".to_string(), toml::Value::String("not_a_number".to_string()));
780
781 config.rules.insert(
782 "TEST001".to_string(),
783 crate::config::RuleConfig {
784 severity: None,
785 values: rule_values,
786 },
787 );
788
789 let rule_config: TestRuleConfig = load_rule_config(&config);
791 assert_eq!(rule_config, TestRuleConfig::default());
792 }
793
794 #[test]
797 fn test_is_rule_name_valid() {
798 assert!(is_rule_name("MD001"));
800 assert!(is_rule_name("MD060"));
801 assert!(is_rule_name("MD123"));
802 assert!(is_rule_name("MD999"));
803
804 assert!(is_rule_name("md001"));
806 assert!(is_rule_name("Md060"));
807 assert!(is_rule_name("mD123"));
808
809 assert!(is_rule_name("MD0001"));
811 assert!(is_rule_name("MD12345"));
812 }
813
814 #[test]
815 fn test_is_rule_name_invalid() {
816 assert!(!is_rule_name("MD"));
818 assert!(!is_rule_name("MD1"));
819 assert!(!is_rule_name("M"));
820 assert!(!is_rule_name(""));
821
822 assert!(!is_rule_name("disable"));
824 assert!(!is_rule_name("enable"));
825 assert!(!is_rule_name("flavor"));
826 assert!(!is_rule_name("line-length"));
827 assert!(!is_rule_name("global"));
828
829 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")); }
836
837 #[test]
840 fn test_json_to_rule_config_simple() {
841 let json = serde_json::json!({
842 "enabled": true,
843 "style": "aligned"
844 });
845
846 let rule_config = json_to_rule_config(&json).unwrap();
847
848 assert_eq!(rule_config.values.get("enabled"), Some(&toml::Value::Boolean(true)));
849 assert_eq!(
850 rule_config.values.get("style"),
851 Some(&toml::Value::String("aligned".to_string()))
852 );
853 assert!(rule_config.severity.is_none());
854 }
855
856 #[test]
857 fn test_json_to_rule_config_with_numbers() {
858 let json = serde_json::json!({
859 "line-length": 120,
860 "max-width": 0,
861 "indent": 4
862 });
863
864 let rule_config = json_to_rule_config(&json).unwrap();
865
866 assert_eq!(rule_config.values.get("line-length"), Some(&toml::Value::Integer(120)));
867 assert_eq!(rule_config.values.get("max-width"), Some(&toml::Value::Integer(0)));
868 assert_eq!(rule_config.values.get("indent"), Some(&toml::Value::Integer(4)));
869 }
870
871 #[test]
872 fn test_json_to_rule_config_with_arrays() {
873 let json = serde_json::json!({
874 "names": ["JavaScript", "TypeScript", "React"],
875 "exclude-patterns": ["*.test.md", "draft-*"]
876 });
877
878 let rule_config = json_to_rule_config(&json).unwrap();
879
880 let expected_names = toml::Value::Array(vec![
881 toml::Value::String("JavaScript".to_string()),
882 toml::Value::String("TypeScript".to_string()),
883 toml::Value::String("React".to_string()),
884 ]);
885 assert_eq!(rule_config.values.get("names"), Some(&expected_names));
886
887 let expected_patterns = toml::Value::Array(vec![
888 toml::Value::String("*.test.md".to_string()),
889 toml::Value::String("draft-*".to_string()),
890 ]);
891 assert_eq!(rule_config.values.get("exclude-patterns"), Some(&expected_patterns));
892 }
893
894 #[test]
895 fn test_json_to_rule_config_with_severity() {
896 let json = serde_json::json!({
898 "severity": "error",
899 "style": "aligned"
900 });
901 let rule_config = json_to_rule_config(&json).unwrap();
902 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Error));
903 assert!(!rule_config.values.contains_key("severity")); let json = serde_json::json!({
907 "severity": "warning",
908 "enabled": true
909 });
910 let rule_config = json_to_rule_config(&json).unwrap();
911 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Warning));
912
913 let json = serde_json::json!({
915 "severity": "info"
916 });
917 let rule_config = json_to_rule_config(&json).unwrap();
918 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Info));
919
920 let json = serde_json::json!({
922 "severity": "ERROR"
923 });
924 let rule_config = json_to_rule_config(&json).unwrap();
925 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Error));
926 }
927
928 #[test]
929 fn test_json_to_rule_config_invalid_severity() {
930 let json = serde_json::json!({
932 "severity": "critical",
933 "style": "aligned"
934 });
935 let rule_config = json_to_rule_config(&json).unwrap();
936 assert!(rule_config.severity.is_none()); assert_eq!(
938 rule_config.values.get("style"),
939 Some(&toml::Value::String("aligned".to_string()))
940 );
941
942 let json = serde_json::json!({
944 "severity": 1,
945 "enabled": true
946 });
947 let rule_config = json_to_rule_config(&json).unwrap();
948 assert!(rule_config.severity.is_none()); }
950
951 #[test]
952 fn test_json_to_rule_config_non_object() {
953 assert!(json_to_rule_config(&serde_json::json!(42)).is_none());
955 assert!(json_to_rule_config(&serde_json::json!("string")).is_none());
956 assert!(json_to_rule_config(&serde_json::json!(true)).is_none());
957 assert!(json_to_rule_config(&serde_json::json!([1, 2, 3])).is_none());
958 assert!(json_to_rule_config(&serde_json::Value::Null).is_none());
959 }
960
961 #[test]
962 fn test_json_to_rule_config_empty_object() {
963 let json = serde_json::json!({});
964 let rule_config = json_to_rule_config(&json).unwrap();
965 assert!(rule_config.values.is_empty());
966 assert!(rule_config.severity.is_none());
967 }
968
969 #[test]
970 fn test_json_to_rule_config_nested_objects() {
971 let json = serde_json::json!({
973 "options": {
974 "nested-key": "nested-value",
975 "nested-number": 42
976 }
977 });
978
979 let rule_config = json_to_rule_config(&json).unwrap();
980
981 let options = rule_config.values.get("options").unwrap();
982 if let toml::Value::Table(table) = options {
983 assert_eq!(
984 table.get("nested-key"),
985 Some(&toml::Value::String("nested-value".to_string()))
986 );
987 assert_eq!(table.get("nested-number"), Some(&toml::Value::Integer(42)));
988 } else {
989 panic!("options should be a table");
990 }
991 }
992
993 #[test]
994 fn test_json_to_rule_config_md060_example() {
995 let json = serde_json::json!({
997 "enabled": true,
998 "style": "aligned",
999 "max-width": 120,
1000 "column-align": "auto",
1001 "loose-last-column": false
1002 });
1003
1004 let rule_config = json_to_rule_config(&json).unwrap();
1005
1006 assert_eq!(rule_config.values.get("enabled"), Some(&toml::Value::Boolean(true)));
1007 assert_eq!(
1008 rule_config.values.get("style"),
1009 Some(&toml::Value::String("aligned".to_string()))
1010 );
1011 assert_eq!(rule_config.values.get("max-width"), Some(&toml::Value::Integer(120)));
1012 assert_eq!(
1013 rule_config.values.get("column-align"),
1014 Some(&toml::Value::String("auto".to_string()))
1015 );
1016 assert_eq!(
1017 rule_config.values.get("loose-last-column"),
1018 Some(&toml::Value::Boolean(false))
1019 );
1020 }
1021
1022 #[test]
1023 fn test_json_to_rule_config_md044_example() {
1024 let json = serde_json::json!({
1026 "names": ["JavaScript", "TypeScript", "GitHub", "macOS"],
1027 "code-blocks": false,
1028 "html-elements": false
1029 });
1030
1031 let rule_config = json_to_rule_config(&json).unwrap();
1032
1033 let expected_names = toml::Value::Array(vec![
1034 toml::Value::String("JavaScript".to_string()),
1035 toml::Value::String("TypeScript".to_string()),
1036 toml::Value::String("GitHub".to_string()),
1037 toml::Value::String("macOS".to_string()),
1038 ]);
1039 assert_eq!(rule_config.values.get("names"), Some(&expected_names));
1040 assert_eq!(
1041 rule_config.values.get("code-blocks"),
1042 Some(&toml::Value::Boolean(false))
1043 );
1044 assert_eq!(
1045 rule_config.values.get("html-elements"),
1046 Some(&toml::Value::Boolean(false))
1047 );
1048 }
1049
1050 #[test]
1053 fn test_json_to_rule_config_with_warnings_valid() {
1054 let json = serde_json::json!({
1055 "severity": "error",
1056 "enabled": true
1057 });
1058
1059 let result = json_to_rule_config_with_warnings(&json);
1060
1061 assert!(result.config.is_some());
1062 assert!(
1063 result.warnings.is_empty(),
1064 "Expected no warnings, got: {:?}",
1065 result.warnings
1066 );
1067 assert_eq!(result.config.unwrap().severity, Some(crate::rule::Severity::Error));
1068 }
1069
1070 #[test]
1071 fn test_json_to_rule_config_with_warnings_invalid_severity() {
1072 let json = serde_json::json!({
1073 "severity": "critical",
1074 "style": "aligned"
1075 });
1076
1077 let result = json_to_rule_config_with_warnings(&json);
1078
1079 assert!(result.config.is_some());
1080 assert_eq!(result.warnings.len(), 1);
1081 assert!(result.warnings[0].contains("Invalid severity 'critical'"));
1082 assert!(result.config.unwrap().severity.is_none());
1084 }
1085
1086 #[test]
1087 fn test_json_to_rule_config_with_warnings_wrong_severity_type() {
1088 let json = serde_json::json!({
1089 "severity": 123,
1090 "enabled": true
1091 });
1092
1093 let result = json_to_rule_config_with_warnings(&json);
1094
1095 assert!(result.config.is_some());
1096 assert_eq!(result.warnings.len(), 1);
1097 assert!(result.warnings[0].contains("Severity must be a string"));
1098 }
1099
1100 #[test]
1101 fn test_json_to_rule_config_with_warnings_non_object() {
1102 let json = serde_json::json!("not an object");
1103
1104 let result = json_to_rule_config_with_warnings(&json);
1105
1106 assert!(result.config.is_none());
1107 assert_eq!(result.warnings.len(), 1);
1108 assert!(result.warnings[0].contains("Expected object"));
1109 }
1110
1111 #[test]
1114 fn test_rule_config_integration_with_config() {
1115 let mut config = crate::config::Config::default();
1117
1118 let md060_json = serde_json::json!({
1120 "enabled": true,
1121 "style": "aligned",
1122 "max-width": 120
1123 });
1124 let md013_json = serde_json::json!({
1125 "line-length": 100,
1126 "code-blocks": false
1127 });
1128
1129 if let Some(md060_config) = json_to_rule_config(&md060_json) {
1130 config.rules.insert("MD060".to_string(), md060_config);
1131 }
1132 if let Some(md013_config) = json_to_rule_config(&md013_json) {
1133 config.rules.insert("MD013".to_string(), md013_config);
1134 }
1135
1136 assert!(config.rules.contains_key("MD060"));
1138 assert!(config.rules.contains_key("MD013"));
1139
1140 let md060 = config.rules.get("MD060").unwrap();
1142 assert_eq!(md060.values.get("enabled"), Some(&toml::Value::Boolean(true)));
1143 assert_eq!(
1144 md060.values.get("style"),
1145 Some(&toml::Value::String("aligned".to_string()))
1146 );
1147 assert_eq!(md060.values.get("max-width"), Some(&toml::Value::Integer(120)));
1148 }
1149
1150 #[test]
1151 fn test_rule_config_integration_with_severity() {
1152 let mut config = crate::config::Config::default();
1153
1154 let json = serde_json::json!({
1155 "severity": "error",
1156 "enabled": true
1157 });
1158
1159 if let Some(rule_config) = json_to_rule_config(&json) {
1160 config.rules.insert("MD041".to_string(), rule_config);
1161 }
1162
1163 let md041 = config.rules.get("MD041").unwrap();
1164 assert_eq!(md041.severity, Some(crate::rule::Severity::Error));
1165 }
1166
1167 #[test]
1168 fn test_rule_config_integration_case_normalization() {
1169 let mut config = crate::config::Config::default();
1171
1172 let json = serde_json::json!({ "enabled": true });
1173
1174 for rule_name in ["md060", "MD060", "Md060"] {
1176 if is_rule_name(rule_name)
1177 && let Some(rule_config) = json_to_rule_config(&json)
1178 {
1179 config.rules.insert(rule_name.to_ascii_uppercase(), rule_config);
1180 }
1181 }
1182
1183 assert!(config.rules.contains_key("MD060"));
1185 assert_eq!(config.rules.len(), 1); }
1187
1188 #[test]
1189 fn test_rule_config_integration_filters_non_rules() {
1190 let keys = ["MD060", "disable", "enable", "flavor", "line-length", "global"];
1192
1193 let rule_keys: Vec<_> = keys.iter().filter(|k| is_rule_name(k)).collect();
1194
1195 assert_eq!(rule_keys, vec![&"MD060"]);
1196 }
1197
1198 #[test]
1199 fn test_multiple_rule_configs_with_mixed_validity() {
1200 let rules = vec![
1202 ("MD060", serde_json::json!({ "severity": "error", "style": "aligned" })),
1203 (
1204 "MD013",
1205 serde_json::json!({ "severity": "invalid", "line-length": 100 }),
1206 ),
1207 ("MD041", serde_json::json!({ "enabled": true })),
1208 ];
1209
1210 let mut config = crate::config::Config::default();
1211 let mut all_warnings = Vec::new();
1212
1213 for (name, json) in rules {
1214 let result = json_to_rule_config_with_warnings(&json);
1215 all_warnings.extend(result.warnings);
1216 if let Some(rule_config) = result.config {
1217 config.rules.insert(name.to_string(), rule_config);
1218 }
1219 }
1220
1221 assert_eq!(config.rules.len(), 3);
1223
1224 assert_eq!(all_warnings.len(), 1);
1226 assert!(all_warnings[0].contains("Invalid severity"));
1227
1228 assert_eq!(
1230 config.rules.get("MD060").unwrap().severity,
1231 Some(crate::rule::Severity::Error)
1232 );
1233 assert!(config.rules.get("MD013").unwrap().severity.is_none());
1234 }
1235
1236 #[test]
1240 fn test_end_to_end_md013_line_length_config() {
1241 let content = "# Test\n\nThis is a line that is exactly 50 characters long.\n";
1243
1244 let mut config = crate::config::Config::default();
1246 let json = serde_json::json!({
1247 "line-length": 40
1248 });
1249 if let Some(rule_config) = json_to_rule_config(&json) {
1250 config.rules.insert("MD013".to_string(), rule_config);
1251 }
1252
1253 config.global.enable = vec!["MD013".to_string()];
1255
1256 let rules = crate::rules::all_rules(&config);
1257 let filtered = crate::rules::filter_rules(&rules, &config.global);
1258
1259 let result = crate::lint(
1260 content,
1261 &filtered,
1262 false,
1263 crate::config::MarkdownFlavor::Standard,
1264 None,
1265 Some(&config),
1266 );
1267
1268 let warnings = result.expect("Linting should succeed");
1269
1270 let has_md013 = warnings.iter().any(|w| w.rule_name.as_deref() == Some("MD013"));
1272 assert!(has_md013, "Should have MD013 warning with line-length=40");
1273 }
1274
1275 #[test]
1276 fn test_end_to_end_md013_line_length_no_warning() {
1277 let content = "# Test\n\nThis is a line that is exactly 50 characters long.\n";
1279
1280 let mut config = crate::config::Config::default();
1282 let json = serde_json::json!({
1283 "line-length": 100
1284 });
1285 if let Some(rule_config) = json_to_rule_config(&json) {
1286 config.rules.insert("MD013".to_string(), rule_config);
1287 }
1288
1289 config.global.enable = vec!["MD013".to_string()];
1291
1292 let rules = crate::rules::all_rules(&config);
1293 let filtered = crate::rules::filter_rules(&rules, &config.global);
1294
1295 let result = crate::lint(
1296 content,
1297 &filtered,
1298 false,
1299 crate::config::MarkdownFlavor::Standard,
1300 None,
1301 Some(&config),
1302 );
1303
1304 let warnings = result.expect("Linting should succeed");
1305
1306 let has_md013 = warnings.iter().any(|w| w.rule_name.as_deref() == Some("MD013"));
1308 assert!(!has_md013, "Should NOT have MD013 warning with line-length=100");
1309 }
1310
1311 #[test]
1312 fn test_end_to_end_md044_proper_names() {
1313 let content = "# Test\n\nWe use javascript and typescript.\n";
1315
1316 let mut config = crate::config::Config::default();
1318 let json = serde_json::json!({
1319 "names": ["JavaScript", "TypeScript"],
1320 "code-blocks": false
1321 });
1322 if let Some(rule_config) = json_to_rule_config(&json) {
1323 config.rules.insert("MD044".to_string(), rule_config);
1324 }
1325
1326 config.global.enable = vec!["MD044".to_string()];
1328
1329 let rules = crate::rules::all_rules(&config);
1330 let filtered = crate::rules::filter_rules(&rules, &config.global);
1331
1332 let result = crate::lint(
1333 content,
1334 &filtered,
1335 false,
1336 crate::config::MarkdownFlavor::Standard,
1337 None,
1338 Some(&config),
1339 );
1340
1341 let warnings = result.expect("Linting should succeed");
1342
1343 let md044_warnings: Vec<_> = warnings
1345 .iter()
1346 .filter(|w| w.rule_name.as_deref() == Some("MD044"))
1347 .collect();
1348
1349 assert!(
1350 md044_warnings.len() >= 2,
1351 "Should have MD044 warnings for 'javascript' and 'typescript', got {}",
1352 md044_warnings.len()
1353 );
1354 }
1355
1356 #[test]
1357 fn test_end_to_end_severity_config() {
1358 let content = "test\n"; let mut config = crate::config::Config::default();
1362 let json = serde_json::json!({
1363 "severity": "info"
1364 });
1365 if let Some(rule_config) = json_to_rule_config(&json) {
1366 config.rules.insert("MD041".to_string(), rule_config);
1367 }
1368
1369 config.global.enable = vec!["MD041".to_string()];
1371
1372 let rules = crate::rules::all_rules(&config);
1373 let filtered = crate::rules::filter_rules(&rules, &config.global);
1374
1375 let result = crate::lint(
1376 content,
1377 &filtered,
1378 false,
1379 crate::config::MarkdownFlavor::Standard,
1380 None,
1381 Some(&config),
1382 );
1383
1384 let warnings = result.expect("Linting should succeed");
1385
1386 let md041 = warnings.iter().find(|w| w.rule_name.as_deref() == Some("MD041"));
1388 assert!(md041.is_some(), "Should have MD041 warning");
1389 assert_eq!(
1390 md041.unwrap().severity,
1391 crate::rule::Severity::Info,
1392 "MD041 should have Info severity from config"
1393 );
1394 }
1395}