1#[allow(dead_code)]
7#[derive(Debug, Clone, PartialEq)]
8pub enum PluginKind {
9 AssetLoader,
10 TargetProvider,
11 Exporter,
12 Validator,
13}
14
15#[allow(dead_code)]
16#[derive(Debug, Clone)]
17pub struct PluginDescriptor {
18 pub id: String,
19 pub name: String,
20 pub version: String,
22 pub kind: PluginKind,
23 pub supported_extensions: Vec<String>,
24 pub description: String,
25}
26
27#[allow(dead_code)]
28#[derive(Debug, Default)]
29pub struct PluginRegistry {
30 plugins: Vec<PluginDescriptor>,
31}
32
33impl PluginRegistry {
34 pub fn new() -> Self {
35 Self {
36 plugins: Vec::new(),
37 }
38 }
39
40 pub fn register(&mut self, desc: PluginDescriptor) -> Result<(), String> {
43 if self.plugins.iter().any(|p| p.id == desc.id) {
44 return Err(format!(
45 "plugin with id '{}' is already registered",
46 desc.id
47 ));
48 }
49 self.plugins.push(desc);
50 Ok(())
51 }
52
53 pub fn unregister(&mut self, id: &str) -> bool {
55 let before = self.plugins.len();
56 self.plugins.retain(|p| p.id != id);
57 self.plugins.len() < before
58 }
59
60 pub fn find_by_id(&self, id: &str) -> Option<&PluginDescriptor> {
62 self.plugins.iter().find(|p| p.id == id)
63 }
64
65 pub fn find_by_extension(&self, ext: &str) -> Vec<&PluginDescriptor> {
67 self.plugins
68 .iter()
69 .filter(|p| p.supported_extensions.iter().any(|e| e == ext))
70 .collect()
71 }
72
73 pub fn find_by_kind(&self, kind: &PluginKind) -> Vec<&PluginDescriptor> {
75 self.plugins.iter().filter(|p| &p.kind == kind).collect()
76 }
77
78 pub fn count(&self) -> usize {
80 self.plugins.len()
81 }
82
83 pub fn all(&self) -> &[PluginDescriptor] {
85 &self.plugins
86 }
87
88 pub fn to_json(&self) -> String {
90 let mut out = String::from("[\n");
91 for (i, p) in self.plugins.iter().enumerate() {
92 let kind_str = match p.kind {
93 PluginKind::AssetLoader => "AssetLoader",
94 PluginKind::TargetProvider => "TargetProvider",
95 PluginKind::Exporter => "Exporter",
96 PluginKind::Validator => "Validator",
97 };
98 let exts: Vec<String> = p
99 .supported_extensions
100 .iter()
101 .map(|e| format!("\"{}\"", e))
102 .collect();
103 out.push_str(&format!(
104 " {{\"id\":\"{}\",\"name\":\"{}\",\"version\":\"{}\",\"kind\":\"{}\",\"extensions\":[{}],\"description\":\"{}\"}}",
105 p.id, p.name, p.version, kind_str, exts.join(","), p.description
106 ));
107 if i + 1 < self.plugins.len() {
108 out.push(',');
109 }
110 out.push('\n');
111 }
112 out.push(']');
113 out
114 }
115}
116
117#[allow(dead_code)]
121pub fn default_builtin_plugins() -> Vec<PluginDescriptor> {
122 vec![
123 PluginDescriptor {
124 id: "obj_loader".to_string(),
125 name: "Wavefront OBJ Loader".to_string(),
126 version: "1.0.0".to_string(),
127 kind: PluginKind::AssetLoader,
128 supported_extensions: vec!["obj".to_string()],
129 description: "Loads Wavefront .obj mesh files".to_string(),
130 },
131 PluginDescriptor {
132 id: "glb_loader".to_string(),
133 name: "GLB Loader".to_string(),
134 version: "1.0.0".to_string(),
135 kind: PluginKind::AssetLoader,
136 supported_extensions: vec!["glb".to_string(), "gltf".to_string()],
137 description: "Loads binary or JSON glTF files".to_string(),
138 },
139 PluginDescriptor {
140 id: "target_loader".to_string(),
141 name: "MakeHuman Target Loader".to_string(),
142 version: "1.0.0".to_string(),
143 kind: PluginKind::TargetProvider,
144 supported_extensions: vec!["target".to_string()],
145 description: "Loads MakeHuman .target morph files".to_string(),
146 },
147 PluginDescriptor {
148 id: "glb_exporter".to_string(),
149 name: "GLB Exporter".to_string(),
150 version: "1.0.0".to_string(),
151 kind: PluginKind::Exporter,
152 supported_extensions: vec!["glb".to_string()],
153 description: "Exports meshes to binary glTF".to_string(),
154 },
155 PluginDescriptor {
156 id: "ply_exporter".to_string(),
157 name: "PLY Exporter".to_string(),
158 version: "1.0.0".to_string(),
159 kind: PluginKind::Exporter,
160 supported_extensions: vec!["ply".to_string()],
161 description: "Exports meshes to Stanford PLY format".to_string(),
162 },
163 PluginDescriptor {
164 id: "pack_validator".to_string(),
165 name: "Pack Validator".to_string(),
166 version: "1.0.0".to_string(),
167 kind: PluginKind::Validator,
168 supported_extensions: vec!["toml".to_string(), "json".to_string()],
169 description: "Validates OxiHuman asset pack manifests".to_string(),
170 },
171 ]
172}
173
174#[allow(dead_code)]
179pub fn parse_semver(s: &str) -> Option<(u32, u32, u32)> {
180 let parts: Vec<&str> = s.split('.').collect();
181 if parts.len() != 3 {
182 return None;
183 }
184 let major = parts[0].parse::<u32>().ok()?;
185 let minor = parts[1].parse::<u32>().ok()?;
186 let patch = parts[2].parse::<u32>().ok()?;
187 Some((major, minor, patch))
188}
189
190#[allow(dead_code)]
192pub fn semver_gte(a: (u32, u32, u32), b: (u32, u32, u32)) -> bool {
193 a >= b
194}
195
196#[cfg(test)]
199mod tests {
200 use super::*;
201
202 fn make_desc(id: &str, kind: PluginKind, exts: &[&str]) -> PluginDescriptor {
203 PluginDescriptor {
204 id: id.to_string(),
205 name: format!("Plugin {}", id),
206 version: "1.0.0".to_string(),
207 kind,
208 supported_extensions: exts.iter().map(|e| e.to_string()).collect(),
209 description: "test".to_string(),
210 }
211 }
212
213 #[test]
214 fn new_registry_is_empty() {
215 let reg = PluginRegistry::new();
216 assert_eq!(reg.count(), 0);
217 }
218
219 #[test]
220 fn register_success() {
221 let mut reg = PluginRegistry::new();
222 let desc = make_desc("my_loader", PluginKind::AssetLoader, &["obj"]);
223 assert!(reg.register(desc).is_ok());
224 assert_eq!(reg.count(), 1);
225 }
226
227 #[test]
228 fn duplicate_id_is_rejected() {
229 let mut reg = PluginRegistry::new();
230 reg.register(make_desc("dup", PluginKind::AssetLoader, &["obj"]))
231 .expect("should succeed");
232 let result = reg.register(make_desc("dup", PluginKind::Exporter, &["glb"]));
233 assert!(result.is_err());
234 assert!(result.unwrap_err().contains("dup"));
235 }
236
237 #[test]
238 fn unregister_removes_plugin() {
239 let mut reg = PluginRegistry::new();
240 reg.register(make_desc("to_remove", PluginKind::Validator, &[]))
241 .expect("should succeed");
242 assert_eq!(reg.count(), 1);
243 let removed = reg.unregister("to_remove");
244 assert!(removed);
245 assert_eq!(reg.count(), 0);
246 }
247
248 #[test]
249 fn unregister_nonexistent_returns_false() {
250 let mut reg = PluginRegistry::new();
251 assert!(!reg.unregister("nope"));
252 }
253
254 #[test]
255 fn find_by_id_found() {
256 let mut reg = PluginRegistry::new();
257 reg.register(make_desc("finder", PluginKind::AssetLoader, &["obj"]))
258 .expect("should succeed");
259 assert!(reg.find_by_id("finder").is_some());
260 }
261
262 #[test]
263 fn find_by_id_not_found() {
264 let reg = PluginRegistry::new();
265 assert!(reg.find_by_id("ghost").is_none());
266 }
267
268 #[test]
269 fn find_by_extension_obj() {
270 let mut reg = PluginRegistry::new();
271 reg.register(make_desc("obj_l", PluginKind::AssetLoader, &["obj"]))
272 .expect("should succeed");
273 reg.register(make_desc("glb_l", PluginKind::AssetLoader, &["glb"]))
274 .expect("should succeed");
275 let results = reg.find_by_extension("obj");
276 assert_eq!(results.len(), 1);
277 assert_eq!(results[0].id, "obj_l");
278 }
279
280 #[test]
281 fn find_by_kind_count() {
282 let mut reg = PluginRegistry::new();
283 reg.register(make_desc("l1", PluginKind::AssetLoader, &[]))
284 .expect("should succeed");
285 reg.register(make_desc("l2", PluginKind::AssetLoader, &[]))
286 .expect("should succeed");
287 reg.register(make_desc("e1", PluginKind::Exporter, &[]))
288 .expect("should succeed");
289 let loaders = reg.find_by_kind(&PluginKind::AssetLoader);
290 assert_eq!(loaders.len(), 2);
291 }
292
293 #[test]
294 fn count_returns_correct_value() {
295 let mut reg = PluginRegistry::new();
296 for i in 0..5 {
297 reg.register(make_desc(&format!("p{}", i), PluginKind::Validator, &[]))
298 .expect("should succeed");
299 }
300 assert_eq!(reg.count(), 5);
301 }
302
303 #[test]
304 fn to_json_contains_id() {
305 let mut reg = PluginRegistry::new();
306 reg.register(make_desc(
307 "json_test_plugin",
308 PluginKind::Exporter,
309 &["glb"],
310 ))
311 .expect("should succeed");
312 let json = reg.to_json();
313 assert!(json.contains("json_test_plugin"));
314 }
315
316 #[test]
317 fn default_builtin_plugins_has_six_or_more() {
318 let plugins = default_builtin_plugins();
319 assert!(plugins.len() >= 6);
320 }
321
322 #[test]
323 fn parse_semver_valid() {
324 assert_eq!(parse_semver("1.2.3"), Some((1, 2, 3)));
325 assert_eq!(parse_semver("0.0.0"), Some((0, 0, 0)));
326 assert_eq!(parse_semver("10.20.30"), Some((10, 20, 30)));
327 }
328
329 #[test]
330 fn parse_semver_invalid_returns_none() {
331 assert_eq!(parse_semver("1.2"), None);
332 assert_eq!(parse_semver("a.b.c"), None);
333 assert_eq!(parse_semver(""), None);
334 assert_eq!(parse_semver("1.2.3.4"), None);
335 }
336
337 #[test]
338 fn semver_gte_comparisons() {
339 assert!(semver_gte((1, 0, 0), (1, 0, 0)));
340 assert!(semver_gte((2, 0, 0), (1, 9, 9)));
341 assert!(semver_gte((1, 1, 0), (1, 0, 9)));
342 assert!(!semver_gte((1, 0, 0), (1, 0, 1)));
343 assert!(!semver_gte((0, 9, 9), (1, 0, 0)));
344 }
345
346 #[test]
347 fn all_returns_slice_of_plugins() {
348 let mut reg = PluginRegistry::new();
349 reg.register(make_desc("a1", PluginKind::AssetLoader, &[]))
350 .expect("should succeed");
351 reg.register(make_desc("a2", PluginKind::AssetLoader, &[]))
352 .expect("should succeed");
353 assert_eq!(reg.all().len(), 2);
354 }
355}