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 json_to_toml_value(json_val: &serde_json::Value) -> Option<toml::Value> {
106 match json_val {
107 serde_json::Value::Null => None,
108 serde_json::Value::Bool(b) => Some(toml::Value::Boolean(*b)),
109 serde_json::Value::Number(n) => {
110 if let Some(i) = n.as_i64() {
111 Some(toml::Value::Integer(i))
112 } else {
113 n.as_f64().map(toml::Value::Float)
114 }
115 }
116 serde_json::Value::String(s) => Some(toml::Value::String(s.clone())),
117 serde_json::Value::Array(arr) => {
118 let toml_arr: Vec<_> = arr.iter().filter_map(json_to_toml_value).collect();
119 Some(toml::Value::Array(toml_arr))
120 }
121 serde_json::Value::Object(obj) => {
122 let mut toml_table = toml::map::Map::new();
123 for (k, v) in obj {
124 if let Some(toml_v) = json_to_toml_value(v) {
125 toml_table.insert(k.clone(), toml_v);
126 }
127 }
128 Some(toml::Value::Table(toml_table))
129 }
130 }
131}
132
133pub fn is_rule_name(name: &str) -> bool {
137 let upper = name.to_ascii_uppercase();
138 upper.starts_with("MD") && upper.len() >= 4 && upper[2..].chars().all(|c| c.is_ascii_digit())
139}
140
141#[derive(Debug, Default)]
143pub struct RuleConfigConversion {
144 pub config: Option<crate::config::RuleConfig>,
146 pub warnings: Vec<String>,
148}
149
150pub fn json_to_rule_config(json_value: &serde_json::Value) -> Option<crate::config::RuleConfig> {
158 json_to_rule_config_with_warnings(json_value).config
159}
160
161pub fn json_to_rule_config_with_warnings(json_value: &serde_json::Value) -> RuleConfigConversion {
166 use std::collections::BTreeMap;
167
168 let mut result = RuleConfigConversion::default();
169
170 let Some(obj) = json_value.as_object() else {
171 result.warnings.push(format!(
172 "Expected object for rule config, got {}",
173 json_type_name(json_value)
174 ));
175 return result;
176 };
177
178 let mut values = BTreeMap::new();
179 let mut severity = None;
180
181 for (key, val) in obj {
182 if key == "severity" {
184 if let Some(s) = val.as_str() {
185 match s.to_lowercase().as_str() {
186 "error" => severity = Some(crate::rule::Severity::Error),
187 "warning" => severity = Some(crate::rule::Severity::Warning),
188 "info" => severity = Some(crate::rule::Severity::Info),
189 _ => {
190 result.warnings.push(format!(
191 "Invalid severity '{s}', expected 'error', 'warning', or 'info'"
192 ));
193 }
194 }
195 } else {
196 result
197 .warnings
198 .push(format!("Severity must be a string, got {}", json_type_name(val)));
199 }
200 continue;
201 }
202
203 if let Some(toml_val) = json_to_toml_value(val) {
205 values.insert(key.clone(), toml_val);
206 } else if !val.is_null() {
207 result
208 .warnings
209 .push(format!("Could not convert '{key}' value to config format"));
210 }
211 }
212
213 result.config = Some(crate::config::RuleConfig { severity, values });
214 result
215}
216
217fn json_type_name(val: &serde_json::Value) -> &'static str {
219 match val {
220 serde_json::Value::Null => "null",
221 serde_json::Value::Bool(_) => "boolean",
222 serde_json::Value::Number(_) => "number",
223 serde_json::Value::String(_) => "string",
224 serde_json::Value::Array(_) => "array",
225 serde_json::Value::Object(_) => "object",
226 }
227}
228
229pub fn toml_value_to_json(toml_val: &toml::Value) -> Option<serde_json::Value> {
231 match toml_val {
232 toml::Value::String(s) => Some(serde_json::Value::String(s.clone())),
233 toml::Value::Integer(i) => Some(serde_json::json!(i)),
234 toml::Value::Float(f) => Some(serde_json::json!(f)),
235 toml::Value::Boolean(b) => Some(serde_json::Value::Bool(*b)),
236 toml::Value::Array(arr) => {
237 let json_arr: Vec<_> = arr.iter().filter_map(toml_value_to_json).collect();
238 Some(serde_json::Value::Array(json_arr))
239 }
240 toml::Value::Table(table) => {
241 let mut json_obj = serde_json::Map::new();
242 for (k, v) in table {
243 if let Some(json_v) = toml_value_to_json(v) {
244 json_obj.insert(k.clone(), json_v);
245 }
246 }
247 Some(serde_json::Value::Object(json_obj))
248 }
249 toml::Value::Datetime(_) => None, }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256 use serde::{Deserialize, Serialize};
257 use std::collections::BTreeMap;
258
259 #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
261 #[serde(default)]
262 struct TestRuleConfig {
263 #[serde(default)]
264 enabled: bool,
265 #[serde(default)]
266 indent: i64,
267 #[serde(default)]
268 style: String,
269 #[serde(default)]
270 items: Vec<String>,
271 }
272
273 impl RuleConfig for TestRuleConfig {
274 const RULE_NAME: &'static str = "TEST001";
275 }
276
277 #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
281 #[serde(default)]
282 struct NullableTestConfig {
283 #[serde(default)]
284 enabled: bool,
285 #[serde(default, alias = "key-order")]
286 key_order: Option<Vec<String>>,
287 #[serde(default, alias = "title-pattern")]
288 title_pattern: Option<String>,
289 }
290
291 impl RuleConfig for NullableTestConfig {
292 const RULE_NAME: &'static str = "TEST_NULLABLE";
293 }
294
295 #[test]
296 fn test_is_nullable_sentinel() {
297 let sentinel = toml::Value::String(NULLABLE_SENTINEL.to_string());
298 assert!(is_nullable_sentinel(&sentinel));
299
300 let regular = toml::Value::String("normal".to_string());
301 assert!(!is_nullable_sentinel(®ular));
302
303 let integer = toml::Value::Integer(42);
304 assert!(!is_nullable_sentinel(&integer));
305 }
306
307 #[test]
308 fn test_is_polymorphic_sentinel() {
309 let sentinel = polymorphic_sentinel_value();
310 assert!(is_polymorphic_sentinel(&sentinel));
311
312 let nullable = toml::Value::String(NULLABLE_SENTINEL.to_string());
314 assert!(!is_polymorphic_sentinel(&nullable));
315 assert!(!is_nullable_sentinel(&sentinel));
316
317 let regular = toml::Value::String("normal".to_string());
318 assert!(!is_polymorphic_sentinel(®ular));
319 }
320
321 #[test]
322 fn test_config_schema_table_preserves_nullable_keys() {
323 let config = NullableTestConfig::default();
324 let table = config_schema_table(&config).unwrap();
325
326 assert!(table.contains_key("enabled"), "enabled key missing");
328 assert!(table.contains_key("key_order"), "key_order key missing");
329 assert!(table.contains_key("title_pattern"), "title_pattern key missing");
330
331 assert!(is_nullable_sentinel(table.get("key_order").unwrap()));
333 assert!(is_nullable_sentinel(table.get("title_pattern").unwrap()));
334
335 assert_eq!(table.get("enabled"), Some(&toml::Value::Boolean(false)));
337 }
338
339 #[test]
340 fn test_config_schema_table_non_null_option_uses_real_value() {
341 let config = NullableTestConfig {
342 enabled: true,
343 key_order: Some(vec!["title".to_string(), "date".to_string()]),
344 title_pattern: Some("pattern".to_string()),
345 };
346 let table = config_schema_table(&config).unwrap();
347
348 let key_order = table.get("key_order").unwrap();
350 assert!(!is_nullable_sentinel(key_order));
351 assert!(matches!(key_order, toml::Value::Array(_)));
352
353 let title_pattern = table.get("title_pattern").unwrap();
354 assert!(!is_nullable_sentinel(title_pattern));
355 assert_eq!(title_pattern, &toml::Value::String("pattern".to_string()));
356 }
357
358 #[test]
359 fn test_json_to_toml_value_still_drops_null() {
360 assert!(json_to_toml_value(&serde_json::Value::Null).is_none());
362 }
363
364 #[test]
365 fn test_config_schema_table_all_keys_present() {
366 let config = NullableTestConfig::default();
367 let table = config_schema_table(&config).unwrap();
368 assert_eq!(table.len(), 3, "Expected 3 keys: enabled, key_order, title_pattern");
369 }
370
371 #[test]
372 fn test_config_schema_table_never_drops_keys() {
373 let mut obj = serde_json::Map::new();
377 obj.insert("real_key".to_string(), serde_json::json!(42));
378 obj.insert("null_key".to_string(), serde_json::Value::Null);
379 let json = serde_json::Value::Object(obj);
380
381 let obj = json.as_object().unwrap();
383 let mut table = toml::map::Map::new();
384 for (k, v) in obj {
385 if v.is_null() {
386 table.insert(k.clone(), toml::Value::String(NULLABLE_SENTINEL.to_string()));
387 } else {
388 let toml_v =
389 json_to_toml_value(v).unwrap_or_else(|| toml::Value::String(NULLABLE_SENTINEL.to_string()));
390 table.insert(k.clone(), toml_v);
391 }
392 }
393
394 assert_eq!(table.len(), 2, "Both keys must be present");
395 assert!(table.contains_key("real_key"));
396 assert!(table.contains_key("null_key"));
397 }
398
399 #[test]
400 fn test_toml_value_to_json_basic_types() {
401 let toml_str = toml::Value::String("hello".to_string());
403 let json_str = toml_value_to_json(&toml_str).unwrap();
404 assert_eq!(json_str, serde_json::Value::String("hello".to_string()));
405
406 let toml_int = toml::Value::Integer(42);
408 let json_int = toml_value_to_json(&toml_int).unwrap();
409 assert_eq!(json_int, serde_json::json!(42));
410
411 let toml_float = toml::Value::Float(1.234);
413 let json_float = toml_value_to_json(&toml_float).unwrap();
414 assert_eq!(json_float, serde_json::json!(1.234));
415
416 let toml_bool = toml::Value::Boolean(true);
418 let json_bool = toml_value_to_json(&toml_bool).unwrap();
419 assert_eq!(json_bool, serde_json::Value::Bool(true));
420 }
421
422 #[test]
423 fn test_toml_value_to_json_complex_types() {
424 let toml_arr = toml::Value::Array(vec![
426 toml::Value::String("a".to_string()),
427 toml::Value::String("b".to_string()),
428 ]);
429 let json_arr = toml_value_to_json(&toml_arr).unwrap();
430 assert_eq!(json_arr, serde_json::json!(["a", "b"]));
431
432 let mut toml_table = toml::map::Map::new();
434 toml_table.insert("key1".to_string(), toml::Value::String("value1".to_string()));
435 toml_table.insert("key2".to_string(), toml::Value::Integer(123));
436 let toml_tbl = toml::Value::Table(toml_table);
437 let json_tbl = toml_value_to_json(&toml_tbl).unwrap();
438
439 let expected = serde_json::json!({
440 "key1": "value1",
441 "key2": 123
442 });
443 assert_eq!(json_tbl, expected);
444 }
445
446 #[test]
447 fn test_toml_value_to_json_datetime() {
448 let toml_dt = toml::Value::Datetime("2023-01-01T00:00:00Z".parse().unwrap());
450 assert!(toml_value_to_json(&toml_dt).is_none());
451 }
452
453 #[test]
454 fn test_json_to_toml_value_basic_types() {
455 assert!(json_to_toml_value(&serde_json::Value::Null).is_none());
457
458 let json_bool = serde_json::Value::Bool(false);
460 let toml_bool = json_to_toml_value(&json_bool).unwrap();
461 assert_eq!(toml_bool, toml::Value::Boolean(false));
462
463 let json_int = serde_json::json!(42);
465 let toml_int = json_to_toml_value(&json_int).unwrap();
466 assert_eq!(toml_int, toml::Value::Integer(42));
467
468 let json_float = serde_json::json!(1.234);
470 let toml_float = json_to_toml_value(&json_float).unwrap();
471 assert_eq!(toml_float, toml::Value::Float(1.234));
472
473 let json_str = serde_json::Value::String("test".to_string());
475 let toml_str = json_to_toml_value(&json_str).unwrap();
476 assert_eq!(toml_str, toml::Value::String("test".to_string()));
477 }
478
479 #[test]
480 fn test_json_to_toml_value_complex_types() {
481 let json_arr = serde_json::json!(["x", "y", "z"]);
483 let toml_arr = json_to_toml_value(&json_arr).unwrap();
484 if let toml::Value::Array(arr) = toml_arr {
485 assert_eq!(arr.len(), 3);
486 assert_eq!(arr[0], toml::Value::String("x".to_string()));
487 assert_eq!(arr[1], toml::Value::String("y".to_string()));
488 assert_eq!(arr[2], toml::Value::String("z".to_string()));
489 } else {
490 panic!("Expected array");
491 }
492
493 let json_obj = serde_json::json!({
495 "name": "test",
496 "count": 10,
497 "active": true
498 });
499 let toml_obj = json_to_toml_value(&json_obj).unwrap();
500 if let toml::Value::Table(table) = toml_obj {
501 assert_eq!(table.get("name"), Some(&toml::Value::String("test".to_string())));
502 assert_eq!(table.get("count"), Some(&toml::Value::Integer(10)));
503 assert_eq!(table.get("active"), Some(&toml::Value::Boolean(true)));
504 } else {
505 panic!("Expected table");
506 }
507 }
508
509 #[test]
510 fn test_load_rule_config_default() {
511 let config = crate::config::Config::default();
513
514 let rule_config: TestRuleConfig = load_rule_config(&config);
516 assert_eq!(rule_config, TestRuleConfig::default());
517 }
518
519 #[test]
520 fn test_load_rule_config_with_values() {
521 let mut config = crate::config::Config::default();
523 let mut rule_values = BTreeMap::new();
524 rule_values.insert("enabled".to_string(), toml::Value::Boolean(true));
525 rule_values.insert("indent".to_string(), toml::Value::Integer(4));
526 rule_values.insert("style".to_string(), toml::Value::String("consistent".to_string()));
527 rule_values.insert(
528 "items".to_string(),
529 toml::Value::Array(vec![
530 toml::Value::String("item1".to_string()),
531 toml::Value::String("item2".to_string()),
532 ]),
533 );
534
535 config.rules.insert(
536 "TEST001".to_string(),
537 crate::config::RuleConfig {
538 severity: None,
539 values: rule_values,
540 },
541 );
542
543 let rule_config: TestRuleConfig = load_rule_config(&config);
545 assert!(rule_config.enabled);
546 assert_eq!(rule_config.indent, 4);
547 assert_eq!(rule_config.style, "consistent");
548 assert_eq!(rule_config.items, vec!["item1", "item2"]);
549 }
550
551 #[test]
552 fn test_load_rule_config_partial() {
553 let mut config = crate::config::Config::default();
555 let mut rule_values = BTreeMap::new();
556 rule_values.insert("enabled".to_string(), toml::Value::Boolean(true));
557 rule_values.insert("style".to_string(), toml::Value::String("custom".to_string()));
558
559 config.rules.insert(
560 "TEST001".to_string(),
561 crate::config::RuleConfig {
562 severity: None,
563 values: rule_values,
564 },
565 );
566
567 let rule_config: TestRuleConfig = load_rule_config(&config);
569 assert!(rule_config.enabled); assert_eq!(rule_config.indent, 0); assert_eq!(rule_config.style, "custom"); assert_eq!(rule_config.items, Vec::<String>::new()); }
574
575 #[test]
576 fn test_conversion_roundtrip() {
577 let original = toml::Value::Table({
579 let mut table = toml::map::Map::new();
580 table.insert("string".to_string(), toml::Value::String("test".to_string()));
581 table.insert("number".to_string(), toml::Value::Integer(42));
582 table.insert("bool".to_string(), toml::Value::Boolean(true));
583 table.insert(
584 "array".to_string(),
585 toml::Value::Array(vec![
586 toml::Value::String("a".to_string()),
587 toml::Value::String("b".to_string()),
588 ]),
589 );
590 table
591 });
592
593 let json = toml_value_to_json(&original).unwrap();
594 let back_to_toml = json_to_toml_value(&json).unwrap();
595
596 assert_eq!(original, back_to_toml);
597 }
598
599 #[test]
600 fn test_edge_cases() {
601 let empty_arr = toml::Value::Array(vec![]);
603 let json_arr = toml_value_to_json(&empty_arr).unwrap();
604 assert_eq!(json_arr, serde_json::json!([]));
605
606 let empty_table = toml::Value::Table(toml::map::Map::new());
608 let json_table = toml_value_to_json(&empty_table).unwrap();
609 assert_eq!(json_table, serde_json::json!({}));
610
611 let nested = toml::Value::Table({
613 let mut outer = toml::map::Map::new();
614 outer.insert(
615 "inner".to_string(),
616 toml::Value::Table({
617 let mut inner = toml::map::Map::new();
618 inner.insert("value".to_string(), toml::Value::Integer(123));
619 inner
620 }),
621 );
622 outer
623 });
624 let json_nested = toml_value_to_json(&nested).unwrap();
625 assert_eq!(
626 json_nested,
627 serde_json::json!({
628 "inner": {
629 "value": 123
630 }
631 })
632 );
633 }
634
635 #[test]
636 fn test_float_edge_cases() {
637 let nan = serde_json::Number::from_f64(f64::NAN);
639 assert!(nan.is_none());
640
641 let inf = serde_json::Number::from_f64(f64::INFINITY);
642 assert!(inf.is_none());
643
644 let valid_float = toml::Value::Float(1.23);
646 let json_float = toml_value_to_json(&valid_float).unwrap();
647 assert_eq!(json_float, serde_json::json!(1.23));
648 }
649
650 #[test]
651 fn test_invalid_config_returns_default() {
652 let mut config = crate::config::Config::default();
654 let mut rule_values = BTreeMap::new();
655 rule_values.insert("unknown_field".to_string(), toml::Value::Boolean(true));
656 rule_values.insert("items".to_string(), toml::Value::Table(toml::map::Map::new()));
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_eq!(rule_config, TestRuleConfig::default());
671 }
672
673 #[test]
674 fn test_invalid_field_type() {
675 let mut config = crate::config::Config::default();
677 let mut rule_values = BTreeMap::new();
678 rule_values.insert("indent".to_string(), toml::Value::String("not_a_number".to_string()));
680
681 config.rules.insert(
682 "TEST001".to_string(),
683 crate::config::RuleConfig {
684 severity: None,
685 values: rule_values,
686 },
687 );
688
689 let rule_config: TestRuleConfig = load_rule_config(&config);
691 assert_eq!(rule_config, TestRuleConfig::default());
692 }
693
694 #[test]
697 fn test_is_rule_name_valid() {
698 assert!(is_rule_name("MD001"));
700 assert!(is_rule_name("MD060"));
701 assert!(is_rule_name("MD123"));
702 assert!(is_rule_name("MD999"));
703
704 assert!(is_rule_name("md001"));
706 assert!(is_rule_name("Md060"));
707 assert!(is_rule_name("mD123"));
708
709 assert!(is_rule_name("MD0001"));
711 assert!(is_rule_name("MD12345"));
712 }
713
714 #[test]
715 fn test_is_rule_name_invalid() {
716 assert!(!is_rule_name("MD"));
718 assert!(!is_rule_name("MD1"));
719 assert!(!is_rule_name("M"));
720 assert!(!is_rule_name(""));
721
722 assert!(!is_rule_name("disable"));
724 assert!(!is_rule_name("enable"));
725 assert!(!is_rule_name("flavor"));
726 assert!(!is_rule_name("line-length"));
727 assert!(!is_rule_name("global"));
728
729 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")); }
736
737 #[test]
740 fn test_json_to_rule_config_simple() {
741 let json = serde_json::json!({
742 "enabled": true,
743 "style": "aligned"
744 });
745
746 let rule_config = json_to_rule_config(&json).unwrap();
747
748 assert_eq!(rule_config.values.get("enabled"), Some(&toml::Value::Boolean(true)));
749 assert_eq!(
750 rule_config.values.get("style"),
751 Some(&toml::Value::String("aligned".to_string()))
752 );
753 assert!(rule_config.severity.is_none());
754 }
755
756 #[test]
757 fn test_json_to_rule_config_with_numbers() {
758 let json = serde_json::json!({
759 "line-length": 120,
760 "max-width": 0,
761 "indent": 4
762 });
763
764 let rule_config = json_to_rule_config(&json).unwrap();
765
766 assert_eq!(rule_config.values.get("line-length"), Some(&toml::Value::Integer(120)));
767 assert_eq!(rule_config.values.get("max-width"), Some(&toml::Value::Integer(0)));
768 assert_eq!(rule_config.values.get("indent"), Some(&toml::Value::Integer(4)));
769 }
770
771 #[test]
772 fn test_json_to_rule_config_with_arrays() {
773 let json = serde_json::json!({
774 "names": ["JavaScript", "TypeScript", "React"],
775 "exclude-patterns": ["*.test.md", "draft-*"]
776 });
777
778 let rule_config = json_to_rule_config(&json).unwrap();
779
780 let expected_names = toml::Value::Array(vec![
781 toml::Value::String("JavaScript".to_string()),
782 toml::Value::String("TypeScript".to_string()),
783 toml::Value::String("React".to_string()),
784 ]);
785 assert_eq!(rule_config.values.get("names"), Some(&expected_names));
786
787 let expected_patterns = toml::Value::Array(vec![
788 toml::Value::String("*.test.md".to_string()),
789 toml::Value::String("draft-*".to_string()),
790 ]);
791 assert_eq!(rule_config.values.get("exclude-patterns"), Some(&expected_patterns));
792 }
793
794 #[test]
795 fn test_json_to_rule_config_with_severity() {
796 let json = serde_json::json!({
798 "severity": "error",
799 "style": "aligned"
800 });
801 let rule_config = json_to_rule_config(&json).unwrap();
802 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Error));
803 assert!(!rule_config.values.contains_key("severity")); let json = serde_json::json!({
807 "severity": "warning",
808 "enabled": true
809 });
810 let rule_config = json_to_rule_config(&json).unwrap();
811 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Warning));
812
813 let json = serde_json::json!({
815 "severity": "info"
816 });
817 let rule_config = json_to_rule_config(&json).unwrap();
818 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Info));
819
820 let json = serde_json::json!({
822 "severity": "ERROR"
823 });
824 let rule_config = json_to_rule_config(&json).unwrap();
825 assert_eq!(rule_config.severity, Some(crate::rule::Severity::Error));
826 }
827
828 #[test]
829 fn test_json_to_rule_config_invalid_severity() {
830 let json = serde_json::json!({
832 "severity": "critical",
833 "style": "aligned"
834 });
835 let rule_config = json_to_rule_config(&json).unwrap();
836 assert!(rule_config.severity.is_none()); assert_eq!(
838 rule_config.values.get("style"),
839 Some(&toml::Value::String("aligned".to_string()))
840 );
841
842 let json = serde_json::json!({
844 "severity": 1,
845 "enabled": true
846 });
847 let rule_config = json_to_rule_config(&json).unwrap();
848 assert!(rule_config.severity.is_none()); }
850
851 #[test]
852 fn test_json_to_rule_config_non_object() {
853 assert!(json_to_rule_config(&serde_json::json!(42)).is_none());
855 assert!(json_to_rule_config(&serde_json::json!("string")).is_none());
856 assert!(json_to_rule_config(&serde_json::json!(true)).is_none());
857 assert!(json_to_rule_config(&serde_json::json!([1, 2, 3])).is_none());
858 assert!(json_to_rule_config(&serde_json::Value::Null).is_none());
859 }
860
861 #[test]
862 fn test_json_to_rule_config_empty_object() {
863 let json = serde_json::json!({});
864 let rule_config = json_to_rule_config(&json).unwrap();
865 assert!(rule_config.values.is_empty());
866 assert!(rule_config.severity.is_none());
867 }
868
869 #[test]
870 fn test_json_to_rule_config_nested_objects() {
871 let json = serde_json::json!({
873 "options": {
874 "nested-key": "nested-value",
875 "nested-number": 42
876 }
877 });
878
879 let rule_config = json_to_rule_config(&json).unwrap();
880
881 let options = rule_config.values.get("options").unwrap();
882 if let toml::Value::Table(table) = options {
883 assert_eq!(
884 table.get("nested-key"),
885 Some(&toml::Value::String("nested-value".to_string()))
886 );
887 assert_eq!(table.get("nested-number"), Some(&toml::Value::Integer(42)));
888 } else {
889 panic!("options should be a table");
890 }
891 }
892
893 #[test]
894 fn test_json_to_rule_config_md060_example() {
895 let json = serde_json::json!({
897 "enabled": true,
898 "style": "aligned",
899 "max-width": 120,
900 "column-align": "auto",
901 "loose-last-column": false
902 });
903
904 let rule_config = json_to_rule_config(&json).unwrap();
905
906 assert_eq!(rule_config.values.get("enabled"), Some(&toml::Value::Boolean(true)));
907 assert_eq!(
908 rule_config.values.get("style"),
909 Some(&toml::Value::String("aligned".to_string()))
910 );
911 assert_eq!(rule_config.values.get("max-width"), Some(&toml::Value::Integer(120)));
912 assert_eq!(
913 rule_config.values.get("column-align"),
914 Some(&toml::Value::String("auto".to_string()))
915 );
916 assert_eq!(
917 rule_config.values.get("loose-last-column"),
918 Some(&toml::Value::Boolean(false))
919 );
920 }
921
922 #[test]
923 fn test_json_to_rule_config_md044_example() {
924 let json = serde_json::json!({
926 "names": ["JavaScript", "TypeScript", "GitHub", "macOS"],
927 "code-blocks": false,
928 "html-elements": false
929 });
930
931 let rule_config = json_to_rule_config(&json).unwrap();
932
933 let expected_names = toml::Value::Array(vec![
934 toml::Value::String("JavaScript".to_string()),
935 toml::Value::String("TypeScript".to_string()),
936 toml::Value::String("GitHub".to_string()),
937 toml::Value::String("macOS".to_string()),
938 ]);
939 assert_eq!(rule_config.values.get("names"), Some(&expected_names));
940 assert_eq!(
941 rule_config.values.get("code-blocks"),
942 Some(&toml::Value::Boolean(false))
943 );
944 assert_eq!(
945 rule_config.values.get("html-elements"),
946 Some(&toml::Value::Boolean(false))
947 );
948 }
949
950 #[test]
953 fn test_json_to_rule_config_with_warnings_valid() {
954 let json = serde_json::json!({
955 "severity": "error",
956 "enabled": true
957 });
958
959 let result = json_to_rule_config_with_warnings(&json);
960
961 assert!(result.config.is_some());
962 assert!(
963 result.warnings.is_empty(),
964 "Expected no warnings, got: {:?}",
965 result.warnings
966 );
967 assert_eq!(result.config.unwrap().severity, Some(crate::rule::Severity::Error));
968 }
969
970 #[test]
971 fn test_json_to_rule_config_with_warnings_invalid_severity() {
972 let json = serde_json::json!({
973 "severity": "critical",
974 "style": "aligned"
975 });
976
977 let result = json_to_rule_config_with_warnings(&json);
978
979 assert!(result.config.is_some());
980 assert_eq!(result.warnings.len(), 1);
981 assert!(result.warnings[0].contains("Invalid severity 'critical'"));
982 assert!(result.config.unwrap().severity.is_none());
984 }
985
986 #[test]
987 fn test_json_to_rule_config_with_warnings_wrong_severity_type() {
988 let json = serde_json::json!({
989 "severity": 123,
990 "enabled": true
991 });
992
993 let result = json_to_rule_config_with_warnings(&json);
994
995 assert!(result.config.is_some());
996 assert_eq!(result.warnings.len(), 1);
997 assert!(result.warnings[0].contains("Severity must be a string"));
998 }
999
1000 #[test]
1001 fn test_json_to_rule_config_with_warnings_non_object() {
1002 let json = serde_json::json!("not an object");
1003
1004 let result = json_to_rule_config_with_warnings(&json);
1005
1006 assert!(result.config.is_none());
1007 assert_eq!(result.warnings.len(), 1);
1008 assert!(result.warnings[0].contains("Expected object"));
1009 }
1010
1011 #[test]
1014 fn test_rule_config_integration_with_config() {
1015 let mut config = crate::config::Config::default();
1017
1018 let md060_json = serde_json::json!({
1020 "enabled": true,
1021 "style": "aligned",
1022 "max-width": 120
1023 });
1024 let md013_json = serde_json::json!({
1025 "line-length": 100,
1026 "code-blocks": false
1027 });
1028
1029 if let Some(md060_config) = json_to_rule_config(&md060_json) {
1030 config.rules.insert("MD060".to_string(), md060_config);
1031 }
1032 if let Some(md013_config) = json_to_rule_config(&md013_json) {
1033 config.rules.insert("MD013".to_string(), md013_config);
1034 }
1035
1036 assert!(config.rules.contains_key("MD060"));
1038 assert!(config.rules.contains_key("MD013"));
1039
1040 let md060 = config.rules.get("MD060").unwrap();
1042 assert_eq!(md060.values.get("enabled"), Some(&toml::Value::Boolean(true)));
1043 assert_eq!(
1044 md060.values.get("style"),
1045 Some(&toml::Value::String("aligned".to_string()))
1046 );
1047 assert_eq!(md060.values.get("max-width"), Some(&toml::Value::Integer(120)));
1048 }
1049
1050 #[test]
1051 fn test_rule_config_integration_with_severity() {
1052 let mut config = crate::config::Config::default();
1053
1054 let json = serde_json::json!({
1055 "severity": "error",
1056 "enabled": true
1057 });
1058
1059 if let Some(rule_config) = json_to_rule_config(&json) {
1060 config.rules.insert("MD041".to_string(), rule_config);
1061 }
1062
1063 let md041 = config.rules.get("MD041").unwrap();
1064 assert_eq!(md041.severity, Some(crate::rule::Severity::Error));
1065 }
1066
1067 #[test]
1068 fn test_rule_config_integration_case_normalization() {
1069 let mut config = crate::config::Config::default();
1071
1072 let json = serde_json::json!({ "enabled": true });
1073
1074 for rule_name in ["md060", "MD060", "Md060"] {
1076 if is_rule_name(rule_name)
1077 && let Some(rule_config) = json_to_rule_config(&json)
1078 {
1079 config.rules.insert(rule_name.to_ascii_uppercase(), rule_config);
1080 }
1081 }
1082
1083 assert!(config.rules.contains_key("MD060"));
1085 assert_eq!(config.rules.len(), 1); }
1087
1088 #[test]
1089 fn test_rule_config_integration_filters_non_rules() {
1090 let keys = ["MD060", "disable", "enable", "flavor", "line-length", "global"];
1092
1093 let rule_keys: Vec<_> = keys.iter().filter(|k| is_rule_name(k)).collect();
1094
1095 assert_eq!(rule_keys, vec![&"MD060"]);
1096 }
1097
1098 #[test]
1099 fn test_multiple_rule_configs_with_mixed_validity() {
1100 let rules = vec![
1102 ("MD060", serde_json::json!({ "severity": "error", "style": "aligned" })),
1103 (
1104 "MD013",
1105 serde_json::json!({ "severity": "invalid", "line-length": 100 }),
1106 ),
1107 ("MD041", serde_json::json!({ "enabled": true })),
1108 ];
1109
1110 let mut config = crate::config::Config::default();
1111 let mut all_warnings = Vec::new();
1112
1113 for (name, json) in rules {
1114 let result = json_to_rule_config_with_warnings(&json);
1115 all_warnings.extend(result.warnings);
1116 if let Some(rule_config) = result.config {
1117 config.rules.insert(name.to_string(), rule_config);
1118 }
1119 }
1120
1121 assert_eq!(config.rules.len(), 3);
1123
1124 assert_eq!(all_warnings.len(), 1);
1126 assert!(all_warnings[0].contains("Invalid severity"));
1127
1128 assert_eq!(
1130 config.rules.get("MD060").unwrap().severity,
1131 Some(crate::rule::Severity::Error)
1132 );
1133 assert!(config.rules.get("MD013").unwrap().severity.is_none());
1134 }
1135
1136 #[test]
1140 fn test_end_to_end_md013_line_length_config() {
1141 let content = "# Test\n\nThis is a line that is exactly 50 characters long.\n";
1143
1144 let mut config = crate::config::Config::default();
1146 let json = serde_json::json!({
1147 "line-length": 40
1148 });
1149 if let Some(rule_config) = json_to_rule_config(&json) {
1150 config.rules.insert("MD013".to_string(), rule_config);
1151 }
1152
1153 config.global.enable = vec!["MD013".to_string()];
1155
1156 let rules = crate::rules::all_rules(&config);
1157 let filtered = crate::rules::filter_rules(&rules, &config.global);
1158
1159 let result = crate::lint(
1160 content,
1161 &filtered,
1162 false,
1163 crate::config::MarkdownFlavor::Standard,
1164 None,
1165 Some(&config),
1166 );
1167
1168 let warnings = result.expect("Linting should succeed");
1169
1170 let has_md013 = warnings.iter().any(|w| w.rule_name.as_deref() == Some("MD013"));
1172 assert!(has_md013, "Should have MD013 warning with line-length=40");
1173 }
1174
1175 #[test]
1176 fn test_end_to_end_md013_line_length_no_warning() {
1177 let content = "# Test\n\nThis is a line that is exactly 50 characters long.\n";
1179
1180 let mut config = crate::config::Config::default();
1182 let json = serde_json::json!({
1183 "line-length": 100
1184 });
1185 if let Some(rule_config) = json_to_rule_config(&json) {
1186 config.rules.insert("MD013".to_string(), rule_config);
1187 }
1188
1189 config.global.enable = vec!["MD013".to_string()];
1191
1192 let rules = crate::rules::all_rules(&config);
1193 let filtered = crate::rules::filter_rules(&rules, &config.global);
1194
1195 let result = crate::lint(
1196 content,
1197 &filtered,
1198 false,
1199 crate::config::MarkdownFlavor::Standard,
1200 None,
1201 Some(&config),
1202 );
1203
1204 let warnings = result.expect("Linting should succeed");
1205
1206 let has_md013 = warnings.iter().any(|w| w.rule_name.as_deref() == Some("MD013"));
1208 assert!(!has_md013, "Should NOT have MD013 warning with line-length=100");
1209 }
1210
1211 #[test]
1212 fn test_end_to_end_md044_proper_names() {
1213 let content = "# Test\n\nWe use javascript and typescript.\n";
1215
1216 let mut config = crate::config::Config::default();
1218 let json = serde_json::json!({
1219 "names": ["JavaScript", "TypeScript"],
1220 "code-blocks": false
1221 });
1222 if let Some(rule_config) = json_to_rule_config(&json) {
1223 config.rules.insert("MD044".to_string(), rule_config);
1224 }
1225
1226 config.global.enable = vec!["MD044".to_string()];
1228
1229 let rules = crate::rules::all_rules(&config);
1230 let filtered = crate::rules::filter_rules(&rules, &config.global);
1231
1232 let result = crate::lint(
1233 content,
1234 &filtered,
1235 false,
1236 crate::config::MarkdownFlavor::Standard,
1237 None,
1238 Some(&config),
1239 );
1240
1241 let warnings = result.expect("Linting should succeed");
1242
1243 let md044_warnings: Vec<_> = warnings
1245 .iter()
1246 .filter(|w| w.rule_name.as_deref() == Some("MD044"))
1247 .collect();
1248
1249 assert!(
1250 md044_warnings.len() >= 2,
1251 "Should have MD044 warnings for 'javascript' and 'typescript', got {}",
1252 md044_warnings.len()
1253 );
1254 }
1255
1256 #[test]
1257 fn test_end_to_end_severity_config() {
1258 let content = "test\n"; let mut config = crate::config::Config::default();
1262 let json = serde_json::json!({
1263 "severity": "info"
1264 });
1265 if let Some(rule_config) = json_to_rule_config(&json) {
1266 config.rules.insert("MD041".to_string(), rule_config);
1267 }
1268
1269 config.global.enable = vec!["MD041".to_string()];
1271
1272 let rules = crate::rules::all_rules(&config);
1273 let filtered = crate::rules::filter_rules(&rules, &config.global);
1274
1275 let result = crate::lint(
1276 content,
1277 &filtered,
1278 false,
1279 crate::config::MarkdownFlavor::Standard,
1280 None,
1281 Some(&config),
1282 );
1283
1284 let warnings = result.expect("Linting should succeed");
1285
1286 let md041 = warnings.iter().find(|w| w.rule_name.as_deref() == Some("MD041"));
1288 assert!(md041.is_some(), "Should have MD041 warning");
1289 assert_eq!(
1290 md041.unwrap().severity,
1291 crate::rule::Severity::Info,
1292 "MD041 should have Info severity from config"
1293 );
1294 }
1295}