mdbook_lint_core/
engine.rs1use crate::error::Result;
4use crate::registry::RuleRegistry;
5use serde_json::Value;
6
7pub trait RuleProvider: Send + Sync {
9 fn provider_id(&self) -> &'static str;
11
12 fn description(&self) -> &'static str;
14
15 fn version(&self) -> &'static str;
17
18 fn register_rules(&self, registry: &mut RuleRegistry);
20
21 fn config_schema(&self) -> Option<Value> {
23 None
24 }
25
26 fn rule_ids(&self) -> Vec<&'static str> {
28 Vec::new()
29 }
30
31 fn initialize(&self) -> Result<()> {
33 Ok(())
34 }
35}
36
37#[derive(Default)]
39pub struct PluginRegistry {
40 providers: Vec<Box<dyn RuleProvider>>,
41}
42
43impl PluginRegistry {
44 pub fn new() -> Self {
46 Self {
47 providers: Vec::new(),
48 }
49 }
50
51 pub fn register_provider(&mut self, provider: Box<dyn RuleProvider>) -> Result<()> {
53 provider.initialize()?;
55
56 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 pub fn providers(&self) -> &[Box<dyn RuleProvider>] {
74 &self.providers
75 }
76
77 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 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 pub fn create_engine(&self) -> Result<LintEngine> {
98 let registry = self.create_rule_registry()?;
99 Ok(LintEngine::with_registry(registry))
100 }
101
102 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 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#[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
140pub struct LintEngine {
142 registry: RuleRegistry,
143}
144
145impl LintEngine {
146 pub fn new() -> Self {
148 Self {
149 registry: RuleRegistry::new(),
150 }
151 }
152
153 pub fn with_registry(registry: RuleRegistry) -> Self {
155 Self { registry }
156 }
157
158 pub fn registry(&self) -> &RuleRegistry {
160 &self.registry
161 }
162
163 pub fn registry_mut(&mut self) -> &mut RuleRegistry {
165 &mut self.registry
166 }
167
168 pub fn lint_document(&self, document: &crate::Document) -> Result<Vec<crate::Violation>> {
170 self.registry.check_document_optimized(document)
171 }
172
173 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 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 pub fn available_rules(&self) -> Vec<&'static str> {
191 self.registry.rule_ids()
192 }
193
194 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 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 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 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 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 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 let _violations = engine.lint_content("# Test\n", "test.md").unwrap();
421
422 let document =
424 crate::Document::new("# Test".to_string(), PathBuf::from("test.md")).unwrap();
425 let _violations = engine.lint_document(&document).unwrap();
426 }
427}