1use schemars::JsonSchema;
2use semver::Version;
3use serde::{Deserialize, Serialize};
4
5use super::command::Namespace;
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
9pub struct CompatibilityRange {
10 pub min_inclusive: String,
12 pub max_exclusive: Option<String>,
14}
15
16impl CompatibilityRange {
17 pub fn new(min_inclusive: &str, max_exclusive: Option<&str>) -> Result<Self, String> {
19 let _ = Version::parse(min_inclusive)
20 .map_err(|error| format!("invalid min_inclusive semver: {error}"))?;
21 if let Some(max) = max_exclusive {
22 let _ = Version::parse(max)
23 .map_err(|error| format!("invalid max_exclusive semver: {error}"))?;
24 }
25 Ok(Self {
26 min_inclusive: min_inclusive.to_string(),
27 max_exclusive: max_exclusive.map(ToString::to_string),
28 })
29 }
30
31 pub fn supports_host(&self, host_version: &str) -> Result<bool, String> {
33 let host = Version::parse(host_version)
34 .map_err(|error| format!("invalid host semver: {error}"))?;
35 let min = Version::parse(&self.min_inclusive)
36 .map_err(|error| format!("invalid min_inclusive semver: {error}"))?;
37 if host < min {
38 return Ok(false);
39 }
40 if let Some(max) = &self.max_exclusive {
41 let max = Version::parse(max)
42 .map_err(|error| format!("invalid max_exclusive semver: {error}"))?;
43 return Ok(host < max);
44 }
45 Ok(true)
46 }
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
51pub struct PluginCapability {
52 pub name: String,
54 pub version: Option<String>,
56}
57
58impl PluginCapability {
59 pub fn new(name: &str, version: Option<&str>) -> Result<Self, String> {
61 if name.trim().is_empty() {
62 return Err("capability name cannot be empty".to_string());
63 }
64 Ok(Self { name: name.to_string(), version: version.map(ToString::to_string) })
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
70#[serde(rename_all = "kebab-case")]
71#[non_exhaustive]
72pub enum PluginKind {
73 Native,
75 #[default]
77 Delegated,
78 Python,
80 ExternalExec,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
86#[serde(rename_all = "lowercase")]
87pub enum PluginTrustClass {
88 Core,
90 Verified,
92 Community,
94 Unknown,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
100#[serde(rename_all = "lowercase")]
101#[non_exhaustive]
102pub enum PluginLifecycleState {
103 Discovered,
105 Validated,
107 Installed,
109 Enabled,
111 Disabled,
113 Broken,
115 Incompatible,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
121pub struct PluginManifestV2 {
122 pub name: String,
124 pub version: String,
126 pub schema_version: String,
128 pub manifest_version: String,
130 pub compatibility: CompatibilityRange,
132 pub namespace: Namespace,
134 #[serde(default)]
136 pub kind: PluginKind,
137 pub trust_class: PluginTrustClass,
139 #[serde(default)]
141 pub aliases: Vec<String>,
142 pub entrypoint: String,
144 pub capabilities: Vec<PluginCapability>,
146}
147
148impl PluginManifestV2 {
149 #[allow(clippy::too_many_arguments)]
151 pub fn new(
152 name: &str,
153 version: &str,
154 schema_version: &str,
155 manifest_version: &str,
156 compatibility: CompatibilityRange,
157 namespace: Namespace,
158 kind: PluginKind,
159 trust_class: PluginTrustClass,
160 aliases: Vec<String>,
161 entrypoint: &str,
162 capabilities: Vec<PluginCapability>,
163 ) -> Result<Self, String> {
164 if name.trim().is_empty() {
165 return Err("plugin name cannot be empty".to_string());
166 }
167 if version.trim().is_empty() {
168 return Err("plugin version cannot be empty".to_string());
169 }
170 if schema_version.trim().is_empty() {
171 return Err("plugin schema_version cannot be empty".to_string());
172 }
173 if schema_version != "v2" {
174 return Err("plugin schema_version must be v2".to_string());
175 }
176 if manifest_version.trim().is_empty() {
177 return Err("plugin manifest_version cannot be empty".to_string());
178 }
179 if manifest_version != "v2" {
180 return Err("plugin manifest_version must be v2".to_string());
181 }
182 if entrypoint.trim().is_empty() {
183 return Err("plugin entrypoint cannot be empty".to_string());
184 }
185 Ok(Self {
186 name: name.to_string(),
187 version: version.to_string(),
188 schema_version: schema_version.to_string(),
189 manifest_version: manifest_version.to_string(),
190 compatibility,
191 namespace,
192 kind,
193 trust_class,
194 aliases,
195 entrypoint: entrypoint.to_string(),
196 capabilities,
197 })
198 }
199}