Skip to main content

bijux_cli/contracts/
plugin.rs

1use schemars::JsonSchema;
2use semver::Version;
3use serde::{Deserialize, Serialize};
4
5use super::command::Namespace;
6
7/// Stable compatibility range contract for plugins and features.
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
9pub struct CompatibilityRange {
10    /// Minimum supported version inclusive.
11    pub min_inclusive: String,
12    /// Optional maximum supported version exclusive.
13    pub max_exclusive: Option<String>,
14}
15
16impl CompatibilityRange {
17    /// Build a validated compatibility range.
18    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    /// Check whether a host version is supported by this range.
32    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/// Stable plugin capability declaration.
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
51pub struct PluginCapability {
52    /// Capability identifier.
53    pub name: String,
54    /// Optional capability version.
55    pub version: Option<String>,
56}
57
58impl PluginCapability {
59    /// Build a validated plugin capability declaration.
60    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/// Stable plugin kind declaration.
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
70#[serde(rename_all = "kebab-case")]
71#[non_exhaustive]
72pub enum PluginKind {
73    /// Future in-process plugin ABI.
74    Native,
75    /// Delegated plugin loaded through host contract bridge.
76    #[default]
77    Delegated,
78    /// Python delegated plugin runtime.
79    Python,
80    /// External executable plugin.
81    ExternalExec,
82}
83
84/// Stable plugin lifecycle state in registry and diagnostics.
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
86#[serde(rename_all = "lowercase")]
87#[non_exhaustive]
88pub enum PluginLifecycleState {
89    /// Artifact located during discovery.
90    Discovered,
91    /// Manifest and contract validation passed.
92    Validated,
93    /// Plugin installed in registry.
94    Installed,
95    /// Plugin actively enabled for routing.
96    Enabled,
97    /// Plugin present but inactive.
98    Disabled,
99    /// Plugin failed validation or runtime loading.
100    Broken,
101    /// Plugin failed compatibility checks.
102    Incompatible,
103}
104
105/// Current plugin manifest contract.
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
107pub struct PluginManifestV2 {
108    /// Plugin name.
109    pub name: String,
110    /// Plugin version.
111    pub version: String,
112    /// Plugin schema version.
113    pub schema_version: String,
114    /// Manifest contract version.
115    pub manifest_version: String,
116    /// Compatibility range for host CLI.
117    pub compatibility: CompatibilityRange,
118    /// Declared top-level namespace.
119    pub namespace: Namespace,
120    /// Plugin execution kind.
121    #[serde(default)]
122    pub kind: PluginKind,
123    /// Declared command aliases.
124    #[serde(default)]
125    pub aliases: Vec<String>,
126    /// Plugin entrypoint (binary path or module symbol).
127    pub entrypoint: String,
128    /// Declared capabilities.
129    pub capabilities: Vec<PluginCapability>,
130}
131
132impl PluginManifestV2 {
133    /// Build a validated v2 plugin manifest.
134    #[allow(clippy::too_many_arguments)]
135    pub fn new(
136        name: &str,
137        version: &str,
138        schema_version: &str,
139        manifest_version: &str,
140        compatibility: CompatibilityRange,
141        namespace: Namespace,
142        kind: PluginKind,
143        aliases: Vec<String>,
144        entrypoint: &str,
145        capabilities: Vec<PluginCapability>,
146    ) -> Result<Self, String> {
147        if name.trim().is_empty() {
148            return Err("plugin name cannot be empty".to_string());
149        }
150        if version.trim().is_empty() {
151            return Err("plugin version cannot be empty".to_string());
152        }
153        if schema_version.trim().is_empty() {
154            return Err("plugin schema_version cannot be empty".to_string());
155        }
156        if schema_version != "v2" {
157            return Err("plugin schema_version must be v2".to_string());
158        }
159        if manifest_version.trim().is_empty() {
160            return Err("plugin manifest_version cannot be empty".to_string());
161        }
162        if manifest_version != "v2" {
163            return Err("plugin manifest_version must be v2".to_string());
164        }
165        if entrypoint.trim().is_empty() {
166            return Err("plugin entrypoint cannot be empty".to_string());
167        }
168        Ok(Self {
169            name: name.to_string(),
170            version: version.to_string(),
171            schema_version: schema_version.to_string(),
172            manifest_version: manifest_version.to_string(),
173            compatibility,
174            namespace,
175            kind,
176            aliases,
177            entrypoint: entrypoint.to_string(),
178            capabilities,
179        })
180    }
181}