Skip to main content

modde_core/
plugin.rs

1use std::path::{Path, PathBuf};
2
3use serde::{Deserialize, Serialize};
4
5/// Plugin manifest (TOML format).
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct PluginManifest {
8    pub name: String,
9    pub version: String,
10    pub description: String,
11    pub author: String,
12    /// What this plugin provides.
13    pub provides: Vec<PluginCapability>,
14}
15
16/// What a plugin can provide.
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
18pub enum PluginCapability {
19    /// Additional game support.
20    Game { game_id: String },
21    /// Additional installer format.
22    Installer { format: String },
23    /// Additional diagnostic rules.
24    Diagnostic,
25}
26
27/// Load a plugin manifest from a TOML file.
28pub fn load_manifest(path: &Path) -> crate::error::Result<PluginManifest> {
29    let content = std::fs::read_to_string(path)?;
30    let manifest: PluginManifest = toml::from_str(&content)?;
31    Ok(manifest)
32}
33
34/// Scan the plugins directory for plugin manifests.
35#[must_use]
36pub fn scan_plugins() -> Vec<(PathBuf, PluginManifest)> {
37    let plugin_dir = crate::paths::data_dir().join("plugins");
38    if !plugin_dir.exists() {
39        return Vec::new();
40    }
41
42    let mut plugins = Vec::new();
43    if let Ok(entries) = std::fs::read_dir(&plugin_dir) {
44        for entry in entries.flatten() {
45            let manifest_path = entry.path().join("plugin.toml");
46            if manifest_path.exists()
47                && let Ok(manifest) = load_manifest(&manifest_path)
48            {
49                plugins.push((entry.path(), manifest));
50            }
51        }
52    }
53    plugins
54}
55
56/// List all registered plugin capabilities.
57#[must_use]
58pub fn list_capabilities() -> Vec<(String, PluginCapability)> {
59    scan_plugins()
60        .into_iter()
61        .flat_map(|(_, manifest)| {
62            let name = manifest.name.clone();
63            manifest
64                .provides
65                .into_iter()
66                .map(move |cap| (name.clone(), cap))
67        })
68        .collect()
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn test_parse_manifest() {
77        let toml_str = r#"
78name = "skyrim-support"
79version = "1.0.0"
80description = "Adds Skyrim SE game support"
81author = "modde-contrib"
82
83[[provides]]
84Game = { game_id = "skyrim-se" }
85
86[[provides]]
87Diagnostic = {}
88"#;
89        let manifest: PluginManifest = toml::from_str(toml_str).unwrap();
90        assert_eq!(manifest.name, "skyrim-support");
91        assert_eq!(manifest.version, "1.0.0");
92        assert_eq!(manifest.description, "Adds Skyrim SE game support");
93        assert_eq!(manifest.author, "modde-contrib");
94        assert_eq!(manifest.provides.len(), 2);
95    }
96
97    #[test]
98    fn test_plugin_capabilities() {
99        let game_cap = PluginCapability::Game {
100            game_id: "cyberpunk2077".to_string(),
101        };
102        let installer_cap = PluginCapability::Installer {
103            format: "fomod".to_string(),
104        };
105        let diag_cap = PluginCapability::Diagnostic;
106
107        // Verify equality
108        assert_eq!(
109            game_cap,
110            PluginCapability::Game {
111                game_id: "cyberpunk2077".to_string()
112            }
113        );
114        assert_ne!(game_cap, diag_cap);
115        assert_ne!(installer_cap, diag_cap);
116
117        // Verify round-trip serialization
118        let toml_str = r#"
119name = "test"
120version = "0.1.0"
121description = "test plugin"
122author = "test"
123
124[[provides]]
125Game = { game_id = "cyberpunk2077" }
126
127[[provides]]
128Installer = { format = "fomod" }
129
130[[provides]]
131Diagnostic = {}
132"#;
133        let manifest: PluginManifest = toml::from_str(toml_str).unwrap();
134        assert_eq!(manifest.provides.len(), 3);
135        assert_eq!(manifest.provides[0], game_cap);
136        assert_eq!(manifest.provides[1], installer_cap);
137        assert_eq!(manifest.provides[2], diag_cap);
138    }
139}