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)> {
147 let json_value = serde_json::to_value(T::default()).ok()?;
148 let toml_value = json_to_toml_value(&json_value)?;
149 match toml_value {
150 toml::Value::Table(table) if !table.is_empty() => Some((T::RULE_NAME.to_string(), toml::Value::Table(table))),
151 _ => None,
152 }
153}
154
155pub fn config_schema_for<T: RuleConfig>() -> Option<(String, toml::Value)> {
160 let table = config_schema_table(&T::default())?;
161 if table.is_empty() {
162 return None;
163 }
164 Some((T::RULE_NAME.to_string(), toml::Value::Table(table)))
165}
166
167#[macro_export]
175macro_rules! impl_rule_config_schema {
176 ($config_ty:ty) => {
177 fn config_schema(&self) -> Option<(String, toml::Value)> {
178 $crate::rule_config_serde::config_schema_for::<$config_ty>()
179 }
180 };
181}
182
183#[macro_export]
187macro_rules! impl_rule_config_sections {
188 ($config_ty:ty) => {
189 fn default_config_section(&self) -> Option<(String, toml::Value)> {
190 $crate::rule_config_serde::default_config_section_for::<$config_ty>()
191 }
192
193 $crate::impl_rule_config_schema!($config_ty);
194 };
195}
196
197#[macro_export]
201macro_rules! impl_rule_config_methods {
202 ($config_ty:ty) => {
203 $crate::impl_rule_config_sections!($config_ty);
204
205 fn from_config(config: &$crate::config::Config) -> Box<dyn $crate::rule::Rule>
206 where
207 Self: Sized,
208 {
209 Box::new(Self::from_config_struct(
210 $crate::rule_config_serde::load_rule_config::<$config_ty>(config),
211 ))
212 }
213 };
214}
215
216pub fn json_to_toml_value(json_val: &serde_json::Value) -> Option<toml::Value> {
218 match json_val {
219 serde_json::Value::Null => None,
220 serde_json::Value::Bool(b) => Some(toml::Value::Boolean(*b)),
221 serde_json::Value::Number(n) => {
222 if let Some(i) = n.as_i64() {
223 Some(toml::Value::Integer(i))
224 } else {
225 n.as_f64().map(toml::Value::Float)
226 }
227 }
228 serde_json::Value::String(s) => Some(toml::Value::String(s.clone())),
229 serde_json::Value::Array(arr) => {
230 let toml_arr: Vec<_> = arr.iter().filter_map(json_to_toml_value).collect();
231 Some(toml::Value::Array(toml_arr))
232 }
233 serde_json::Value::Object(obj) => {
234 let mut toml_table = toml::map::Map::new();
235 for (k, v) in obj {
236 if let Some(toml_v) = json_to_toml_value(v) {
237 toml_table.insert(k.clone(), toml_v);
238 }
239 }
240 Some(toml::Value::Table(toml_table))
241 }
242 }
243}
244
245pub fn is_rule_name(name: &str) -> bool {
249 let upper = name.to_ascii_uppercase();
250 upper.starts_with("MD") && upper.len() >= 4 && upper[2..].chars().all(|c| c.is_ascii_digit())
251}
252
253#[derive(Debug, Default)]
255pub struct RuleConfigConversion {
256 pub config: Option<crate::config::RuleConfig>,
258 pub warnings: Vec<String>,
260}
261
262pub fn json_to_rule_config(json_value: &serde_json::Value) -> Option<crate::config::RuleConfig> {
270 json_to_rule_config_with_warnings(json_value).config
271}
272
273pub fn json_to_rule_config_with_warnings(json_value: &serde_json::Value) -> RuleConfigConversion {
278 use std::collections::BTreeMap;
279
280 let mut result = RuleConfigConversion::default();
281
282 let Some(obj) = json_value.as_object() else {
283 result.warnings.push(format!(
284 "Expected object for rule config, got {}",
285 json_type_name(json_value)
286 ));
287 return result;
288 };
289
290 let mut values = BTreeMap::new();
291 let mut severity = None;
292
293 for (key, val) in obj {
294 if key == "severity" {
296 if let Some(s) = val.as_str() {
297 match s.to_lowercase().as_str() {
298 "error" => severity = Some(crate::rule::Severity::Error),
299 "warning" => severity = Some(crate::rule::Severity::Warning),
300 "info" => severity = Some(crate::rule::Severity::Info),
301 _ => {
302 result.warnings.push(format!(
303 "Invalid severity '{s}', expected 'error', 'warning', or 'info'"
304 ));
305 }
306 }
307 } else {
308 result
309 .warnings
310 .push(format!("Severity must be a string, got {}", json_type_name(val)));
311 }
312 continue;
313 }
314
315 if let Some(toml_val) = json_to_toml_value(val) {
317 values.insert(key.clone(), toml_val);
318 } else if !val.is_null() {
319 result
320 .warnings
321 .push(format!("Could not convert '{key}' value to config format"));
322 }
323 }
324
325 result.config = Some(crate::config::RuleConfig { severity, values });
326 result
327}
328
329fn json_type_name(val: &serde_json::Value) -> &'static str {
331 match val {
332 serde_json::Value::Null => "null",
333 serde_json::Value::Bool(_) => "boolean",
334 serde_json::Value::Number(_) => "number",
335 serde_json::Value::String(_) => "string",
336 serde_json::Value::Array(_) => "array",
337 serde_json::Value::Object(_) => "object",
338 }
339}
340
341pub fn toml_value_to_json(toml_val: &toml::Value) -> Option<serde_json::Value> {
343 match toml_val {
344 toml::Value::String(s) => Some(serde_json::Value::String(s.clone())),
345 toml::Value::Integer(i) => Some(serde_json::json!(i)),
346 toml::Value::Float(f) => Some(serde_json::json!(f)),
347 toml::Value::Boolean(b) => Some(serde_json::Value::Bool(*b)),
348 toml::Value::Array(arr) => {
349 let json_arr: Vec<_> = arr.iter().filter_map(toml_value_to_json).collect();
350 Some(serde_json::Value::Array(json_arr))
351 }
352 toml::Value::Table(table) => {
353 let mut json_obj = serde_json::Map::new();
354 for (k, v) in table {
355 if let Some(json_v) = toml_value_to_json(v) {
356 json_obj.insert(k.clone(), json_v);
357 }
358 }
359 Some(serde_json::Value::Object(json_obj))
360 }
361 toml::Value::Datetime(_) => None, }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368 use serde::{Deserialize, Serialize};
369 use std::collections::BTreeMap;
370
371 #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
373 #[serde(default)]
374 struct TestRuleConfig {
375 #[serde(default)]
376 enabled: bool,
377 #[serde(default)]
378 indent: i64,
379 #[serde(default)]
380 style: String,
381 #[serde(default)]
382 items: Vec<String>,
383 }
384
385 impl RuleConfig for TestRuleConfig {
386 const RULE_NAME: &'static str = "TEST001";
387 }
388
389 #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
393 #[serde(default)]
394 struct NullableTestConfig {
395 #[serde(default)]
396 enabled: bool,
397 #[serde(default, alias = "key-order")]
398 key_order: Option<Vec<String>>,
399 #[serde(default, alias = "title-pattern")]
400 title_pattern: Option<String>,
401 }
402
403 impl RuleConfig for NullableTestConfig {
404 const RULE_NAME: &'static str = "TEST_NULLABLE";
405 }
406
407 #[test]
408 fn test_is_nullable_sentinel() {
409 let sentinel = toml::Value::String(NULLABLE_SENTINEL.to_string());
410 assert!(is_nullable_sentinel(&sentinel));
411
412 let regular = toml::Value::String("normal".to_string());
413 assert!(!is_nullable_sentinel(®ular));
414
415 let integer = toml::Value::Integer(42);
416 assert!(!is_nullable_sentinel(&integer));
417 }
418
419 #[test]
420 fn test_is_polymorphic_sentinel() {
421 let sentinel = polymorphic_sentinel_value();
422 assert!(is_polymorphic_sentinel(&sentinel));
423
424 let nullable = toml::Value::String(NULLABLE_SENTINEL.to_string());
426 assert!(!is_polymorphic_sentinel(&nullable));
427 assert!(!is_nullable_sentinel(&sentinel));
428
429 let regular = toml::Value::String("normal".to_string());
430 assert!(!is_polymorphic_sentinel(®ular));
431 }
432
433 #[test]
434 fn test_config_schema_table_preserves_nullable_keys() {
435 let config = NullableTestConfig::default();
436 let table = config_schema_table(&config).unwrap();
437
438 assert!(table.contains_key("enabled"), "enabled key missing");
440 assert!(table.contains_key("key_order"), "key_order key missing");
441 assert!(table.contains_key("title_pattern"), "title_pattern key missing");
442
443 assert!(is_nullable_sentinel(table.get("key_order").unwrap()));
445 assert!(is_nullable_sentinel(table.get("title_pattern").unwrap()));
446
447 assert_eq!(table.get("enabled"), Some(&toml::Value::Boolean(false)));
449 }
450
451 #[test]
452 fn test_config_schema_table_non_null_option_uses_real_value() {
453 let config = NullableTestConfig {
454 enabled: true,
455 key_order: Some(vec!["title".to_string(), "date".to_string()]),
456 title_pattern: Some("pattern".to_string()),
457 };
458 let table = config_schema_table(&config).unwrap();
459
460 let key_order = table.get("key_order").unwrap();
462 assert!(!is_nullable_sentinel(key_order));
463 assert!(matches!(key_order, toml::Value::Array(_)));
464
465 let title_pattern = table.get("title_pattern").unwrap();
466 assert!(!is_nullable_sentinel(title_pattern));
467 assert_eq!(title_pattern, &toml::Value::String("pattern".to_string()));
468 }
469
470 #[test]
471 fn test_json_to_toml_value_still_drops_null() {
472 assert!(json_to_toml_value(&serde_json::Value::Null).is_none());
474 }
475
476 #[test]
477 fn test_config_schema_table_all_keys_present() {
478 let config = NullableTestConfig::default();
479 let table = config_schema_table(&config).unwrap();
480 assert_eq!(table.len(), 3, "Expected 3 keys: enabled, key_order, title_pattern");
481 }
482
483 #[test]
484 fn test_config_schema_table_never_drops_keys() {
485 let mut obj = serde_json::Map::new();
489 obj.insert("real_key".to_string(), serde_json::json!(42));
490 obj.insert("null_key".to_string(), serde_json::Value::Null);
491 let json = serde_json::Value::Object(obj);
492
493 let obj = json.as_object().unwrap();
495 let mut table = toml::map::Map::new();
496 for (k, v) in obj {
497 if v.is_null() {
498 table.insert(k.clone(), toml::Value::String(NULLABLE_SENTINEL.to_string()));
499 } else {
500 let toml_v =
501 json_to_toml_value(v).unwrap_or_else(|| toml::Value::String(NULLABLE_SENTINEL.to_string()));
502 table.insert(k.clone(), toml_v);
503 }
504 }
505
506 assert_eq!(table.len(), 2, "Both keys must be present");
507 assert!(table.contains_key("real_key"));
508 assert!(table.contains_key("null_key"));
509 }
510
511 #[test]
512 fn test_toml_value_to_json_basic_types() {
513 let toml_str = toml::Value::String("hello".to_string());
515 let json_str = toml_value_to_json(&toml_str).unwrap();
516 assert_eq!(json_str, serde_json::Value::String("hello".to_string()));
517
518 let toml_int = toml::Value::Integer(42);
520 let json_int = toml_value_to_json(&toml_int).unwrap();
521 assert_eq!(json_int, serde_json::json!(42));
522
523 let toml_float = toml::Value::Float(1.234);
525 let json_float = toml_value_to_json(&toml_float).unwrap();
526 assert_eq!(json_float, serde_json::json!(1.234));
527
528 let toml_bool = toml::Value::Boolean(true);
530 let json_bool = toml_value_to_json(&toml_bool).unwrap();
531 assert_eq!(json_bool, serde_json::Value::Bool(true));
532 }
533
534 #[test]
535 fn test_toml_value_to_json_complex_types() {
536 let toml_arr = toml::Value::Array(vec![
538 toml::Value::String("a".to_string()),
539 toml::Value::String("b".to_string()),
540 ]);
541 let json_arr = toml_value_to_json(&toml_arr).unwrap();
542 assert_eq!(json_arr, serde_json::json!(["a", "b"]));
543
544 let mut toml_table = toml::map::Map::new();
546 toml_table.insert("key1".to_string(), toml::Value::String("value1".to_string()));
547 toml_table.insert("key2".to_string(), toml::Value::Integer(123));
548 let toml_tbl = toml::Value::Table(toml_table);
549 let json_tbl = toml_value_to_json(&toml_tbl).unwrap();
550
551 let expected = serde_json::json!({
552 "key1": "value1",
553 "key2": 123
554 });
555 assert_eq!(json_tbl, expected);
556 }
557
558 #[test]
559 fn test_toml_value_to_json_datetime() {
560 let toml_dt = toml::Value::Datetime("2023-01-01T00:00:00Z".parse().unwrap());
562 assert!(toml_value_to_json(&toml_dt).is_none());
563 }
564
565 #[test]
566 fn test_json_to_toml_value_basic_types() {
567 assert!(json_to_toml_value(&serde_json::Value::Null).is_none());
569
570 let json_bool = serde_json::Value::Bool(false);
572 let toml_bool = json_to_toml_value(&json_bool).unwrap();
573 assert_eq!(toml_bool, toml::Value::Boolean(false));
574
575 let json_int = serde_json::json!(42);
577 let toml_int = json_to_toml_value(&json_int).unwrap();
578 assert_eq!(toml_int, toml::Value::Integer(42));
579
580 let json_float = serde_json::json!(1.234);
582 let toml_float = json_to_toml_value(&json_float).unwrap();
583 assert_eq!(toml_float, toml::Value::Float(1.234));
584
585 let json_str = serde_json::Value::String("test".to_string());
587 let toml_str = json_to_toml_value(&json_str).unwrap();
588 assert_eq!(toml_str, toml::Value::String("test".to_string()));
589 }
590
591 #[test]
592 fn test_json_to_toml_value_complex_types() {
593 let json_arr = serde_json::json!(["x", "y", "z"]);
595 let toml_arr = json_to_toml_value(&json_arr).unwrap();
596 if let toml::Value::Array(arr) = toml_arr {
597 assert_eq!(arr.len(), 3);
598 assert_eq!(arr[0], toml::Value::String("x".to_string()));
599 assert_eq!(arr[1], toml::Value::String("y".to_string()));
600 assert_eq!(arr[2], toml::Value::String("z".to_string()));
601 } else {
602 panic!("Expected array");
603 }
604
605 let json_obj = serde_json::json!({
607 "name": "test",
608 "count": 10,
609 "active": true
610 });
611 let toml_obj = json_to_toml_value(&json_obj).unwrap();
612 if let toml::Value::Table(table) = toml_obj {
613 assert_eq!(table.get("name"), Some(&toml::Value::String("test".to_string())));
614 assert_eq!(table.get("count"), Some(&toml::Value::Integer(10)));
615 assert_eq!(table.get("active"), Some(&toml::Value::Boolean(true)));
616 } else {
617 panic!("Expected table");
618 }
619 }
620
621 #[test]
622 fn test_load_rule_config_default() {
623 let config = crate::config::Config::default();
625
626 let rule_config: TestRuleConfig = load_rule_config(&config);
628 assert_eq!(rule_config, TestRuleConfig::default());
629 }
630
631 #[test]
632 fn test_load_rule_config_with_values() {
633 let mut config = crate::config::Config::default();
635 let mut rule_values = BTreeMap::new();
636 rule_values.insert("enabled".to_string(), toml::Value::Boolean(true));
637 rule_values.insert("indent".to_string(), toml::Value::Integer(4));
638 rule_values.insert("style".to_string(), toml::Value::String("consistent".to_string()));
639 rule_values.insert(
640 "items".to_string(),
641 toml::Value::Array(vec![
642 toml::Value::String("item1".to_string()),
643 toml::Value::String("item2".to_string()),
644 ]),
645 );
646
647 config.rules.insert(
648 "TEST001".to_string(),
649 crate::config::RuleConfig {
650 severity: None,
651 values: rule_values,
652 },
653 );
654
655 let rule_config: TestRuleConfig = load_rule_config(&config);
657 assert!(rule_config.enabled);
658 assert_eq!(rule_config.indent, 4);
659 assert_eq!(rule_config.style, "consistent");
660 assert_eq!(rule_config.items, vec!["item1", "item2"]);
661 }
662
663 #[test]
664 fn test_load_rule_config_partial() {
665 let mut config = crate::config::Config::default();
667 let mut rule_values = BTreeMap::new();
668 rule_values.insert("enabled".to_string(), toml::Value::Boolean(true));
669 rule_values.insert("style".to_string(), toml::Value::String("custom".to_string()));
670
671 config.rules.insert(
672 "TEST001".to_string(),
673 crate::config::RuleConfig {
674 severity: None,
675 values: rule_values,
676 },
677 );
678
679 let rule_config: TestRuleConfig = load_rule_config(&config);
681 assert!(rule_config.enabled); assert_eq!(rule_config.indent, 0); assert_eq!(rule_config.style, "custom"); assert_eq!(rule_config.items, Vec::<String>::new()); }
686
687 #[test]
688 fn test_conversion_roundtrip() {
689 let original = toml::Value::Table({
691 let mut table = toml::map::Map::new();
692 table.insert("string".to_string(), toml::Value::String("test".to_string()));
693 table.insert("number".to_string(), toml::Value::Integer(42));
694 table.insert("bool".to_string(), toml::Value::Boolean(true));
695 table.insert(
696 "array".to_string(),
697 toml::Value::Array(vec![
698 toml::Value::String("a".to_string()),
699 toml::Value::String("b".to_string()),
700 ]),
701 );
702 table
703 });
704
705 let json = toml_value_to_json(&original).unwrap();
706 let back_to_toml = json_to_toml_value(&json).unwrap();
707
708 assert_eq!(original, back_to_toml);
709 }
710
711 #[test]
712 fn test_edge_cases() {
713 let empty_arr = toml::Value::Array(vec![]);
715 let json_arr = toml_value_to_json(&empty_arr).unwrap();
716 assert_eq!(json_arr, serde_json::json!([]));
717
718 let empty_table = toml::Value::Table(toml::map::Map::new());
720 let json_table = toml_value_to_json(&empty_table).unwrap();
721 assert_eq!(json_table, serde_json::json!({}));
722
723 let nested = toml::Value::Table({
725 let mut outer = toml::map::Map::new();
726 outer.insert(
727 "inner".to_string(),
728 toml::Value::Table({
729 let mut inner = toml::map::Map::new();
730 inner.insert("value".to_string(), toml::Value::Integer(123));
731 inner
732 }),
733 );
734 outer
735 });
736 let json_nested = toml_value_to_json(&nested).unwrap();
737 assert_eq!(
738 json_nested,
739 serde_json::json!({
740 "inner": {
741 "value": 123
742 }
743 })
744 );
745 }
746
747 #[test]
748 fn test_float_edge_cases() {
749 let nan = serde_json::Number::from_f64(f64::NAN);
751 assert!(nan.is_none());
752
753 let inf = serde_json::Number::from_f64(f64::INFINITY);
754 assert!(inf.is_none());
755
756 let valid_float = toml::Value::Float(1.23);
758 let json_float = toml_value_to_json(&valid_float).unwrap();
759 assert_eq!(json_float, serde_json::json!(1.23));
760 }
761
762 #[test]
763 fn test_invalid_config_returns_default() {
764 let mut config = crate::config::Config::default();
766 let mut rule_values = BTreeMap::new();
767 rule_values.insert("unknown_field".to_string(), toml::Value::Boolean(true));
768 rule_values.insert("items".to_string(), toml::Value::Table(toml::map::Map::new()));
770
771 config.rules.insert(
772 "TEST001".to_string(),
773 crate::config::RuleConfig {
774 severity: None,
775 values: rule_values,
776 },
777 );
778
779 let rule_config: TestRuleConfig = load_rule_config(&config);
781 assert_eq!(rule_config, TestRuleConfig::default());
783 }
784
785 #[test]
786 fn test_invalid_field_type() {
787 let mut config = crate::config::Config::default();
789 let mut rule_values = BTreeMap::new();
790 rule_values.insert("indent".to_string(), toml::Value::String("not_a_number".to_string()));
792
793 config.rules.insert(
794 "TEST001".to_string(),
795 crate::config::RuleConfig {
796 severity: None,
797 values: rule_values,
798 },
799 );
800
801 let rule_config: TestRuleConfig = load_rule_config(&config);
803 assert_eq!(rule_config, TestRuleConfig::default());
804 }
805
806 #[test]
809 fn test_is_rule_name_valid() {
810 assert!(is_rule_name("MD001"));
812 assert!(is_rule_name("MD060"));
813 assert!(is_rule_name("MD123"));
814 assert!(is_rule_name("MD999"));
815
816 assert!(is_rule_name("md001"));
818 assert!(is_rule_name("Md060"));
819 assert!(is_rule_name("mD123"));
820
821 assert!(is_rule_name("MD0001"));
823 assert!(is_rule_name("MD12345"));
824 }
825
826 #[test]
827 fn test_is_rule_name_invalid() {
828 assert!(!is_rule_name("MD"));
830 assert!(!is_rule_name("MD1"));
831 assert!(!is_rule_name("M"));
832 assert!(!is_rule_name(""));
833
834 assert!(!is_rule_name("disable"));
836 assert!(!is_rule_name("enable"));
837 assert!(!is_rule_name("flavor"));
838 assert!(!is_rule_name("line-length"));
839 assert!(!is_rule_name("global"));
840
841 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")); }
848
849 #[test]
852 fn test_json_to_rule_config_simple() {
853 let json = serde_json::json!({
854 "enabled": true,
855 "style": "aligned"
856 });
857
858 let rule_config = json_to_rule_config(&json).unwrap();
859
860 assert_eq!(rule_config.values.get("enabled"), Some(&toml::Value::Boolean(true)));
861 assert_eq!(
862 rule_config.values.get("style"),
863 Some(&toml::Value::String("aligned".to_string()))
864 );
865 assert!(rule_config.severity.is_none());
866 }
867
868 #[test]
869 fn test_json_to_rule_config_with_numbers() {
870 let json = serde_json::json!({
871 "line-length": 120,
872 "max-width": 0,
873 "indent": 4
874 });
875
876 let rule_config = json_to_rule_config(&json).unwrap();
877
878 assert_eq!(rule_config.values.get("line-length"), Some(&toml::Value::Integer(120)));
879 assert_eq!(rule_config.values.get("max-width"), Some(&toml::Value::Integer(0)));
880 assert_eq!(rule_config.values.get("indent"), Some(&toml::Value::Integer(4)));
881 }
882
883 #[test]
884 fn test_json_to_rule_config_with_arrays() {
885 let json = serde_json::json!({
886 "names": ["JavaScript", "TypeScript", "React"],
887 "exclude-patterns": ["*.test.md", "draft-*"]
888 });
889
890 let rule_config = json_to_rule_config(&json).unwrap();
891
892 let expected_names = toml::Value::Array(vec![
893 toml::Value::String("JavaScript".to_string()),
894 toml::Value::String("TypeScript".to_string()),
895 toml::Value::String("React".to_string()),
896 ]);
897 assert_eq!(rule_config.values.get("names"), Some(&expected_names));
898
899 let expected_patterns = toml::Value::Array(vec![
900 toml::Value::String("*.test.md".to_string()),
901 toml::Value::String("draft-*".to_string()),
902 ]);
903 assert_eq!(rule_config.values.get("exclude-patterns"), Some(&expected_patterns));
904 }
905
906 #[test]
907 fn test_json_to_rule_config_with_severity() {
908 let json = serde_json::json!({
910 "severity": "error",
911 "style": "aligned"
912 });
913 let rule_config = json_to_rule_config(&json).unwrap();
914 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Error));
915 assert!(!rule_config.values.contains_key("severity")); let json = serde_json::json!({
919 "severity": "warning",
920 "enabled": true
921 });
922 let rule_config = json_to_rule_config(&json).unwrap();
923 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Warning));
924
925 let json = serde_json::json!({
927 "severity": "info"
928 });
929 let rule_config = json_to_rule_config(&json).unwrap();
930 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Info));
931
932 let json = serde_json::json!({
934 "severity": "ERROR"
935 });
936 let rule_config = json_to_rule_config(&json).unwrap();
937 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Error));
938 }
939
940 #[test]
941 fn test_json_to_rule_config_invalid_severity() {
942 let json = serde_json::json!({
944 "severity": "critical",
945 "style": "aligned"
946 });
947 let rule_config = json_to_rule_config(&json).unwrap();
948 assert!(rule_config.severity.is_none()); assert_eq!(
950 rule_config.values.get("style"),
951 Some(&toml::Value::String("aligned".to_string()))
952 );
953
954 let json = serde_json::json!({
956 "severity": 1,
957 "enabled": true
958 });
959 let rule_config = json_to_rule_config(&json).unwrap();
960 assert!(rule_config.severity.is_none()); }
962
963 #[test]
964 fn test_json_to_rule_config_non_object() {
965 assert!(json_to_rule_config(&serde_json::json!(42)).is_none());
967 assert!(json_to_rule_config(&serde_json::json!("string")).is_none());
968 assert!(json_to_rule_config(&serde_json::json!(true)).is_none());
969 assert!(json_to_rule_config(&serde_json::json!([1, 2, 3])).is_none());
970 assert!(json_to_rule_config(&serde_json::Value::Null).is_none());
971 }
972
973 #[test]
974 fn test_json_to_rule_config_empty_object() {
975 let json = serde_json::json!({});
976 let rule_config = json_to_rule_config(&json).unwrap();
977 assert!(rule_config.values.is_empty());
978 assert!(rule_config.severity.is_none());
979 }
980
981 #[test]
982 fn test_json_to_rule_config_nested_objects() {
983 let json = serde_json::json!({
985 "options": {
986 "nested-key": "nested-value",
987 "nested-number": 42
988 }
989 });
990
991 let rule_config = json_to_rule_config(&json).unwrap();
992
993 let options = rule_config.values.get("options").unwrap();
994 if let toml::Value::Table(table) = options {
995 assert_eq!(
996 table.get("nested-key"),
997 Some(&toml::Value::String("nested-value".to_string()))
998 );
999 assert_eq!(table.get("nested-number"), Some(&toml::Value::Integer(42)));
1000 } else {
1001 panic!("options should be a table");
1002 }
1003 }
1004
1005 #[test]
1006 fn test_json_to_rule_config_md060_example() {
1007 let json = serde_json::json!({
1009 "enabled": true,
1010 "style": "aligned",
1011 "max-width": 120,
1012 "column-align": "auto",
1013 "loose-last-column": false
1014 });
1015
1016 let rule_config = json_to_rule_config(&json).unwrap();
1017
1018 assert_eq!(rule_config.values.get("enabled"), Some(&toml::Value::Boolean(true)));
1019 assert_eq!(
1020 rule_config.values.get("style"),
1021 Some(&toml::Value::String("aligned".to_string()))
1022 );
1023 assert_eq!(rule_config.values.get("max-width"), Some(&toml::Value::Integer(120)));
1024 assert_eq!(
1025 rule_config.values.get("column-align"),
1026 Some(&toml::Value::String("auto".to_string()))
1027 );
1028 assert_eq!(
1029 rule_config.values.get("loose-last-column"),
1030 Some(&toml::Value::Boolean(false))
1031 );
1032 }
1033
1034 #[test]
1035 fn test_json_to_rule_config_md044_example() {
1036 let json = serde_json::json!({
1038 "names": ["JavaScript", "TypeScript", "GitHub", "macOS"],
1039 "code-blocks": false,
1040 "html-elements": false
1041 });
1042
1043 let rule_config = json_to_rule_config(&json).unwrap();
1044
1045 let expected_names = toml::Value::Array(vec![
1046 toml::Value::String("JavaScript".to_string()),
1047 toml::Value::String("TypeScript".to_string()),
1048 toml::Value::String("GitHub".to_string()),
1049 toml::Value::String("macOS".to_string()),
1050 ]);
1051 assert_eq!(rule_config.values.get("names"), Some(&expected_names));
1052 assert_eq!(
1053 rule_config.values.get("code-blocks"),
1054 Some(&toml::Value::Boolean(false))
1055 );
1056 assert_eq!(
1057 rule_config.values.get("html-elements"),
1058 Some(&toml::Value::Boolean(false))
1059 );
1060 }
1061
1062 #[test]
1065 fn test_json_to_rule_config_with_warnings_valid() {
1066 let json = serde_json::json!({
1067 "severity": "error",
1068 "enabled": true
1069 });
1070
1071 let result = json_to_rule_config_with_warnings(&json);
1072
1073 assert!(result.config.is_some());
1074 assert!(
1075 result.warnings.is_empty(),
1076 "Expected no warnings, got: {:?}",
1077 result.warnings
1078 );
1079 assert_eq!(result.config.unwrap().severity, Some(crate::rule::Severity::Error));
1080 }
1081
1082 #[test]
1083 fn test_json_to_rule_config_with_warnings_invalid_severity() {
1084 let json = serde_json::json!({
1085 "severity": "critical",
1086 "style": "aligned"
1087 });
1088
1089 let result = json_to_rule_config_with_warnings(&json);
1090
1091 assert!(result.config.is_some());
1092 assert_eq!(result.warnings.len(), 1);
1093 assert!(result.warnings[0].contains("Invalid severity 'critical'"));
1094 assert!(result.config.unwrap().severity.is_none());
1096 }
1097
1098 #[test]
1099 fn test_json_to_rule_config_with_warnings_wrong_severity_type() {
1100 let json = serde_json::json!({
1101 "severity": 123,
1102 "enabled": true
1103 });
1104
1105 let result = json_to_rule_config_with_warnings(&json);
1106
1107 assert!(result.config.is_some());
1108 assert_eq!(result.warnings.len(), 1);
1109 assert!(result.warnings[0].contains("Severity must be a string"));
1110 }
1111
1112 #[test]
1113 fn test_json_to_rule_config_with_warnings_non_object() {
1114 let json = serde_json::json!("not an object");
1115
1116 let result = json_to_rule_config_with_warnings(&json);
1117
1118 assert!(result.config.is_none());
1119 assert_eq!(result.warnings.len(), 1);
1120 assert!(result.warnings[0].contains("Expected object"));
1121 }
1122
1123 #[test]
1126 fn test_rule_config_integration_with_config() {
1127 let mut config = crate::config::Config::default();
1129
1130 let md060_json = serde_json::json!({
1132 "enabled": true,
1133 "style": "aligned",
1134 "max-width": 120
1135 });
1136 let md013_json = serde_json::json!({
1137 "line-length": 100,
1138 "code-blocks": false
1139 });
1140
1141 if let Some(md060_config) = json_to_rule_config(&md060_json) {
1142 config.rules.insert("MD060".to_string(), md060_config);
1143 }
1144 if let Some(md013_config) = json_to_rule_config(&md013_json) {
1145 config.rules.insert("MD013".to_string(), md013_config);
1146 }
1147
1148 assert!(config.rules.contains_key("MD060"));
1150 assert!(config.rules.contains_key("MD013"));
1151
1152 let md060 = config.rules.get("MD060").unwrap();
1154 assert_eq!(md060.values.get("enabled"), Some(&toml::Value::Boolean(true)));
1155 assert_eq!(
1156 md060.values.get("style"),
1157 Some(&toml::Value::String("aligned".to_string()))
1158 );
1159 assert_eq!(md060.values.get("max-width"), Some(&toml::Value::Integer(120)));
1160 }
1161
1162 #[test]
1163 fn test_rule_config_integration_with_severity() {
1164 let mut config = crate::config::Config::default();
1165
1166 let json = serde_json::json!({
1167 "severity": "error",
1168 "enabled": true
1169 });
1170
1171 if let Some(rule_config) = json_to_rule_config(&json) {
1172 config.rules.insert("MD041".to_string(), rule_config);
1173 }
1174
1175 let md041 = config.rules.get("MD041").unwrap();
1176 assert_eq!(md041.severity, Some(crate::rule::Severity::Error));
1177 }
1178
1179 #[test]
1180 fn test_rule_config_integration_case_normalization() {
1181 let mut config = crate::config::Config::default();
1183
1184 let json = serde_json::json!({ "enabled": true });
1185
1186 for rule_name in ["md060", "MD060", "Md060"] {
1188 if is_rule_name(rule_name)
1189 && let Some(rule_config) = json_to_rule_config(&json)
1190 {
1191 config.rules.insert(rule_name.to_ascii_uppercase(), rule_config);
1192 }
1193 }
1194
1195 assert!(config.rules.contains_key("MD060"));
1197 assert_eq!(config.rules.len(), 1); }
1199
1200 #[test]
1201 fn test_rule_config_integration_filters_non_rules() {
1202 let keys = ["MD060", "disable", "enable", "flavor", "line-length", "global"];
1204
1205 let rule_keys: Vec<_> = keys.iter().filter(|k| is_rule_name(k)).collect();
1206
1207 assert_eq!(rule_keys, vec![&"MD060"]);
1208 }
1209
1210 #[test]
1211 fn test_multiple_rule_configs_with_mixed_validity() {
1212 let rules = vec![
1214 ("MD060", serde_json::json!({ "severity": "error", "style": "aligned" })),
1215 (
1216 "MD013",
1217 serde_json::json!({ "severity": "invalid", "line-length": 100 }),
1218 ),
1219 ("MD041", serde_json::json!({ "enabled": true })),
1220 ];
1221
1222 let mut config = crate::config::Config::default();
1223 let mut all_warnings = Vec::new();
1224
1225 for (name, json) in rules {
1226 let result = json_to_rule_config_with_warnings(&json);
1227 all_warnings.extend(result.warnings);
1228 if let Some(rule_config) = result.config {
1229 config.rules.insert(name.to_string(), rule_config);
1230 }
1231 }
1232
1233 assert_eq!(config.rules.len(), 3);
1235
1236 assert_eq!(all_warnings.len(), 1);
1238 assert!(all_warnings[0].contains("Invalid severity"));
1239
1240 assert_eq!(
1242 config.rules.get("MD060").unwrap().severity,
1243 Some(crate::rule::Severity::Error)
1244 );
1245 assert!(config.rules.get("MD013").unwrap().severity.is_none());
1246 }
1247
1248 #[test]
1252 fn test_end_to_end_md013_line_length_config() {
1253 let content = "# Test\n\nThis is a line that is exactly 50 characters long.\n";
1255
1256 let mut config = crate::config::Config::default();
1258 let json = serde_json::json!({
1259 "line-length": 40
1260 });
1261 if let Some(rule_config) = json_to_rule_config(&json) {
1262 config.rules.insert("MD013".to_string(), rule_config);
1263 }
1264
1265 config.global.enable = vec!["MD013".to_string()];
1267
1268 let rules = crate::rules::all_rules(&config);
1269 let filtered = crate::rules::filter_rules(&rules, &config.global);
1270
1271 let result = crate::lint(
1272 content,
1273 &filtered,
1274 false,
1275 crate::config::MarkdownFlavor::Standard,
1276 None,
1277 Some(&config),
1278 );
1279
1280 let warnings = result.expect("Linting should succeed");
1281
1282 let has_md013 = warnings.iter().any(|w| w.rule_name.as_deref() == Some("MD013"));
1284 assert!(has_md013, "Should have MD013 warning with line-length=40");
1285 }
1286
1287 #[test]
1288 fn test_end_to_end_md013_line_length_no_warning() {
1289 let content = "# Test\n\nThis is a line that is exactly 50 characters long.\n";
1291
1292 let mut config = crate::config::Config::default();
1294 let json = serde_json::json!({
1295 "line-length": 100
1296 });
1297 if let Some(rule_config) = json_to_rule_config(&json) {
1298 config.rules.insert("MD013".to_string(), rule_config);
1299 }
1300
1301 config.global.enable = vec!["MD013".to_string()];
1303
1304 let rules = crate::rules::all_rules(&config);
1305 let filtered = crate::rules::filter_rules(&rules, &config.global);
1306
1307 let result = crate::lint(
1308 content,
1309 &filtered,
1310 false,
1311 crate::config::MarkdownFlavor::Standard,
1312 None,
1313 Some(&config),
1314 );
1315
1316 let warnings = result.expect("Linting should succeed");
1317
1318 let has_md013 = warnings.iter().any(|w| w.rule_name.as_deref() == Some("MD013"));
1320 assert!(!has_md013, "Should NOT have MD013 warning with line-length=100");
1321 }
1322
1323 #[test]
1324 fn test_end_to_end_md044_proper_names() {
1325 let content = "# Test\n\nWe use javascript and typescript.\n";
1327
1328 let mut config = crate::config::Config::default();
1330 let json = serde_json::json!({
1331 "names": ["JavaScript", "TypeScript"],
1332 "code-blocks": false
1333 });
1334 if let Some(rule_config) = json_to_rule_config(&json) {
1335 config.rules.insert("MD044".to_string(), rule_config);
1336 }
1337
1338 config.global.enable = vec!["MD044".to_string()];
1340
1341 let rules = crate::rules::all_rules(&config);
1342 let filtered = crate::rules::filter_rules(&rules, &config.global);
1343
1344 let result = crate::lint(
1345 content,
1346 &filtered,
1347 false,
1348 crate::config::MarkdownFlavor::Standard,
1349 None,
1350 Some(&config),
1351 );
1352
1353 let warnings = result.expect("Linting should succeed");
1354
1355 let md044_warnings: Vec<_> = warnings
1357 .iter()
1358 .filter(|w| w.rule_name.as_deref() == Some("MD044"))
1359 .collect();
1360
1361 assert!(
1362 md044_warnings.len() >= 2,
1363 "Should have MD044 warnings for 'javascript' and 'typescript', got {}",
1364 md044_warnings.len()
1365 );
1366 }
1367
1368 #[test]
1369 fn test_end_to_end_severity_config() {
1370 let content = "test\n"; let mut config = crate::config::Config::default();
1374 let json = serde_json::json!({
1375 "severity": "info"
1376 });
1377 if let Some(rule_config) = json_to_rule_config(&json) {
1378 config.rules.insert("MD041".to_string(), rule_config);
1379 }
1380
1381 config.global.enable = vec!["MD041".to_string()];
1383
1384 let rules = crate::rules::all_rules(&config);
1385 let filtered = crate::rules::filter_rules(&rules, &config.global);
1386
1387 let result = crate::lint(
1388 content,
1389 &filtered,
1390 false,
1391 crate::config::MarkdownFlavor::Standard,
1392 None,
1393 Some(&config),
1394 );
1395
1396 let warnings = result.expect("Linting should succeed");
1397
1398 let md041 = warnings.iter().find(|w| w.rule_name.as_deref() == Some("MD041"));
1400 assert!(md041.is_some(), "Should have MD041 warning");
1401 assert_eq!(
1402 md041.unwrap().severity,
1403 crate::rule::Severity::Info,
1404 "MD041 should have Info severity from config"
1405 );
1406 }
1407}