mdbook_lint_core/
engine.rs

1//! Rule provider system and lint engine.
2
3use crate::error::Result;
4use crate::registry::RuleRegistry;
5use serde_json::Value;
6
7/// Trait for rule providers to register rules with the engine
8pub trait RuleProvider: Send + Sync {
9    /// Unique identifier for this rule provider
10    fn provider_id(&self) -> &'static str;
11
12    /// Human-readable description of this rule provider
13    fn description(&self) -> &'static str;
14
15    /// Version of this rule provider
16    fn version(&self) -> &'static str;
17
18    /// Register all rules from this provider with the registry
19    fn register_rules(&self, registry: &mut RuleRegistry);
20
21    /// Provider-specific configuration schema
22    fn config_schema(&self) -> Option<Value> {
23        None
24    }
25
26    /// List of rule IDs that this provider registers
27    fn rule_ids(&self) -> Vec<&'static str> {
28        Vec::new()
29    }
30
31    /// Provider initialization hook
32    fn initialize(&self) -> Result<()> {
33        Ok(())
34    }
35}
36
37/// Registry for managing rule providers and creating engines
38#[derive(Default)]
39pub struct PluginRegistry {
40    providers: Vec<Box<dyn RuleProvider>>,
41}
42
43impl PluginRegistry {
44    /// Create a new empty plugin registry
45    pub fn new() -> Self {
46        Self {
47            providers: Vec::new(),
48        }
49    }
50
51    /// Register a rule provider
52    pub fn register_provider(&mut self, provider: Box<dyn RuleProvider>) -> Result<()> {
53        // Initialize the provider
54        provider.initialize()?;
55
56        // Check for duplicate provider IDs
57        let provider_id = provider.provider_id();
58        if self
59            .providers
60            .iter()
61            .any(|p| p.provider_id() == provider_id)
62        {
63            return Err(crate::error::MdBookLintError::plugin_error(format!(
64                "Provider with ID '{provider_id}' is already registered"
65            )));
66        }
67
68        self.providers.push(provider);
69        Ok(())
70    }
71
72    /// Get all registered providers
73    pub fn providers(&self) -> &[Box<dyn RuleProvider>] {
74        &self.providers
75    }
76
77    /// Get a provider by ID
78    pub fn get_provider(&self, id: &str) -> Option<&dyn RuleProvider> {
79        self.providers
80            .iter()
81            .find(|p| p.provider_id() == id)
82            .map(|p| p.as_ref())
83    }
84
85    /// Create a rule registry with all registered providers
86    pub fn create_rule_registry(&self) -> Result<RuleRegistry> {
87        let mut registry = RuleRegistry::new();
88
89        for provider in &self.providers {
90            provider.register_rules(&mut registry);
91        }
92
93        Ok(registry)
94    }
95
96    /// Create a lint engine with all registered providers
97    pub fn create_engine(&self) -> Result<LintEngine> {
98        let registry = self.create_rule_registry()?;
99        Ok(LintEngine::with_registry(registry))
100    }
101
102    /// List all available rule IDs from all providers
103    pub fn available_rule_ids(&self) -> Vec<String> {
104        let mut rule_ids = Vec::new();
105
106        for provider in &self.providers {
107            for rule_id in provider.rule_ids() {
108                rule_ids.push(rule_id.to_string());
109            }
110        }
111
112        rule_ids.sort();
113        rule_ids.dedup();
114        rule_ids
115    }
116
117    /// Get provider information for debugging/introspection
118    pub fn provider_info(&self) -> Vec<ProviderInfo> {
119        self.providers
120            .iter()
121            .map(|p| ProviderInfo {
122                id: p.provider_id().to_string(),
123                description: p.description().to_string(),
124                version: p.version().to_string(),
125                rule_count: p.rule_ids().len(),
126            })
127            .collect()
128    }
129}
130
131/// Information about a registered provider (for debugging/introspection)
132#[derive(Debug, Clone)]
133pub struct ProviderInfo {
134    pub id: String,
135    pub description: String,
136    pub version: String,
137    pub rule_count: usize,
138}
139
140/// Markdown linting engine
141pub struct LintEngine {
142    registry: RuleRegistry,
143}
144
145impl LintEngine {
146    /// Create a new lint engine with no rules
147    pub fn new() -> Self {
148        Self {
149            registry: RuleRegistry::new(),
150        }
151    }
152
153    /// Create a lint engine with an existing rule registry
154    pub fn with_registry(registry: RuleRegistry) -> Self {
155        Self { registry }
156    }
157
158    /// Get the underlying rule registry
159    pub fn registry(&self) -> &RuleRegistry {
160        &self.registry
161    }
162
163    /// Get a mutable reference to the rule registry
164    pub fn registry_mut(&mut self) -> &mut RuleRegistry {
165        &mut self.registry
166    }
167
168    /// Lint a document with all registered rules
169    pub fn lint_document(&self, document: &crate::Document) -> Result<Vec<crate::Violation>> {
170        self.registry.check_document_optimized(document)
171    }
172
173    /// Lint a document with specific configuration
174    pub fn lint_document_with_config(
175        &self,
176        document: &crate::Document,
177        config: &crate::Config,
178    ) -> Result<Vec<crate::Violation>> {
179        self.registry
180            .check_document_optimized_with_config(document, config)
181    }
182
183    /// Lint content string directly (convenience method)
184    pub fn lint_content(&self, content: &str, path: &str) -> Result<Vec<crate::Violation>> {
185        let document = crate::Document::new(content.to_string(), std::path::PathBuf::from(path))?;
186        self.lint_document(&document)
187    }
188
189    /// Get all available rule IDs
190    pub fn available_rules(&self) -> Vec<&'static str> {
191        self.registry.rule_ids()
192    }
193
194    /// Get enabled rules based on configuration
195    pub fn enabled_rules(&self, config: &crate::Config) -> Vec<&dyn crate::rule::Rule> {
196        self.registry.get_enabled_rules(config)
197    }
198}
199
200impl Default for LintEngine {
201    fn default() -> Self {
202        Self::new()
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use crate::rule::{Rule, RuleCategory, RuleMetadata};
210    use std::path::PathBuf;
211
212    // Test rule for plugin system testing
213    struct TestRule;
214
215    impl Rule for TestRule {
216        fn id(&self) -> &'static str {
217            "TEST001"
218        }
219        fn name(&self) -> &'static str {
220            "test-rule"
221        }
222        fn description(&self) -> &'static str {
223            "A test rule"
224        }
225        fn metadata(&self) -> RuleMetadata {
226            RuleMetadata::stable(RuleCategory::Structure)
227        }
228        fn check_with_ast<'a>(
229            &self,
230            _document: &crate::Document,
231            _ast: Option<&'a comrak::nodes::AstNode<'a>>,
232        ) -> Result<Vec<crate::Violation>> {
233            Ok(vec![])
234        }
235    }
236
237    // Test provider
238    struct TestProvider;
239
240    impl RuleProvider for TestProvider {
241        fn provider_id(&self) -> &'static str {
242            "test-provider"
243        }
244        fn description(&self) -> &'static str {
245            "Test provider"
246        }
247        fn version(&self) -> &'static str {
248            "0.1.0"
249        }
250
251        fn register_rules(&self, registry: &mut RuleRegistry) {
252            registry.register(Box::new(TestRule));
253        }
254
255        fn rule_ids(&self) -> Vec<&'static str> {
256            vec!["TEST001"]
257        }
258    }
259
260    #[test]
261    fn test_plugin_registry_basic() {
262        let mut registry = PluginRegistry::new();
263        assert_eq!(registry.providers().len(), 0);
264
265        registry.register_provider(Box::new(TestProvider)).unwrap();
266        assert_eq!(registry.providers().len(), 1);
267
268        let provider = registry.get_provider("test-provider").unwrap();
269        assert_eq!(provider.provider_id(), "test-provider");
270        assert_eq!(provider.description(), "Test provider");
271    }
272
273    #[test]
274    fn test_plugin_registry_duplicate_id() {
275        let mut registry = PluginRegistry::new();
276        registry.register_provider(Box::new(TestProvider)).unwrap();
277
278        // Should fail with duplicate ID
279        let result = registry.register_provider(Box::new(TestProvider));
280        assert!(result.is_err());
281        assert!(
282            result
283                .unwrap_err()
284                .to_string()
285                .contains("already registered")
286        );
287    }
288
289    #[test]
290    fn test_create_engine_from_registry() {
291        let mut registry = PluginRegistry::new();
292        registry.register_provider(Box::new(TestProvider)).unwrap();
293
294        let engine = registry.create_engine().unwrap();
295        let rule_ids = engine.available_rules();
296        assert!(rule_ids.contains(&"TEST001"));
297    }
298
299    #[test]
300    fn test_available_rule_ids() {
301        let mut registry = PluginRegistry::new();
302        registry.register_provider(Box::new(TestProvider)).unwrap();
303
304        let rule_ids = registry.available_rule_ids();
305        assert_eq!(rule_ids, vec!["TEST001"]);
306    }
307
308    #[test]
309    fn test_provider_info() {
310        let mut registry = PluginRegistry::new();
311        registry.register_provider(Box::new(TestProvider)).unwrap();
312
313        let info = registry.provider_info();
314        assert_eq!(info.len(), 1);
315        assert_eq!(info[0].id, "test-provider");
316        assert_eq!(info[0].description, "Test provider");
317        assert_eq!(info[0].version, "0.1.0");
318        assert_eq!(info[0].rule_count, 1);
319    }
320
321    #[test]
322    fn test_get_provider_not_found() {
323        let registry = PluginRegistry::new();
324        assert!(registry.get_provider("nonexistent").is_none());
325    }
326
327    #[test]
328    fn test_create_rule_registry() {
329        let mut registry = PluginRegistry::new();
330        registry.register_provider(Box::new(TestProvider)).unwrap();
331
332        let rule_registry = registry.create_rule_registry().unwrap();
333        assert!(!rule_registry.is_empty());
334    }
335
336    // Test provider with initialization failure
337    struct FailingProvider;
338
339    impl RuleProvider for FailingProvider {
340        fn provider_id(&self) -> &'static str {
341            "failing-provider"
342        }
343        fn description(&self) -> &'static str {
344            "Failing test provider"
345        }
346        fn version(&self) -> &'static str {
347            "0.1.0"
348        }
349        fn register_rules(&self, _registry: &mut RuleRegistry) {}
350        fn initialize(&self) -> Result<()> {
351            Err(crate::error::MdBookLintError::plugin_error(
352                "Initialization failed",
353            ))
354        }
355    }
356
357    #[test]
358    fn test_provider_initialization_failure() {
359        let mut registry = PluginRegistry::new();
360        let result = registry.register_provider(Box::new(FailingProvider));
361        assert!(result.is_err());
362        assert!(
363            result
364                .unwrap_err()
365                .to_string()
366                .contains("Initialization failed")
367        );
368    }
369
370    // Test provider with config schema
371    struct ConfigurableProvider;
372
373    impl RuleProvider for ConfigurableProvider {
374        fn provider_id(&self) -> &'static str {
375            "configurable-provider"
376        }
377        fn description(&self) -> &'static str {
378            "Configurable test provider"
379        }
380        fn version(&self) -> &'static str {
381            "0.1.0"
382        }
383        fn register_rules(&self, _registry: &mut RuleRegistry) {}
384        fn config_schema(&self) -> Option<Value> {
385            Some(serde_json::json!({
386                "type": "object",
387                "properties": {
388                    "enabled": {"type": "boolean"}
389                }
390            }))
391        }
392    }
393
394    #[test]
395    fn test_provider_with_config_schema() {
396        let provider = ConfigurableProvider;
397        let schema = provider.config_schema();
398        assert!(schema.is_some());
399        let schema = schema.unwrap();
400        assert_eq!(schema["type"], "object");
401    }
402
403    #[test]
404    fn test_lint_engine_with_registry() {
405        let mut rule_registry = RuleRegistry::new();
406        rule_registry.register(Box::new(TestRule));
407
408        let engine = LintEngine::with_registry(rule_registry);
409        let rules = engine.available_rules();
410        assert!(rules.contains(&"TEST001"));
411    }
412
413    #[test]
414    fn test_lint_engine_api() {
415        let mut registry = PluginRegistry::new();
416        registry.register_provider(Box::new(TestProvider)).unwrap();
417        let engine = registry.create_engine().unwrap();
418
419        // Test basic content linting
420        let _violations = engine.lint_content("# Test\n", "test.md").unwrap();
421
422        // Test document linting
423        let document =
424            crate::Document::new("# Test".to_string(), PathBuf::from("test.md")).unwrap();
425        let _violations = engine.lint_document(&document).unwrap();
426    }
427}