mdbook_lint_core/
engine.rs

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