Skip to main content

suture_driver/
plugin.rs

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; // plugin descriptor found; future: dynamic loading
109                }
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}