use schemars::JsonSchema;
use semver::Version;
use serde::{Deserialize, Serialize};
use super::command::Namespace;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct CompatibilityRange {
pub min_inclusive: String,
pub max_exclusive: Option<String>,
}
impl CompatibilityRange {
pub fn new(min_inclusive: &str, max_exclusive: Option<&str>) -> Result<Self, String> {
let _ = Version::parse(min_inclusive)
.map_err(|error| format!("invalid min_inclusive semver: {error}"))?;
if let Some(max) = max_exclusive {
let _ = Version::parse(max)
.map_err(|error| format!("invalid max_exclusive semver: {error}"))?;
}
Ok(Self {
min_inclusive: min_inclusive.to_string(),
max_exclusive: max_exclusive.map(ToString::to_string),
})
}
pub fn supports_host(&self, host_version: &str) -> Result<bool, String> {
let host = Version::parse(host_version)
.map_err(|error| format!("invalid host semver: {error}"))?;
let min = Version::parse(&self.min_inclusive)
.map_err(|error| format!("invalid min_inclusive semver: {error}"))?;
if host < min {
return Ok(false);
}
if let Some(max) = &self.max_exclusive {
let max = Version::parse(max)
.map_err(|error| format!("invalid max_exclusive semver: {error}"))?;
return Ok(host < max);
}
Ok(true)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct PluginCapability {
pub name: String,
pub version: Option<String>,
}
impl PluginCapability {
pub fn new(name: &str, version: Option<&str>) -> Result<Self, String> {
if name.trim().is_empty() {
return Err("capability name cannot be empty".to_string());
}
Ok(Self { name: name.to_string(), version: version.map(ToString::to_string) })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum PluginKind {
Native,
#[default]
Delegated,
Python,
ExternalExec,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum PluginLifecycleState {
Discovered,
Validated,
Installed,
Enabled,
Disabled,
Broken,
Incompatible,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct PluginManifestV2 {
pub name: String,
pub version: String,
pub schema_version: String,
pub manifest_version: String,
pub compatibility: CompatibilityRange,
pub namespace: Namespace,
#[serde(default)]
pub kind: PluginKind,
#[serde(default)]
pub aliases: Vec<String>,
pub entrypoint: String,
pub capabilities: Vec<PluginCapability>,
}
impl PluginManifestV2 {
#[allow(clippy::too_many_arguments)]
pub fn new(
name: &str,
version: &str,
schema_version: &str,
manifest_version: &str,
compatibility: CompatibilityRange,
namespace: Namespace,
kind: PluginKind,
aliases: Vec<String>,
entrypoint: &str,
capabilities: Vec<PluginCapability>,
) -> Result<Self, String> {
if name.trim().is_empty() {
return Err("plugin name cannot be empty".to_string());
}
if version.trim().is_empty() {
return Err("plugin version cannot be empty".to_string());
}
if schema_version.trim().is_empty() {
return Err("plugin schema_version cannot be empty".to_string());
}
if schema_version != "v2" {
return Err("plugin schema_version must be v2".to_string());
}
if manifest_version.trim().is_empty() {
return Err("plugin manifest_version cannot be empty".to_string());
}
if manifest_version != "v2" {
return Err("plugin manifest_version must be v2".to_string());
}
if entrypoint.trim().is_empty() {
return Err("plugin entrypoint cannot be empty".to_string());
}
Ok(Self {
name: name.to_string(),
version: version.to_string(),
schema_version: schema_version.to_string(),
manifest_version: manifest_version.to_string(),
compatibility,
namespace,
kind,
aliases,
entrypoint: entrypoint.to_string(),
capabilities,
})
}
}