mockforge_plugin_core/manifest/
models.rs

1//! Core plugin manifest data models
2//!
3//! This module defines the fundamental data structures for plugin manifests,
4//! including the main PluginManifest struct and related types.
5
6use crate::{PluginCapabilities, PluginError, PluginId, PluginVersion, Result};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10use super::schema::ConfigSchema;
11use semver;
12
13/// Plugin manifest structure
14///
15/// The manifest contains all metadata about a plugin, including its capabilities,
16/// dependencies, and configuration requirements.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct PluginManifest {
19    /// Plugin manifest format version
20    pub manifest_version: String,
21    /// Plugin basic information
22    pub plugin: PluginInfo,
23    /// Plugin capabilities and permissions
24    pub capabilities: PluginCapabilities,
25    /// Plugin dependencies
26    pub dependencies: Vec<PluginDependency>,
27    /// Plugin configuration schema
28    pub config_schema: Option<ConfigSchema>,
29    /// Plugin metadata
30    pub metadata: HashMap<String, serde_json::Value>,
31}
32
33impl PluginManifest {
34    /// Create a new plugin manifest
35    pub fn new(plugin: PluginInfo) -> Self {
36        Self {
37            manifest_version: "1.0".to_string(),
38            plugin,
39            capabilities: PluginCapabilities::default(),
40            dependencies: Vec::new(),
41            config_schema: None,
42            metadata: HashMap::new(),
43        }
44    }
45
46    /// Validate manifest
47    pub fn validate(&self) -> Result<()> {
48        // Validate manifest version
49        if self.manifest_version != "1.0" {
50            return Err(PluginError::config_error(&format!(
51                "Unsupported manifest version: {}",
52                self.manifest_version
53            )));
54        }
55
56        // Validate plugin info
57        self.plugin.validate()?;
58
59        // Validate dependencies
60        for dep in &self.dependencies {
61            dep.validate()?;
62        }
63
64        // Validate config schema if present
65        if let Some(schema) = &self.config_schema {
66            schema.validate()?;
67        }
68
69        Ok(())
70    }
71
72    /// Get plugin ID
73    pub fn id(&self) -> &PluginId {
74        &self.plugin.id
75    }
76
77    /// Get plugin version
78    pub fn version(&self) -> &PluginVersion {
79        &self.plugin.version
80    }
81
82    /// Check if plugin supports a specific type
83    pub fn supports_type(&self, plugin_type: &str) -> bool {
84        self.plugin.types.contains(&plugin_type.to_string())
85    }
86
87    /// Get plugin display name
88    pub fn display_name(&self) -> &str {
89        &self.plugin.name
90    }
91
92    /// Get plugin description
93    pub fn description(&self) -> Option<&str> {
94        self.plugin.description.as_deref()
95    }
96
97    /// Get plugin author
98    pub fn author(&self) -> Option<&PluginAuthor> {
99        self.plugin.author.as_ref()
100    }
101
102    /// Check if plugin has a specific capability
103    pub fn has_capability(&self, capability: &str) -> bool {
104        self.capabilities.has_capability(capability)
105    }
106
107    /// Get all plugin types
108    pub fn types(&self) -> &[String] {
109        &self.plugin.types
110    }
111
112    /// Get plugin dependencies
113    pub fn dependencies(&self) -> &[PluginDependency] {
114        &self.dependencies
115    }
116
117    /// Check if plugin requires configuration
118    pub fn requires_config(&self) -> bool {
119        self.config_schema.is_some()
120    }
121}
122
123/// Plugin basic information
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct PluginInfo {
126    /// Unique plugin identifier
127    pub id: PluginId,
128    /// Plugin name (display name)
129    pub name: String,
130    /// Plugin version
131    pub version: PluginVersion,
132    /// Plugin description
133    pub description: Option<String>,
134    /// Plugin author information
135    pub author: Option<PluginAuthor>,
136    /// Supported plugin types
137    pub types: Vec<String>,
138    /// Homepage URL
139    pub homepage: Option<String>,
140    /// Repository URL
141    pub repository: Option<String>,
142    /// License
143    pub license: Option<String>,
144    /// Keywords for plugin discovery
145    pub keywords: Vec<String>,
146}
147
148impl PluginInfo {
149    /// Create new plugin info
150    pub fn new(id: PluginId, name: String, version: PluginVersion) -> Self {
151        Self {
152            id,
153            name,
154            version,
155            description: None,
156            author: None,
157            types: Vec::new(),
158            homepage: None,
159            repository: None,
160            license: None,
161            keywords: Vec::new(),
162        }
163    }
164
165    /// Validate plugin info
166    pub fn validate(&self) -> Result<()> {
167        if self.name.trim().is_empty() {
168            return Err(PluginError::config_error("Plugin name cannot be empty"));
169        }
170
171        if self.types.is_empty() {
172            return Err(PluginError::config_error("Plugin must specify at least one type"));
173        }
174
175        for plugin_type in &self.types {
176            if plugin_type.trim().is_empty() {
177                return Err(PluginError::config_error("Plugin type cannot be empty"));
178            }
179        }
180
181        Ok(())
182    }
183
184    /// Check if plugin matches keywords
185    pub fn matches_keywords(&self, keywords: &[String]) -> bool {
186        if keywords.is_empty() {
187            return true;
188        }
189
190        keywords.iter().any(|keyword| {
191            self.keywords.iter().any(|plugin_keyword| {
192                plugin_keyword.to_lowercase().contains(&keyword.to_lowercase())
193            })
194        })
195    }
196}
197
198/// Plugin author information
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct PluginAuthor {
201    /// Author name
202    pub name: String,
203    /// Author email
204    pub email: Option<String>,
205    /// Author homepage
206    pub url: Option<String>,
207}
208
209impl PluginAuthor {
210    /// Create new author
211    pub fn new(name: String) -> Self {
212        Self {
213            name,
214            email: None,
215            url: None,
216        }
217    }
218
219    /// Validate author info
220    pub fn validate(&self) -> Result<()> {
221        if self.name.trim().is_empty() {
222            return Err(PluginError::config_error("Author name cannot be empty"));
223        }
224        Ok(())
225    }
226}
227
228/// Plugin dependency specification
229#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct PluginDependency {
231    /// Dependency plugin ID
232    pub id: PluginId,
233    /// Version requirement (semver)
234    pub version: String,
235    /// Whether this dependency is optional
236    pub optional: bool,
237}
238
239impl PluginDependency {
240    /// Create new dependency
241    pub fn new(id: PluginId, version: String) -> Self {
242        Self {
243            id,
244            version,
245            optional: false,
246        }
247    }
248
249    /// Create optional dependency
250    pub fn optional(id: PluginId, version: String) -> Self {
251        Self {
252            id,
253            version,
254            optional: true,
255        }
256    }
257
258    /// Validate dependency
259    pub fn validate(&self) -> Result<()> {
260        if self.version.trim().is_empty() {
261            return Err(PluginError::config_error(&format!(
262                "Dependency {} version cannot be empty",
263                self.id
264            )));
265        }
266        Ok(())
267    }
268
269    /// Check if version requirement is satisfied
270    pub fn satisfies_version(&self, version: &PluginVersion) -> bool {
271        // Handle wildcard
272        if self.version == "*" {
273            return true;
274        }
275
276        // For plugin dependencies, treat bare versions as exact matches
277        // Prepend "=" if the version doesn't start with a comparator
278        let req_str = if self.version.starts_with(|c: char| c.is_ascii_digit()) {
279            format!("={}", self.version)
280        } else {
281            self.version.clone()
282        };
283
284        // Parse version requirement
285        let req = match semver::VersionReq::parse(&req_str) {
286            Ok(req) => req,
287            Err(_) => return false, // Invalid requirement
288        };
289
290        // Convert PluginVersion to semver::Version
291        let semver_version = match version.to_semver() {
292            Ok(v) => v,
293            Err(_) => return false, // Invalid version
294        };
295
296        req.matches(&semver_version)
297    }
298}
299
300#[cfg(test)]
301mod tests {
302
303    #[test]
304    fn test_module_compiles() {
305        // Basic compilation test
306    }
307}