Skip to main content

oxihuman_core/
plugin_api.rs

1//! Plugin registration and lifecycle API.
2
3#[allow(dead_code)]
4#[derive(Clone, PartialEq, Debug)]
5pub enum PluginState {
6    Unloaded,
7    Loaded,
8    Active,
9    Error(String),
10}
11
12#[allow(dead_code)]
13pub struct PluginMetadata {
14    pub id: String,
15    pub name: String,
16    pub version: [u32; 3], // major, minor, patch
17    pub author: String,
18    pub description: String,
19    pub dependencies: Vec<String>,
20}
21
22#[allow(dead_code)]
23pub struct Plugin {
24    pub metadata: PluginMetadata,
25    pub state: PluginState,
26    pub load_order: u32,
27}
28
29#[allow(dead_code)]
30pub struct PluginApiRegistry {
31    pub plugins: Vec<Plugin>,
32    pub next_order: u32,
33}
34
35#[allow(dead_code)]
36pub fn new_registry() -> PluginApiRegistry {
37    PluginApiRegistry {
38        plugins: Vec::new(),
39        next_order: 0,
40    }
41}
42
43#[allow(dead_code)]
44pub fn register_plugin(registry: &mut PluginApiRegistry, meta: PluginMetadata) -> usize {
45    let order = registry.next_order;
46    registry.next_order += 1;
47    let plugin = Plugin {
48        metadata: meta,
49        state: PluginState::Loaded,
50        load_order: order,
51    };
52    registry.plugins.push(plugin);
53    registry.plugins.len() - 1
54}
55
56#[allow(dead_code)]
57pub fn get_plugin<'a>(registry: &'a PluginApiRegistry, id: &str) -> Option<&'a Plugin> {
58    registry.plugins.iter().find(|p| p.metadata.id == id)
59}
60
61#[allow(dead_code)]
62pub fn activate_plugin(registry: &mut PluginApiRegistry, id: &str) -> bool {
63    if let Some(p) = registry.plugins.iter_mut().find(|p| p.metadata.id == id) {
64        match p.state {
65            PluginState::Loaded | PluginState::Unloaded => {
66                p.state = PluginState::Active;
67                true
68            }
69            _ => false,
70        }
71    } else {
72        false
73    }
74}
75
76#[allow(dead_code)]
77pub fn deactivate_plugin(registry: &mut PluginApiRegistry, id: &str) -> bool {
78    if let Some(p) = registry.plugins.iter_mut().find(|p| p.metadata.id == id) {
79        if p.state == PluginState::Active {
80            p.state = PluginState::Loaded;
81            true
82        } else {
83            false
84        }
85    } else {
86        false
87    }
88}
89
90#[allow(dead_code)]
91pub fn unload_plugin(registry: &mut PluginApiRegistry, id: &str) -> bool {
92    if let Some(p) = registry.plugins.iter_mut().find(|p| p.metadata.id == id) {
93        p.state = PluginState::Unloaded;
94        true
95    } else {
96        false
97    }
98}
99
100#[allow(dead_code)]
101pub fn set_plugin_error(registry: &mut PluginApiRegistry, id: &str, msg: &str) {
102    if let Some(p) = registry.plugins.iter_mut().find(|p| p.metadata.id == id) {
103        p.state = PluginState::Error(msg.to_string());
104    }
105}
106
107#[allow(dead_code)]
108pub fn active_plugins(registry: &PluginApiRegistry) -> Vec<&Plugin> {
109    registry
110        .plugins
111        .iter()
112        .filter(|p| p.state == PluginState::Active)
113        .collect()
114}
115
116#[allow(dead_code)]
117pub fn plugin_count(registry: &PluginApiRegistry) -> usize {
118    registry.plugins.len()
119}
120
121#[allow(dead_code)]
122pub fn has_dependency(registry: &PluginApiRegistry, plugin_id: &str, dep_id: &str) -> bool {
123    if let Some(p) = get_plugin(registry, plugin_id) {
124        p.metadata.dependencies.iter().any(|d| d == dep_id)
125    } else {
126        false
127    }
128}
129
130/// Topological sort of plugins by dependencies (Kahn's algorithm).
131#[allow(dead_code)]
132pub fn dependency_order(registry: &PluginApiRegistry) -> Vec<&str> {
133    let n = registry.plugins.len();
134    // Build adjacency: dep → dependent
135    let mut in_degree = vec![0usize; n];
136    let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
137
138    for (i, plugin) in registry.plugins.iter().enumerate() {
139        for dep in &plugin.metadata.dependencies {
140            if let Some(j) = registry.plugins.iter().position(|p| &p.metadata.id == dep) {
141                adj[j].push(i);
142                in_degree[i] += 1;
143            }
144        }
145    }
146
147    let mut queue: Vec<usize> = (0..n).filter(|&i| in_degree[i] == 0).collect();
148    let mut result = Vec::new();
149
150    while !queue.is_empty() {
151        let node = queue.remove(0);
152        result.push(registry.plugins[node].metadata.id.as_str());
153        for &next in &adj[node] {
154            in_degree[next] -= 1;
155            if in_degree[next] == 0 {
156                queue.push(next);
157            }
158        }
159    }
160
161    result
162}
163
164#[allow(dead_code)]
165pub fn plugin_version_string(plugin: &Plugin) -> String {
166    let [major, minor, patch] = plugin.metadata.version;
167    format!("{}.{}.{}", major, minor, patch)
168}
169
170#[allow(dead_code)]
171pub fn check_dependencies_met(registry: &PluginApiRegistry, plugin_id: &str) -> bool {
172    if let Some(plugin) = get_plugin(registry, plugin_id) {
173        plugin.metadata.dependencies.iter().all(|dep| {
174            if let Some(dep_plugin) = get_plugin(registry, dep) {
175                dep_plugin.state == PluginState::Active || dep_plugin.state == PluginState::Loaded
176            } else {
177                false
178            }
179        })
180    } else {
181        false
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    fn make_meta(id: &str, deps: Vec<&str>) -> PluginMetadata {
190        PluginMetadata {
191            id: id.to_string(),
192            name: format!("Plugin {}", id),
193            version: [1, 0, 0],
194            author: "test".to_string(),
195            description: "test plugin".to_string(),
196            dependencies: deps.into_iter().map(|s| s.to_string()).collect(),
197        }
198    }
199
200    #[test]
201    fn test_new_registry_empty() {
202        let reg = new_registry();
203        assert_eq!(reg.plugins.len(), 0);
204        assert_eq!(reg.next_order, 0);
205    }
206
207    #[test]
208    fn test_register_plugin() {
209        let mut reg = new_registry();
210        let idx = register_plugin(&mut reg, make_meta("foo", vec![]));
211        assert_eq!(idx, 0);
212        assert_eq!(reg.plugins.len(), 1);
213        assert_eq!(reg.plugins[0].metadata.id, "foo");
214        assert_eq!(reg.plugins[0].state, PluginState::Loaded);
215    }
216
217    #[test]
218    fn test_get_plugin_found() {
219        let mut reg = new_registry();
220        register_plugin(&mut reg, make_meta("bar", vec![]));
221        let p = get_plugin(&reg, "bar");
222        assert!(p.is_some());
223        assert_eq!(p.expect("should succeed").metadata.id, "bar");
224    }
225
226    #[test]
227    fn test_get_plugin_not_found() {
228        let reg = new_registry();
229        assert!(get_plugin(&reg, "missing").is_none());
230    }
231
232    #[test]
233    fn test_activate_plugin() {
234        let mut reg = new_registry();
235        register_plugin(&mut reg, make_meta("baz", vec![]));
236        assert!(activate_plugin(&mut reg, "baz"));
237        assert_eq!(
238            get_plugin(&reg, "baz").expect("should succeed").state,
239            PluginState::Active
240        );
241    }
242
243    #[test]
244    fn test_activate_plugin_missing() {
245        let mut reg = new_registry();
246        assert!(!activate_plugin(&mut reg, "ghost"));
247    }
248
249    #[test]
250    fn test_deactivate_plugin() {
251        let mut reg = new_registry();
252        register_plugin(&mut reg, make_meta("qux", vec![]));
253        activate_plugin(&mut reg, "qux");
254        assert!(deactivate_plugin(&mut reg, "qux"));
255        assert_eq!(
256            get_plugin(&reg, "qux").expect("should succeed").state,
257            PluginState::Loaded
258        );
259    }
260
261    #[test]
262    fn test_active_plugins_list() {
263        let mut reg = new_registry();
264        register_plugin(&mut reg, make_meta("a", vec![]));
265        register_plugin(&mut reg, make_meta("b", vec![]));
266        activate_plugin(&mut reg, "a");
267        let active = active_plugins(&reg);
268        assert_eq!(active.len(), 1);
269        assert_eq!(active[0].metadata.id, "a");
270    }
271
272    #[test]
273    fn test_set_plugin_error() {
274        let mut reg = new_registry();
275        register_plugin(&mut reg, make_meta("err_plugin", vec![]));
276        set_plugin_error(&mut reg, "err_plugin", "init failed");
277        let p = get_plugin(&reg, "err_plugin").expect("should succeed");
278        assert!(matches!(&p.state, PluginState::Error(msg) if msg == "init failed"));
279    }
280
281    #[test]
282    fn test_plugin_version_string() {
283        let mut reg = new_registry();
284        register_plugin(
285            &mut reg,
286            PluginMetadata {
287                id: "ver".to_string(),
288                name: "Ver".to_string(),
289                version: [2, 3, 4],
290                author: "test".to_string(),
291                description: "".to_string(),
292                dependencies: vec![],
293            },
294        );
295        let p = get_plugin(&reg, "ver").expect("should succeed");
296        assert_eq!(plugin_version_string(p), "2.3.4");
297    }
298
299    #[test]
300    fn test_has_dependency_true() {
301        let mut reg = new_registry();
302        register_plugin(&mut reg, make_meta("dep_a", vec![]));
303        register_plugin(&mut reg, make_meta("dep_b", vec!["dep_a"]));
304        assert!(has_dependency(&reg, "dep_b", "dep_a"));
305    }
306
307    #[test]
308    fn test_has_dependency_false() {
309        let mut reg = new_registry();
310        register_plugin(&mut reg, make_meta("solo", vec![]));
311        assert!(!has_dependency(&reg, "solo", "nonexistent"));
312    }
313
314    #[test]
315    fn test_dependency_order() {
316        let mut reg = new_registry();
317        register_plugin(&mut reg, make_meta("base", vec![]));
318        register_plugin(&mut reg, make_meta("mid", vec!["base"]));
319        register_plugin(&mut reg, make_meta("top", vec!["mid"]));
320        let order = dependency_order(&reg);
321        assert_eq!(order.len(), 3);
322        let base_pos = order
323            .iter()
324            .position(|&s| s == "base")
325            .expect("should succeed");
326        let mid_pos = order
327            .iter()
328            .position(|&s| s == "mid")
329            .expect("should succeed");
330        let top_pos = order
331            .iter()
332            .position(|&s| s == "top")
333            .expect("should succeed");
334        assert!(base_pos < mid_pos);
335        assert!(mid_pos < top_pos);
336    }
337
338    #[test]
339    fn test_check_dependencies_met() {
340        let mut reg = new_registry();
341        register_plugin(&mut reg, make_meta("lib", vec![]));
342        register_plugin(&mut reg, make_meta("app", vec!["lib"]));
343        // lib is Loaded (not just unloaded), so deps are met
344        assert!(check_dependencies_met(&reg, "app"));
345    }
346
347    #[test]
348    fn test_plugin_count() {
349        let mut reg = new_registry();
350        register_plugin(&mut reg, make_meta("x", vec![]));
351        register_plugin(&mut reg, make_meta("y", vec![]));
352        assert_eq!(plugin_count(&reg), 2);
353    }
354}