mdbook_lint_core/
engine.rs1use crate::config::Config;
4use crate::error::Result;
5use crate::registry::RuleRegistry;
6use serde_json::Value;
7
8pub trait RuleProvider: Send + Sync {
10 fn provider_id(&self) -> &'static str;
12
13 fn description(&self) -> &'static str;
15
16 fn version(&self) -> &'static str;
18
19 fn register_rules(&self, registry: &mut RuleRegistry);
21
22 fn config_schema(&self) -> Option<Value> {
24 None
25 }
26
27 fn rule_ids(&self) -> Vec<&'static str> {
29 Vec::new()
30 }
31
32 fn initialize(&self) -> Result<()> {
34 Ok(())
35 }
36
37 fn register_rules_with_config(&self, registry: &mut RuleRegistry, _config: Option<&Config>) {
41 self.register_rules(registry);
43 }
44}
45
46#[derive(Default)]
48pub struct PluginRegistry {
49 providers: Vec<Box<dyn RuleProvider>>,
50}
51
52impl PluginRegistry {
53 pub fn new() -> Self {
55 Self {
56 providers: Vec::new(),
57 }
58 }
59
60 pub fn register_provider(&mut self, provider: Box<dyn RuleProvider>) -> Result<()> {
62 provider.initialize()?;
64
65 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 pub fn providers(&self) -> &[Box<dyn RuleProvider>] {
83 &self.providers
84 }
85
86 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 pub fn create_rule_registry(&self) -> Result<RuleRegistry> {
96 self.create_rule_registry_with_config(None)
97 }
98
99 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 pub fn create_engine(&self) -> Result<LintEngine> {
115 self.create_engine_with_config(None)
116 }
117
118 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 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 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#[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
162pub struct LintEngine {
164 registry: RuleRegistry,
165}
166
167impl LintEngine {
168 pub fn new() -> Self {
170 Self {
171 registry: RuleRegistry::new(),
172 }
173 }
174
175 pub fn with_registry(registry: RuleRegistry) -> Self {
177 Self { registry }
178 }
179
180 pub fn registry(&self) -> &RuleRegistry {
182 &self.registry
183 }
184
185 pub fn registry_mut(&mut self) -> &mut RuleRegistry {
187 &mut self.registry
188 }
189
190 pub fn lint_document(&self, document: &crate::Document) -> Result<Vec<crate::Violation>> {
192 self.registry.check_document_optimized(document)
193 }
194
195 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 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 pub fn available_rules(&self) -> Vec<&'static str> {
213 self.registry.rule_ids()
214 }
215
216 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 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 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 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 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 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 let _violations = engine.lint_content("# Test\n", "test.md").unwrap();
443
444 let document =
446 crate::Document::new("# Test".to_string(), PathBuf::from("test.md")).unwrap();
447 let _violations = engine.lint_document(&document).unwrap();
448 }
449}