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 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
55const POLYMORPHIC_SENTINEL: &str = "\0__polymorphic__";
62
63pub fn is_nullable_sentinel(value: &toml::Value) -> bool {
65 matches!(value, toml::Value::String(s) if s == NULLABLE_SENTINEL)
66}
67
68pub fn is_polymorphic_sentinel(value: &toml::Value) -> bool {
70 matches!(value, toml::Value::String(s) if s == POLYMORPHIC_SENTINEL)
71}
72
73pub fn polymorphic_sentinel_value() -> toml::Value {
79 toml::Value::String(POLYMORPHIC_SENTINEL.to_string())
80}
81
82pub fn config_schema_table<T: RuleConfig>(config: &T) -> Option<toml::map::Map<String, toml::Value>> {
88 let json_value = serde_json::to_value(config).ok()?;
89 let obj = json_value.as_object()?;
90 let mut table = toml::map::Map::new();
91 for (k, v) in obj {
92 if v.is_null() {
93 table.insert(k.clone(), toml::Value::String(NULLABLE_SENTINEL.to_string()));
94 } else {
95 let toml_v = json_to_toml_value(v).unwrap_or_else(|| toml::Value::String(NULLABLE_SENTINEL.to_string()));
98 table.insert(k.clone(), toml_v);
99 }
100 }
101 Some(table)
102}
103
104pub fn default_config_section_for<T: RuleConfig>() -> Option<(String, toml::Value)> {
111 let json_value = serde_json::to_value(T::default()).ok()?;
112 let toml_value = json_to_toml_value(&json_value)?;
113 match toml_value {
114 toml::Value::Table(table) if !table.is_empty() => Some((T::RULE_NAME.to_string(), toml::Value::Table(table))),
115 _ => None,
116 }
117}
118
119pub fn nullable_config_section_for<T: RuleConfig>() -> Option<(String, toml::Value)> {
123 let table = config_schema_table(&T::default())?;
124 if table.is_empty() {
125 return None;
126 }
127 Some((T::RULE_NAME.to_string(), toml::Value::Table(table)))
128}
129
130#[macro_export]
138macro_rules! impl_rule_config_methods {
139 ($config_ty:ty) => {
140 fn default_config_section(&self) -> Option<(String, toml::Value)> {
141 $crate::rule_config_serde::default_config_section_for::<$config_ty>()
142 }
143
144 fn from_config(config: &$crate::config::Config) -> Box<dyn $crate::rule::Rule>
145 where
146 Self: Sized,
147 {
148 Box::new(Self::from_config_struct(
149 $crate::rule_config_serde::load_rule_config::<$config_ty>(config),
150 ))
151 }
152 };
153 ($config_ty:ty, nullable) => {
154 fn default_config_section(&self) -> Option<(String, toml::Value)> {
155 $crate::rule_config_serde::nullable_config_section_for::<$config_ty>()
156 }
157
158 fn from_config(config: &$crate::config::Config) -> Box<dyn $crate::rule::Rule>
159 where
160 Self: Sized,
161 {
162 Box::new(Self::from_config_struct(
163 $crate::rule_config_serde::load_rule_config::<$config_ty>(config),
164 ))
165 }
166 };
167}
168
169pub fn json_to_toml_value(json_val: &serde_json::Value) -> Option<toml::Value> {
171 match json_val {
172 serde_json::Value::Null => None,
173 serde_json::Value::Bool(b) => Some(toml::Value::Boolean(*b)),
174 serde_json::Value::Number(n) => {
175 if let Some(i) = n.as_i64() {
176 Some(toml::Value::Integer(i))
177 } else {
178 n.as_f64().map(toml::Value::Float)
179 }
180 }
181 serde_json::Value::String(s) => Some(toml::Value::String(s.clone())),
182 serde_json::Value::Array(arr) => {
183 let toml_arr: Vec<_> = arr.iter().filter_map(json_to_toml_value).collect();
184 Some(toml::Value::Array(toml_arr))
185 }
186 serde_json::Value::Object(obj) => {
187 let mut toml_table = toml::map::Map::new();
188 for (k, v) in obj {
189 if let Some(toml_v) = json_to_toml_value(v) {
190 toml_table.insert(k.clone(), toml_v);
191 }
192 }
193 Some(toml::Value::Table(toml_table))
194 }
195 }
196}
197
198pub fn is_rule_name(name: &str) -> bool {
202 let upper = name.to_ascii_uppercase();
203 upper.starts_with("MD") && upper.len() >= 4 && upper[2..].chars().all(|c| c.is_ascii_digit())
204}
205
206#[derive(Debug, Default)]
208pub struct RuleConfigConversion {
209 pub config: Option<crate::config::RuleConfig>,
211 pub warnings: Vec<String>,
213}
214
215pub fn json_to_rule_config(json_value: &serde_json::Value) -> Option<crate::config::RuleConfig> {
223 json_to_rule_config_with_warnings(json_value).config
224}
225
226pub fn json_to_rule_config_with_warnings(json_value: &serde_json::Value) -> RuleConfigConversion {
231 use std::collections::BTreeMap;
232
233 let mut result = RuleConfigConversion::default();
234
235 let Some(obj) = json_value.as_object() else {
236 result.warnings.push(format!(
237 "Expected object for rule config, got {}",
238 json_type_name(json_value)
239 ));
240 return result;
241 };
242
243 let mut values = BTreeMap::new();
244 let mut severity = None;
245
246 for (key, val) in obj {
247 if key == "severity" {
249 if let Some(s) = val.as_str() {
250 match s.to_lowercase().as_str() {
251 "error" => severity = Some(crate::rule::Severity::Error),
252 "warning" => severity = Some(crate::rule::Severity::Warning),
253 "info" => severity = Some(crate::rule::Severity::Info),
254 _ => {
255 result.warnings.push(format!(
256 "Invalid severity '{s}', expected 'error', 'warning', or 'info'"
257 ));
258 }
259 }
260 } else {
261 result
262 .warnings
263 .push(format!("Severity must be a string, got {}", json_type_name(val)));
264 }
265 continue;
266 }
267
268 if let Some(toml_val) = json_to_toml_value(val) {
270 values.insert(key.clone(), toml_val);
271 } else if !val.is_null() {
272 result
273 .warnings
274 .push(format!("Could not convert '{key}' value to config format"));
275 }
276 }
277
278 result.config = Some(crate::config::RuleConfig { severity, values });
279 result
280}
281
282fn json_type_name(val: &serde_json::Value) -> &'static str {
284 match val {
285 serde_json::Value::Null => "null",
286 serde_json::Value::Bool(_) => "boolean",
287 serde_json::Value::Number(_) => "number",
288 serde_json::Value::String(_) => "string",
289 serde_json::Value::Array(_) => "array",
290 serde_json::Value::Object(_) => "object",
291 }
292}
293
294pub fn toml_value_to_json(toml_val: &toml::Value) -> Option<serde_json::Value> {
296 match toml_val {
297 toml::Value::String(s) => Some(serde_json::Value::String(s.clone())),
298 toml::Value::Integer(i) => Some(serde_json::json!(i)),
299 toml::Value::Float(f) => Some(serde_json::json!(f)),
300 toml::Value::Boolean(b) => Some(serde_json::Value::Bool(*b)),
301 toml::Value::Array(arr) => {
302 let json_arr: Vec<_> = arr.iter().filter_map(toml_value_to_json).collect();
303 Some(serde_json::Value::Array(json_arr))
304 }
305 toml::Value::Table(table) => {
306 let mut json_obj = serde_json::Map::new();
307 for (k, v) in table {
308 if let Some(json_v) = toml_value_to_json(v) {
309 json_obj.insert(k.clone(), json_v);
310 }
311 }
312 Some(serde_json::Value::Object(json_obj))
313 }
314 toml::Value::Datetime(_) => None, }
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321 use serde::{Deserialize, Serialize};
322 use std::collections::BTreeMap;
323
324 #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
326 #[serde(default)]
327 struct TestRuleConfig {
328 #[serde(default)]
329 enabled: bool,
330 #[serde(default)]
331 indent: i64,
332 #[serde(default)]
333 style: String,
334 #[serde(default)]
335 items: Vec<String>,
336 }
337
338 impl RuleConfig for TestRuleConfig {
339 const RULE_NAME: &'static str = "TEST001";
340 }
341
342 #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
346 #[serde(default)]
347 struct NullableTestConfig {
348 #[serde(default)]
349 enabled: bool,
350 #[serde(default, alias = "key-order")]
351 key_order: Option<Vec<String>>,
352 #[serde(default, alias = "title-pattern")]
353 title_pattern: Option<String>,
354 }
355
356 impl RuleConfig for NullableTestConfig {
357 const RULE_NAME: &'static str = "TEST_NULLABLE";
358 }
359
360 #[test]
361 fn test_is_nullable_sentinel() {
362 let sentinel = toml::Value::String(NULLABLE_SENTINEL.to_string());
363 assert!(is_nullable_sentinel(&sentinel));
364
365 let regular = toml::Value::String("normal".to_string());
366 assert!(!is_nullable_sentinel(®ular));
367
368 let integer = toml::Value::Integer(42);
369 assert!(!is_nullable_sentinel(&integer));
370 }
371
372 #[test]
373 fn test_is_polymorphic_sentinel() {
374 let sentinel = polymorphic_sentinel_value();
375 assert!(is_polymorphic_sentinel(&sentinel));
376
377 let nullable = toml::Value::String(NULLABLE_SENTINEL.to_string());
379 assert!(!is_polymorphic_sentinel(&nullable));
380 assert!(!is_nullable_sentinel(&sentinel));
381
382 let regular = toml::Value::String("normal".to_string());
383 assert!(!is_polymorphic_sentinel(®ular));
384 }
385
386 #[test]
387 fn test_config_schema_table_preserves_nullable_keys() {
388 let config = NullableTestConfig::default();
389 let table = config_schema_table(&config).unwrap();
390
391 assert!(table.contains_key("enabled"), "enabled key missing");
393 assert!(table.contains_key("key_order"), "key_order key missing");
394 assert!(table.contains_key("title_pattern"), "title_pattern key missing");
395
396 assert!(is_nullable_sentinel(table.get("key_order").unwrap()));
398 assert!(is_nullable_sentinel(table.get("title_pattern").unwrap()));
399
400 assert_eq!(table.get("enabled"), Some(&toml::Value::Boolean(false)));
402 }
403
404 #[test]
405 fn test_config_schema_table_non_null_option_uses_real_value() {
406 let config = NullableTestConfig {
407 enabled: true,
408 key_order: Some(vec!["title".to_string(), "date".to_string()]),
409 title_pattern: Some("pattern".to_string()),
410 };
411 let table = config_schema_table(&config).unwrap();
412
413 let key_order = table.get("key_order").unwrap();
415 assert!(!is_nullable_sentinel(key_order));
416 assert!(matches!(key_order, toml::Value::Array(_)));
417
418 let title_pattern = table.get("title_pattern").unwrap();
419 assert!(!is_nullable_sentinel(title_pattern));
420 assert_eq!(title_pattern, &toml::Value::String("pattern".to_string()));
421 }
422
423 #[test]
424 fn test_json_to_toml_value_still_drops_null() {
425 assert!(json_to_toml_value(&serde_json::Value::Null).is_none());
427 }
428
429 #[test]
430 fn test_config_schema_table_all_keys_present() {
431 let config = NullableTestConfig::default();
432 let table = config_schema_table(&config).unwrap();
433 assert_eq!(table.len(), 3, "Expected 3 keys: enabled, key_order, title_pattern");
434 }
435
436 #[test]
437 fn test_config_schema_table_never_drops_keys() {
438 let mut obj = serde_json::Map::new();
442 obj.insert("real_key".to_string(), serde_json::json!(42));
443 obj.insert("null_key".to_string(), serde_json::Value::Null);
444 let json = serde_json::Value::Object(obj);
445
446 let obj = json.as_object().unwrap();
448 let mut table = toml::map::Map::new();
449 for (k, v) in obj {
450 if v.is_null() {
451 table.insert(k.clone(), toml::Value::String(NULLABLE_SENTINEL.to_string()));
452 } else {
453 let toml_v =
454 json_to_toml_value(v).unwrap_or_else(|| toml::Value::String(NULLABLE_SENTINEL.to_string()));
455 table.insert(k.clone(), toml_v);
456 }
457 }
458
459 assert_eq!(table.len(), 2, "Both keys must be present");
460 assert!(table.contains_key("real_key"));
461 assert!(table.contains_key("null_key"));
462 }
463
464 #[test]
465 fn test_toml_value_to_json_basic_types() {
466 let toml_str = toml::Value::String("hello".to_string());
468 let json_str = toml_value_to_json(&toml_str).unwrap();
469 assert_eq!(json_str, serde_json::Value::String("hello".to_string()));
470
471 let toml_int = toml::Value::Integer(42);
473 let json_int = toml_value_to_json(&toml_int).unwrap();
474 assert_eq!(json_int, serde_json::json!(42));
475
476 let toml_float = toml::Value::Float(1.234);
478 let json_float = toml_value_to_json(&toml_float).unwrap();
479 assert_eq!(json_float, serde_json::json!(1.234));
480
481 let toml_bool = toml::Value::Boolean(true);
483 let json_bool = toml_value_to_json(&toml_bool).unwrap();
484 assert_eq!(json_bool, serde_json::Value::Bool(true));
485 }
486
487 #[test]
488 fn test_toml_value_to_json_complex_types() {
489 let toml_arr = toml::Value::Array(vec![
491 toml::Value::String("a".to_string()),
492 toml::Value::String("b".to_string()),
493 ]);
494 let json_arr = toml_value_to_json(&toml_arr).unwrap();
495 assert_eq!(json_arr, serde_json::json!(["a", "b"]));
496
497 let mut toml_table = toml::map::Map::new();
499 toml_table.insert("key1".to_string(), toml::Value::String("value1".to_string()));
500 toml_table.insert("key2".to_string(), toml::Value::Integer(123));
501 let toml_tbl = toml::Value::Table(toml_table);
502 let json_tbl = toml_value_to_json(&toml_tbl).unwrap();
503
504 let expected = serde_json::json!({
505 "key1": "value1",
506 "key2": 123
507 });
508 assert_eq!(json_tbl, expected);
509 }
510
511 #[test]
512 fn test_toml_value_to_json_datetime() {
513 let toml_dt = toml::Value::Datetime("2023-01-01T00:00:00Z".parse().unwrap());
515 assert!(toml_value_to_json(&toml_dt).is_none());
516 }
517
518 #[test]
519 fn test_json_to_toml_value_basic_types() {
520 assert!(json_to_toml_value(&serde_json::Value::Null).is_none());
522
523 let json_bool = serde_json::Value::Bool(false);
525 let toml_bool = json_to_toml_value(&json_bool).unwrap();
526 assert_eq!(toml_bool, toml::Value::Boolean(false));
527
528 let json_int = serde_json::json!(42);
530 let toml_int = json_to_toml_value(&json_int).unwrap();
531 assert_eq!(toml_int, toml::Value::Integer(42));
532
533 let json_float = serde_json::json!(1.234);
535 let toml_float = json_to_toml_value(&json_float).unwrap();
536 assert_eq!(toml_float, toml::Value::Float(1.234));
537
538 let json_str = serde_json::Value::String("test".to_string());
540 let toml_str = json_to_toml_value(&json_str).unwrap();
541 assert_eq!(toml_str, toml::Value::String("test".to_string()));
542 }
543
544 #[test]
545 fn test_json_to_toml_value_complex_types() {
546 let json_arr = serde_json::json!(["x", "y", "z"]);
548 let toml_arr = json_to_toml_value(&json_arr).unwrap();
549 if let toml::Value::Array(arr) = toml_arr {
550 assert_eq!(arr.len(), 3);
551 assert_eq!(arr[0], toml::Value::String("x".to_string()));
552 assert_eq!(arr[1], toml::Value::String("y".to_string()));
553 assert_eq!(arr[2], toml::Value::String("z".to_string()));
554 } else {
555 panic!("Expected array");
556 }
557
558 let json_obj = serde_json::json!({
560 "name": "test",
561 "count": 10,
562 "active": true
563 });
564 let toml_obj = json_to_toml_value(&json_obj).unwrap();
565 if let toml::Value::Table(table) = toml_obj {
566 assert_eq!(table.get("name"), Some(&toml::Value::String("test".to_string())));
567 assert_eq!(table.get("count"), Some(&toml::Value::Integer(10)));
568 assert_eq!(table.get("active"), Some(&toml::Value::Boolean(true)));
569 } else {
570 panic!("Expected table");
571 }
572 }
573
574 #[test]
575 fn test_load_rule_config_default() {
576 let config = crate::config::Config::default();
578
579 let rule_config: TestRuleConfig = load_rule_config(&config);
581 assert_eq!(rule_config, TestRuleConfig::default());
582 }
583
584 #[test]
585 fn test_load_rule_config_with_values() {
586 let mut config = crate::config::Config::default();
588 let mut rule_values = BTreeMap::new();
589 rule_values.insert("enabled".to_string(), toml::Value::Boolean(true));
590 rule_values.insert("indent".to_string(), toml::Value::Integer(4));
591 rule_values.insert("style".to_string(), toml::Value::String("consistent".to_string()));
592 rule_values.insert(
593 "items".to_string(),
594 toml::Value::Array(vec![
595 toml::Value::String("item1".to_string()),
596 toml::Value::String("item2".to_string()),
597 ]),
598 );
599
600 config.rules.insert(
601 "TEST001".to_string(),
602 crate::config::RuleConfig {
603 severity: None,
604 values: rule_values,
605 },
606 );
607
608 let rule_config: TestRuleConfig = load_rule_config(&config);
610 assert!(rule_config.enabled);
611 assert_eq!(rule_config.indent, 4);
612 assert_eq!(rule_config.style, "consistent");
613 assert_eq!(rule_config.items, vec!["item1", "item2"]);
614 }
615
616 #[test]
617 fn test_load_rule_config_partial() {
618 let mut config = crate::config::Config::default();
620 let mut rule_values = BTreeMap::new();
621 rule_values.insert("enabled".to_string(), toml::Value::Boolean(true));
622 rule_values.insert("style".to_string(), toml::Value::String("custom".to_string()));
623
624 config.rules.insert(
625 "TEST001".to_string(),
626 crate::config::RuleConfig {
627 severity: None,
628 values: rule_values,
629 },
630 );
631
632 let rule_config: TestRuleConfig = load_rule_config(&config);
634 assert!(rule_config.enabled); assert_eq!(rule_config.indent, 0); assert_eq!(rule_config.style, "custom"); assert_eq!(rule_config.items, Vec::<String>::new()); }
639
640 #[test]
641 fn test_conversion_roundtrip() {
642 let original = toml::Value::Table({
644 let mut table = toml::map::Map::new();
645 table.insert("string".to_string(), toml::Value::String("test".to_string()));
646 table.insert("number".to_string(), toml::Value::Integer(42));
647 table.insert("bool".to_string(), toml::Value::Boolean(true));
648 table.insert(
649 "array".to_string(),
650 toml::Value::Array(vec![
651 toml::Value::String("a".to_string()),
652 toml::Value::String("b".to_string()),
653 ]),
654 );
655 table
656 });
657
658 let json = toml_value_to_json(&original).unwrap();
659 let back_to_toml = json_to_toml_value(&json).unwrap();
660
661 assert_eq!(original, back_to_toml);
662 }
663
664 #[test]
665 fn test_edge_cases() {
666 let empty_arr = toml::Value::Array(vec![]);
668 let json_arr = toml_value_to_json(&empty_arr).unwrap();
669 assert_eq!(json_arr, serde_json::json!([]));
670
671 let empty_table = toml::Value::Table(toml::map::Map::new());
673 let json_table = toml_value_to_json(&empty_table).unwrap();
674 assert_eq!(json_table, serde_json::json!({}));
675
676 let nested = toml::Value::Table({
678 let mut outer = toml::map::Map::new();
679 outer.insert(
680 "inner".to_string(),
681 toml::Value::Table({
682 let mut inner = toml::map::Map::new();
683 inner.insert("value".to_string(), toml::Value::Integer(123));
684 inner
685 }),
686 );
687 outer
688 });
689 let json_nested = toml_value_to_json(&nested).unwrap();
690 assert_eq!(
691 json_nested,
692 serde_json::json!({
693 "inner": {
694 "value": 123
695 }
696 })
697 );
698 }
699
700 #[test]
701 fn test_float_edge_cases() {
702 let nan = serde_json::Number::from_f64(f64::NAN);
704 assert!(nan.is_none());
705
706 let inf = serde_json::Number::from_f64(f64::INFINITY);
707 assert!(inf.is_none());
708
709 let valid_float = toml::Value::Float(1.23);
711 let json_float = toml_value_to_json(&valid_float).unwrap();
712 assert_eq!(json_float, serde_json::json!(1.23));
713 }
714
715 #[test]
716 fn test_invalid_config_returns_default() {
717 let mut config = crate::config::Config::default();
719 let mut rule_values = BTreeMap::new();
720 rule_values.insert("unknown_field".to_string(), toml::Value::Boolean(true));
721 rule_values.insert("items".to_string(), toml::Value::Table(toml::map::Map::new()));
723
724 config.rules.insert(
725 "TEST001".to_string(),
726 crate::config::RuleConfig {
727 severity: None,
728 values: rule_values,
729 },
730 );
731
732 let rule_config: TestRuleConfig = load_rule_config(&config);
734 assert_eq!(rule_config, TestRuleConfig::default());
736 }
737
738 #[test]
739 fn test_invalid_field_type() {
740 let mut config = crate::config::Config::default();
742 let mut rule_values = BTreeMap::new();
743 rule_values.insert("indent".to_string(), toml::Value::String("not_a_number".to_string()));
745
746 config.rules.insert(
747 "TEST001".to_string(),
748 crate::config::RuleConfig {
749 severity: None,
750 values: rule_values,
751 },
752 );
753
754 let rule_config: TestRuleConfig = load_rule_config(&config);
756 assert_eq!(rule_config, TestRuleConfig::default());
757 }
758
759 #[test]
762 fn test_is_rule_name_valid() {
763 assert!(is_rule_name("MD001"));
765 assert!(is_rule_name("MD060"));
766 assert!(is_rule_name("MD123"));
767 assert!(is_rule_name("MD999"));
768
769 assert!(is_rule_name("md001"));
771 assert!(is_rule_name("Md060"));
772 assert!(is_rule_name("mD123"));
773
774 assert!(is_rule_name("MD0001"));
776 assert!(is_rule_name("MD12345"));
777 }
778
779 #[test]
780 fn test_is_rule_name_invalid() {
781 assert!(!is_rule_name("MD"));
783 assert!(!is_rule_name("MD1"));
784 assert!(!is_rule_name("M"));
785 assert!(!is_rule_name(""));
786
787 assert!(!is_rule_name("disable"));
789 assert!(!is_rule_name("enable"));
790 assert!(!is_rule_name("flavor"));
791 assert!(!is_rule_name("line-length"));
792 assert!(!is_rule_name("global"));
793
794 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")); }
801
802 #[test]
805 fn test_json_to_rule_config_simple() {
806 let json = serde_json::json!({
807 "enabled": true,
808 "style": "aligned"
809 });
810
811 let rule_config = json_to_rule_config(&json).unwrap();
812
813 assert_eq!(rule_config.values.get("enabled"), Some(&toml::Value::Boolean(true)));
814 assert_eq!(
815 rule_config.values.get("style"),
816 Some(&toml::Value::String("aligned".to_string()))
817 );
818 assert!(rule_config.severity.is_none());
819 }
820
821 #[test]
822 fn test_json_to_rule_config_with_numbers() {
823 let json = serde_json::json!({
824 "line-length": 120,
825 "max-width": 0,
826 "indent": 4
827 });
828
829 let rule_config = json_to_rule_config(&json).unwrap();
830
831 assert_eq!(rule_config.values.get("line-length"), Some(&toml::Value::Integer(120)));
832 assert_eq!(rule_config.values.get("max-width"), Some(&toml::Value::Integer(0)));
833 assert_eq!(rule_config.values.get("indent"), Some(&toml::Value::Integer(4)));
834 }
835
836 #[test]
837 fn test_json_to_rule_config_with_arrays() {
838 let json = serde_json::json!({
839 "names": ["JavaScript", "TypeScript", "React"],
840 "exclude-patterns": ["*.test.md", "draft-*"]
841 });
842
843 let rule_config = json_to_rule_config(&json).unwrap();
844
845 let expected_names = toml::Value::Array(vec![
846 toml::Value::String("JavaScript".to_string()),
847 toml::Value::String("TypeScript".to_string()),
848 toml::Value::String("React".to_string()),
849 ]);
850 assert_eq!(rule_config.values.get("names"), Some(&expected_names));
851
852 let expected_patterns = toml::Value::Array(vec![
853 toml::Value::String("*.test.md".to_string()),
854 toml::Value::String("draft-*".to_string()),
855 ]);
856 assert_eq!(rule_config.values.get("exclude-patterns"), Some(&expected_patterns));
857 }
858
859 #[test]
860 fn test_json_to_rule_config_with_severity() {
861 let json = serde_json::json!({
863 "severity": "error",
864 "style": "aligned"
865 });
866 let rule_config = json_to_rule_config(&json).unwrap();
867 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Error));
868 assert!(!rule_config.values.contains_key("severity")); let json = serde_json::json!({
872 "severity": "warning",
873 "enabled": true
874 });
875 let rule_config = json_to_rule_config(&json).unwrap();
876 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Warning));
877
878 let json = serde_json::json!({
880 "severity": "info"
881 });
882 let rule_config = json_to_rule_config(&json).unwrap();
883 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Info));
884
885 let json = serde_json::json!({
887 "severity": "ERROR"
888 });
889 let rule_config = json_to_rule_config(&json).unwrap();
890 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Error));
891 }
892
893 #[test]
894 fn test_json_to_rule_config_invalid_severity() {
895 let json = serde_json::json!({
897 "severity": "critical",
898 "style": "aligned"
899 });
900 let rule_config = json_to_rule_config(&json).unwrap();
901 assert!(rule_config.severity.is_none()); assert_eq!(
903 rule_config.values.get("style"),
904 Some(&toml::Value::String("aligned".to_string()))
905 );
906
907 let json = serde_json::json!({
909 "severity": 1,
910 "enabled": true
911 });
912 let rule_config = json_to_rule_config(&json).unwrap();
913 assert!(rule_config.severity.is_none()); }
915
916 #[test]
917 fn test_json_to_rule_config_non_object() {
918 assert!(json_to_rule_config(&serde_json::json!(42)).is_none());
920 assert!(json_to_rule_config(&serde_json::json!("string")).is_none());
921 assert!(json_to_rule_config(&serde_json::json!(true)).is_none());
922 assert!(json_to_rule_config(&serde_json::json!([1, 2, 3])).is_none());
923 assert!(json_to_rule_config(&serde_json::Value::Null).is_none());
924 }
925
926 #[test]
927 fn test_json_to_rule_config_empty_object() {
928 let json = serde_json::json!({});
929 let rule_config = json_to_rule_config(&json).unwrap();
930 assert!(rule_config.values.is_empty());
931 assert!(rule_config.severity.is_none());
932 }
933
934 #[test]
935 fn test_json_to_rule_config_nested_objects() {
936 let json = serde_json::json!({
938 "options": {
939 "nested-key": "nested-value",
940 "nested-number": 42
941 }
942 });
943
944 let rule_config = json_to_rule_config(&json).unwrap();
945
946 let options = rule_config.values.get("options").unwrap();
947 if let toml::Value::Table(table) = options {
948 assert_eq!(
949 table.get("nested-key"),
950 Some(&toml::Value::String("nested-value".to_string()))
951 );
952 assert_eq!(table.get("nested-number"), Some(&toml::Value::Integer(42)));
953 } else {
954 panic!("options should be a table");
955 }
956 }
957
958 #[test]
959 fn test_json_to_rule_config_md060_example() {
960 let json = serde_json::json!({
962 "enabled": true,
963 "style": "aligned",
964 "max-width": 120,
965 "column-align": "auto",
966 "loose-last-column": false
967 });
968
969 let rule_config = json_to_rule_config(&json).unwrap();
970
971 assert_eq!(rule_config.values.get("enabled"), Some(&toml::Value::Boolean(true)));
972 assert_eq!(
973 rule_config.values.get("style"),
974 Some(&toml::Value::String("aligned".to_string()))
975 );
976 assert_eq!(rule_config.values.get("max-width"), Some(&toml::Value::Integer(120)));
977 assert_eq!(
978 rule_config.values.get("column-align"),
979 Some(&toml::Value::String("auto".to_string()))
980 );
981 assert_eq!(
982 rule_config.values.get("loose-last-column"),
983 Some(&toml::Value::Boolean(false))
984 );
985 }
986
987 #[test]
988 fn test_json_to_rule_config_md044_example() {
989 let json = serde_json::json!({
991 "names": ["JavaScript", "TypeScript", "GitHub", "macOS"],
992 "code-blocks": false,
993 "html-elements": false
994 });
995
996 let rule_config = json_to_rule_config(&json).unwrap();
997
998 let expected_names = toml::Value::Array(vec![
999 toml::Value::String("JavaScript".to_string()),
1000 toml::Value::String("TypeScript".to_string()),
1001 toml::Value::String("GitHub".to_string()),
1002 toml::Value::String("macOS".to_string()),
1003 ]);
1004 assert_eq!(rule_config.values.get("names"), Some(&expected_names));
1005 assert_eq!(
1006 rule_config.values.get("code-blocks"),
1007 Some(&toml::Value::Boolean(false))
1008 );
1009 assert_eq!(
1010 rule_config.values.get("html-elements"),
1011 Some(&toml::Value::Boolean(false))
1012 );
1013 }
1014
1015 #[test]
1018 fn test_json_to_rule_config_with_warnings_valid() {
1019 let json = serde_json::json!({
1020 "severity": "error",
1021 "enabled": true
1022 });
1023
1024 let result = json_to_rule_config_with_warnings(&json);
1025
1026 assert!(result.config.is_some());
1027 assert!(
1028 result.warnings.is_empty(),
1029 "Expected no warnings, got: {:?}",
1030 result.warnings
1031 );
1032 assert_eq!(result.config.unwrap().severity, Some(crate::rule::Severity::Error));
1033 }
1034
1035 #[test]
1036 fn test_json_to_rule_config_with_warnings_invalid_severity() {
1037 let json = serde_json::json!({
1038 "severity": "critical",
1039 "style": "aligned"
1040 });
1041
1042 let result = json_to_rule_config_with_warnings(&json);
1043
1044 assert!(result.config.is_some());
1045 assert_eq!(result.warnings.len(), 1);
1046 assert!(result.warnings[0].contains("Invalid severity 'critical'"));
1047 assert!(result.config.unwrap().severity.is_none());
1049 }
1050
1051 #[test]
1052 fn test_json_to_rule_config_with_warnings_wrong_severity_type() {
1053 let json = serde_json::json!({
1054 "severity": 123,
1055 "enabled": true
1056 });
1057
1058 let result = json_to_rule_config_with_warnings(&json);
1059
1060 assert!(result.config.is_some());
1061 assert_eq!(result.warnings.len(), 1);
1062 assert!(result.warnings[0].contains("Severity must be a string"));
1063 }
1064
1065 #[test]
1066 fn test_json_to_rule_config_with_warnings_non_object() {
1067 let json = serde_json::json!("not an object");
1068
1069 let result = json_to_rule_config_with_warnings(&json);
1070
1071 assert!(result.config.is_none());
1072 assert_eq!(result.warnings.len(), 1);
1073 assert!(result.warnings[0].contains("Expected object"));
1074 }
1075
1076 #[test]
1079 fn test_rule_config_integration_with_config() {
1080 let mut config = crate::config::Config::default();
1082
1083 let md060_json = serde_json::json!({
1085 "enabled": true,
1086 "style": "aligned",
1087 "max-width": 120
1088 });
1089 let md013_json = serde_json::json!({
1090 "line-length": 100,
1091 "code-blocks": false
1092 });
1093
1094 if let Some(md060_config) = json_to_rule_config(&md060_json) {
1095 config.rules.insert("MD060".to_string(), md060_config);
1096 }
1097 if let Some(md013_config) = json_to_rule_config(&md013_json) {
1098 config.rules.insert("MD013".to_string(), md013_config);
1099 }
1100
1101 assert!(config.rules.contains_key("MD060"));
1103 assert!(config.rules.contains_key("MD013"));
1104
1105 let md060 = config.rules.get("MD060").unwrap();
1107 assert_eq!(md060.values.get("enabled"), Some(&toml::Value::Boolean(true)));
1108 assert_eq!(
1109 md060.values.get("style"),
1110 Some(&toml::Value::String("aligned".to_string()))
1111 );
1112 assert_eq!(md060.values.get("max-width"), Some(&toml::Value::Integer(120)));
1113 }
1114
1115 #[test]
1116 fn test_rule_config_integration_with_severity() {
1117 let mut config = crate::config::Config::default();
1118
1119 let json = serde_json::json!({
1120 "severity": "error",
1121 "enabled": true
1122 });
1123
1124 if let Some(rule_config) = json_to_rule_config(&json) {
1125 config.rules.insert("MD041".to_string(), rule_config);
1126 }
1127
1128 let md041 = config.rules.get("MD041").unwrap();
1129 assert_eq!(md041.severity, Some(crate::rule::Severity::Error));
1130 }
1131
1132 #[test]
1133 fn test_rule_config_integration_case_normalization() {
1134 let mut config = crate::config::Config::default();
1136
1137 let json = serde_json::json!({ "enabled": true });
1138
1139 for rule_name in ["md060", "MD060", "Md060"] {
1141 if is_rule_name(rule_name)
1142 && let Some(rule_config) = json_to_rule_config(&json)
1143 {
1144 config.rules.insert(rule_name.to_ascii_uppercase(), rule_config);
1145 }
1146 }
1147
1148 assert!(config.rules.contains_key("MD060"));
1150 assert_eq!(config.rules.len(), 1); }
1152
1153 #[test]
1154 fn test_rule_config_integration_filters_non_rules() {
1155 let keys = ["MD060", "disable", "enable", "flavor", "line-length", "global"];
1157
1158 let rule_keys: Vec<_> = keys.iter().filter(|k| is_rule_name(k)).collect();
1159
1160 assert_eq!(rule_keys, vec![&"MD060"]);
1161 }
1162
1163 #[test]
1164 fn test_multiple_rule_configs_with_mixed_validity() {
1165 let rules = vec![
1167 ("MD060", serde_json::json!({ "severity": "error", "style": "aligned" })),
1168 (
1169 "MD013",
1170 serde_json::json!({ "severity": "invalid", "line-length": 100 }),
1171 ),
1172 ("MD041", serde_json::json!({ "enabled": true })),
1173 ];
1174
1175 let mut config = crate::config::Config::default();
1176 let mut all_warnings = Vec::new();
1177
1178 for (name, json) in rules {
1179 let result = json_to_rule_config_with_warnings(&json);
1180 all_warnings.extend(result.warnings);
1181 if let Some(rule_config) = result.config {
1182 config.rules.insert(name.to_string(), rule_config);
1183 }
1184 }
1185
1186 assert_eq!(config.rules.len(), 3);
1188
1189 assert_eq!(all_warnings.len(), 1);
1191 assert!(all_warnings[0].contains("Invalid severity"));
1192
1193 assert_eq!(
1195 config.rules.get("MD060").unwrap().severity,
1196 Some(crate::rule::Severity::Error)
1197 );
1198 assert!(config.rules.get("MD013").unwrap().severity.is_none());
1199 }
1200
1201 #[test]
1205 fn test_end_to_end_md013_line_length_config() {
1206 let content = "# Test\n\nThis is a line that is exactly 50 characters long.\n";
1208
1209 let mut config = crate::config::Config::default();
1211 let json = serde_json::json!({
1212 "line-length": 40
1213 });
1214 if let Some(rule_config) = json_to_rule_config(&json) {
1215 config.rules.insert("MD013".to_string(), rule_config);
1216 }
1217
1218 config.global.enable = vec!["MD013".to_string()];
1220
1221 let rules = crate::rules::all_rules(&config);
1222 let filtered = crate::rules::filter_rules(&rules, &config.global);
1223
1224 let result = crate::lint(
1225 content,
1226 &filtered,
1227 false,
1228 crate::config::MarkdownFlavor::Standard,
1229 None,
1230 Some(&config),
1231 );
1232
1233 let warnings = result.expect("Linting should succeed");
1234
1235 let has_md013 = warnings.iter().any(|w| w.rule_name.as_deref() == Some("MD013"));
1237 assert!(has_md013, "Should have MD013 warning with line-length=40");
1238 }
1239
1240 #[test]
1241 fn test_end_to_end_md013_line_length_no_warning() {
1242 let content = "# Test\n\nThis is a line that is exactly 50 characters long.\n";
1244
1245 let mut config = crate::config::Config::default();
1247 let json = serde_json::json!({
1248 "line-length": 100
1249 });
1250 if let Some(rule_config) = json_to_rule_config(&json) {
1251 config.rules.insert("MD013".to_string(), rule_config);
1252 }
1253
1254 config.global.enable = vec!["MD013".to_string()];
1256
1257 let rules = crate::rules::all_rules(&config);
1258 let filtered = crate::rules::filter_rules(&rules, &config.global);
1259
1260 let result = crate::lint(
1261 content,
1262 &filtered,
1263 false,
1264 crate::config::MarkdownFlavor::Standard,
1265 None,
1266 Some(&config),
1267 );
1268
1269 let warnings = result.expect("Linting should succeed");
1270
1271 let has_md013 = warnings.iter().any(|w| w.rule_name.as_deref() == Some("MD013"));
1273 assert!(!has_md013, "Should NOT have MD013 warning with line-length=100");
1274 }
1275
1276 #[test]
1277 fn test_end_to_end_md044_proper_names() {
1278 let content = "# Test\n\nWe use javascript and typescript.\n";
1280
1281 let mut config = crate::config::Config::default();
1283 let json = serde_json::json!({
1284 "names": ["JavaScript", "TypeScript"],
1285 "code-blocks": false
1286 });
1287 if let Some(rule_config) = json_to_rule_config(&json) {
1288 config.rules.insert("MD044".to_string(), rule_config);
1289 }
1290
1291 config.global.enable = vec!["MD044".to_string()];
1293
1294 let rules = crate::rules::all_rules(&config);
1295 let filtered = crate::rules::filter_rules(&rules, &config.global);
1296
1297 let result = crate::lint(
1298 content,
1299 &filtered,
1300 false,
1301 crate::config::MarkdownFlavor::Standard,
1302 None,
1303 Some(&config),
1304 );
1305
1306 let warnings = result.expect("Linting should succeed");
1307
1308 let md044_warnings: Vec<_> = warnings
1310 .iter()
1311 .filter(|w| w.rule_name.as_deref() == Some("MD044"))
1312 .collect();
1313
1314 assert!(
1315 md044_warnings.len() >= 2,
1316 "Should have MD044 warnings for 'javascript' and 'typescript', got {}",
1317 md044_warnings.len()
1318 );
1319 }
1320
1321 #[test]
1322 fn test_end_to_end_severity_config() {
1323 let content = "test\n"; let mut config = crate::config::Config::default();
1327 let json = serde_json::json!({
1328 "severity": "info"
1329 });
1330 if let Some(rule_config) = json_to_rule_config(&json) {
1331 config.rules.insert("MD041".to_string(), rule_config);
1332 }
1333
1334 config.global.enable = vec!["MD041".to_string()];
1336
1337 let rules = crate::rules::all_rules(&config);
1338 let filtered = crate::rules::filter_rules(&rules, &config.global);
1339
1340 let result = crate::lint(
1341 content,
1342 &filtered,
1343 false,
1344 crate::config::MarkdownFlavor::Standard,
1345 None,
1346 Some(&config),
1347 );
1348
1349 let warnings = result.expect("Linting should succeed");
1350
1351 let md041 = warnings.iter().find(|w| w.rule_name.as_deref() == Some("MD041"));
1353 assert!(md041.is_some(), "Should have MD041 warning");
1354 assert_eq!(
1355 md041.unwrap().severity,
1356 crate::rule::Severity::Info,
1357 "MD041 should have Info severity from config"
1358 );
1359 }
1360}