1use crate::SutureDriver;
2use std::collections::HashMap;
3use std::path::Path;
4use std::sync::Arc;
5
6pub trait DriverPlugin: Send + Sync {
7 fn name(&self) -> &str;
8 fn extensions(&self) -> &[&str];
9 fn description(&self) -> &str;
10 fn as_driver(&self) -> &dyn SutureDriver;
11}
12
13pub struct BuiltinDriverPlugin<D> {
14 name: &'static str,
15 extensions: Vec<&'static str>,
16 description: &'static str,
17 driver: D,
18}
19
20impl<D: SutureDriver + Send + Sync + 'static> BuiltinDriverPlugin<D> {
21 pub fn new(
22 name: &'static str,
23 extensions: Vec<&'static str>,
24 description: &'static str,
25 driver: D,
26 ) -> Self {
27 Self {
28 name,
29 extensions,
30 description,
31 driver,
32 }
33 }
34}
35
36impl<D: SutureDriver + Send + Sync + 'static> DriverPlugin for BuiltinDriverPlugin<D> {
37 fn name(&self) -> &str {
38 self.name
39 }
40 fn extensions(&self) -> &[&str] {
41 &self.extensions
42 }
43 fn description(&self) -> &str {
44 self.description
45 }
46 fn as_driver(&self) -> &dyn SutureDriver {
47 &self.driver
48 }
49}
50
51pub struct PluginRegistry {
52 plugins: HashMap<String, Arc<dyn DriverPlugin>>,
53 extension_map: HashMap<String, String>,
54}
55
56impl PluginRegistry {
57 pub fn new() -> Self {
58 Self {
59 plugins: HashMap::new(),
60 extension_map: HashMap::new(),
61 }
62 }
63
64 pub fn register(&mut self, plugin: Arc<dyn DriverPlugin>) {
65 let name = plugin.name().to_string();
66 for ext in plugin.extensions() {
67 self.extension_map.insert(ext.to_string(), name.clone());
68 }
69 self.plugins.insert(name, plugin);
70 }
71
72 pub fn get(&self, name: &str) -> Option<&dyn DriverPlugin> {
73 self.plugins.get(name).map(|p| p.as_ref())
74 }
75
76 pub fn get_by_extension(&self, ext: &str) -> Option<&dyn DriverPlugin> {
77 let normalized = if ext.starts_with('.') {
78 ext.to_string()
79 } else {
80 format!(".{}", ext)
81 };
82 self.extension_map
83 .get(&normalized)
84 .and_then(|name| self.plugins.get(name).map(|p| p.as_ref()))
85 }
86
87 pub fn list_drivers(&self) -> Vec<&str> {
88 let mut names: Vec<&str> = self.plugins.keys().map(|s| s.as_str()).collect();
89 names.sort();
90 names
91 }
92
93 pub fn discover_plugins(&mut self, plugin_dir: &Path) {
94 if !plugin_dir.exists() {
95 return;
96 }
97
98 if let Ok(entries) = std::fs::read_dir(plugin_dir) {
99 for entry in entries.flatten() {
100 let path = entry.path();
101 if path
102 .extension()
103 .map(|e| e == "suture-plugin")
104 .unwrap_or(false)
105 && let Ok(content) = std::fs::read_to_string(&path)
106 && let Some(desc) = Self::parse_plugin_descriptor(&content)
107 {
108 let _ = desc; }
110 }
111 }
112 }
113
114 fn parse_plugin_descriptor(content: &str) -> Option<PluginDescriptor> {
115 let mut name = None;
116 let mut extensions = Vec::new();
117 let mut description = String::new();
118
119 for line in content.lines() {
120 let line = line.trim();
121 if let Some(val) = line
122 .strip_prefix("name")
123 .and_then(Self::extract_string_value)
124 {
125 name = Some(val);
126 } else if let Some(start) = line.find('[') {
127 if let Some(end) = line[start..].find(']') {
128 let inner = &line[start + 1..start + end];
129 for ext in inner.split(',') {
130 let ext = ext.trim().trim_matches('"');
131 if !ext.is_empty() {
132 extensions.push(ext.to_string());
133 }
134 }
135 }
136 } else if let Some(val) = line
137 .strip_prefix("description")
138 .and_then(Self::extract_string_value)
139 {
140 description = val;
141 }
142 }
143
144 name.map(|name| PluginDescriptor {
145 name,
146 extensions,
147 description,
148 })
149 }
150
151 fn extract_string_value(line: &str) -> Option<String> {
152 if let Some(eq_pos) = line.find('=') {
153 let val = line[eq_pos + 1..].trim();
154 if val.starts_with('"') && val.ends_with('"') {
155 return Some(val[1..val.len() - 1].to_string());
156 }
157 }
158 None
159 }
160}
161
162#[allow(dead_code)]
163struct PluginDescriptor {
164 name: String,
165 extensions: Vec<String>,
166 #[allow(dead_code)]
167 description: String,
168}
169
170impl Default for PluginRegistry {
171 fn default() -> Self {
172 Self::new()
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use crate::DriverError;
180
181 struct MockDriver {
182 driver_name: &'static str,
183 driver_extensions: Vec<&'static str>,
184 }
185
186 impl MockDriver {
187 fn new(name: &'static str, extensions: Vec<&'static str>) -> Self {
188 Self {
189 driver_name: name,
190 driver_extensions: extensions,
191 }
192 }
193 }
194
195 impl SutureDriver for MockDriver {
196 fn name(&self) -> &str {
197 self.driver_name
198 }
199 fn supported_extensions(&self) -> &[&str] {
200 &self.driver_extensions
201 }
202 fn diff(
203 &self,
204 _base_content: Option<&str>,
205 _new_content: &str,
206 ) -> Result<Vec<crate::SemanticChange>, DriverError> {
207 Ok(vec![])
208 }
209 fn format_diff(
210 &self,
211 _base_content: Option<&str>,
212 _new_content: &str,
213 ) -> Result<String, DriverError> {
214 Ok(String::new())
215 }
216 }
217
218 fn make_plugin(name: &'static str, extensions: Vec<&'static str>) -> Arc<dyn DriverPlugin> {
219 Arc::new(BuiltinDriverPlugin::new(
220 name,
221 extensions.clone(),
222 "test driver",
223 MockDriver::new(name, extensions),
224 ))
225 }
226
227 #[test]
228 fn register_and_get_by_name() {
229 let mut reg = PluginRegistry::new();
230 reg.register(make_plugin("json", vec![".json"]));
231 assert!(reg.get("json").is_some());
232 assert!(reg.get("yaml").is_none());
233 assert_eq!(reg.get("json").unwrap().name(), "json");
234 }
235
236 #[test]
237 fn get_by_extension_with_dot() {
238 let mut reg = PluginRegistry::new();
239 reg.register(make_plugin("json", vec![".json"]));
240 assert!(reg.get_by_extension(".json").is_some());
241 assert!(reg.get_by_extension(".yaml").is_none());
242 }
243
244 #[test]
245 fn get_by_extension_without_dot() {
246 let mut reg = PluginRegistry::new();
247 reg.register(make_plugin("yaml", vec![".yaml", ".yml"]));
248 assert!(reg.get_by_extension("yaml").is_some());
249 assert!(reg.get_by_extension("yml").is_some());
250 }
251
252 #[test]
253 fn list_drivers_sorted() {
254 let mut reg = PluginRegistry::new();
255 reg.register(make_plugin("csv", vec![".csv"]));
256 reg.register(make_plugin("xml", vec![".xml"]));
257 reg.register(make_plugin("json", vec![".json"]));
258 assert_eq!(reg.list_drivers(), vec!["csv", "json", "xml"]);
259 }
260
261 #[test]
262 fn discover_plugins_nonexistent_dir() {
263 let mut reg = PluginRegistry::new();
264 reg.discover_plugins(Path::new("/tmp/suture-test-nonexistent-12345"));
265 assert!(reg.list_drivers().is_empty());
266 }
267
268 #[test]
269 fn parse_plugin_descriptor_valid() {
270 let content = r#"
271name = "my-driver"
272extensions = [".custom", ".ext"]
273description = "A custom driver"
274"#;
275 let desc = PluginRegistry::parse_plugin_descriptor(content).unwrap();
276 assert_eq!(desc.name, "my-driver");
277 assert_eq!(desc.extensions, vec![".custom", ".ext"]);
278 assert_eq!(desc.description, "A custom driver");
279 }
280
281 #[test]
282 fn parse_plugin_descriptor_missing_name() {
283 let content = r#"extensions = [".custom"]"#;
284 assert!(PluginRegistry::parse_plugin_descriptor(content).is_none());
285 }
286
287 #[test]
288 fn as_driver_returns_underlying_driver() {
289 let mut reg = PluginRegistry::new();
290 reg.register(make_plugin("json", vec![".json"]));
291 let plugin = reg.get("json").unwrap();
292 assert_eq!(plugin.as_driver().name(), "json");
293 assert_eq!(plugin.as_driver().supported_extensions(), &[".json"]);
294 }
295}