greentic_types/
pack_manifest.rs

1//! Canonical pack manifest (.gtpack) representation embedding flows and components.
2
3use alloc::collections::BTreeMap;
4use alloc::string::String;
5use alloc::vec::Vec;
6
7use semver::Version;
8
9use crate::pack::extensions::component_sources::{
10    ComponentSourcesError, ComponentSourcesV1, EXT_COMPONENT_SOURCES_V1,
11};
12use crate::{
13    ComponentManifest, Flow, FlowId, FlowKind, PROVIDER_EXTENSION_ID, PackId,
14    ProviderExtensionInline, SecretRequirement, SemverReq, Signature,
15};
16
17#[cfg(feature = "schemars")]
18use schemars::JsonSchema;
19#[cfg(feature = "serde")]
20use serde::{Deserialize, Serialize};
21
22#[cfg(feature = "schemars")]
23fn empty_secret_requirements() -> Vec<SecretRequirement> {
24    Vec::new()
25}
26
27pub(crate) fn extensions_is_empty(value: &Option<BTreeMap<String, ExtensionRef>>) -> bool {
28    value.as_ref().is_none_or(BTreeMap::is_empty)
29}
30
31/// Hint describing the primary purpose of a pack.
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
34#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
35#[cfg_attr(feature = "schemars", derive(JsonSchema))]
36pub enum PackKind {
37    /// Application packs.
38    Application,
39    /// Provider packs exporting components.
40    Provider,
41    /// Infrastructure packs.
42    Infrastructure,
43    /// Library packs.
44    Library,
45}
46
47/// Pack manifest describing bundled flows and components.
48#[derive(Clone, Debug, PartialEq)]
49#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
50#[cfg_attr(
51    feature = "schemars",
52    derive(JsonSchema),
53    schemars(
54        title = "Greentic PackManifest v1",
55        description = "Canonical pack manifest embedding flows, components, dependencies and signatures.",
56        rename = "greentic.pack-manifest.v1"
57    )
58)]
59pub struct PackManifest {
60    /// Schema version for the pack manifest.
61    pub schema_version: String,
62    /// Logical pack identifier.
63    pub pack_id: PackId,
64    /// Pack semantic version.
65    #[cfg_attr(
66        feature = "schemars",
67        schemars(with = "String", description = "SemVer version")
68    )]
69    pub version: Version,
70    /// Pack kind hint.
71    pub kind: PackKind,
72    /// Pack publisher.
73    pub publisher: String,
74    /// Component descriptors bundled within the pack.
75    #[cfg_attr(feature = "serde", serde(default))]
76    pub components: Vec<ComponentManifest>,
77    /// Flow entries embedded in the pack.
78    #[cfg_attr(feature = "serde", serde(default))]
79    pub flows: Vec<PackFlowEntry>,
80    /// Pack dependencies.
81    #[cfg_attr(feature = "serde", serde(default))]
82    pub dependencies: Vec<PackDependency>,
83    /// Capability declarations for the pack.
84    #[cfg_attr(feature = "serde", serde(default))]
85    pub capabilities: Vec<ComponentCapability>,
86    /// Pack-level secret requirements.
87    #[cfg_attr(
88        feature = "serde",
89        serde(default, skip_serializing_if = "Vec::is_empty")
90    )]
91    #[cfg_attr(feature = "schemars", schemars(default = "empty_secret_requirements"))]
92    pub secret_requirements: Vec<SecretRequirement>,
93    /// Pack signatures.
94    #[cfg_attr(feature = "serde", serde(default))]
95    pub signatures: PackSignatures,
96    /// Optional bootstrap/install hints for platform-controlled packs.
97    #[cfg_attr(
98        feature = "serde",
99        serde(default, skip_serializing_if = "Option::is_none")
100    )]
101    pub bootstrap: Option<BootstrapSpec>,
102    /// Optional extension descriptors for provider-specific metadata.
103    #[cfg_attr(
104        feature = "serde",
105        serde(default, skip_serializing_if = "extensions_is_empty")
106    )]
107    pub extensions: Option<BTreeMap<String, ExtensionRef>>,
108}
109
110/// Flow entry embedded in a pack.
111#[derive(Clone, Debug, PartialEq)]
112#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
113#[cfg_attr(feature = "schemars", derive(JsonSchema))]
114pub struct PackFlowEntry {
115    /// Flow identifier.
116    pub id: FlowId,
117    /// Flow kind.
118    pub kind: FlowKind,
119    /// Flow definition.
120    pub flow: Flow,
121    /// Flow tags.
122    #[cfg_attr(feature = "serde", serde(default))]
123    pub tags: Vec<String>,
124    /// Additional entrypoint identifiers for discoverability.
125    #[cfg_attr(feature = "serde", serde(default))]
126    pub entrypoints: Vec<String>,
127}
128
129/// Dependency entry referencing another pack.
130#[derive(Clone, Debug, PartialEq, Eq)]
131#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
132#[cfg_attr(feature = "schemars", derive(JsonSchema))]
133pub struct PackDependency {
134    /// Local alias for the dependency.
135    pub alias: String,
136    /// Referenced pack identifier.
137    pub pack_id: PackId,
138    /// Required version.
139    pub version_req: SemverReq,
140    /// Required capabilities.
141    #[cfg_attr(feature = "serde", serde(default))]
142    pub required_capabilities: Vec<String>,
143}
144
145/// Named capability advertised by a pack or component collection.
146#[derive(Clone, Debug, PartialEq, Eq)]
147#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
148#[cfg_attr(feature = "schemars", derive(JsonSchema))]
149pub struct ComponentCapability {
150    /// Capability name.
151    pub name: String,
152    /// Optional description or metadata.
153    #[cfg_attr(
154        feature = "serde",
155        serde(default, skip_serializing_if = "Option::is_none")
156    )]
157    pub description: Option<String>,
158}
159
160/// Signature bundle accompanying a pack manifest.
161#[derive(Clone, Debug, PartialEq, Eq, Default)]
162#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
163#[cfg_attr(feature = "schemars", derive(JsonSchema))]
164pub struct PackSignatures {
165    /// Optional detached signatures.
166    #[cfg_attr(feature = "serde", serde(default))]
167    pub signatures: Vec<Signature>,
168}
169
170/// Optional bootstrap/install hints for platform-managed packs.
171#[derive(Clone, Debug, PartialEq, Eq, Default)]
172#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
173#[cfg_attr(feature = "schemars", derive(JsonSchema))]
174pub struct BootstrapSpec {
175    /// Flow to run during initial install/bootstrap.
176    #[cfg_attr(
177        feature = "serde",
178        serde(default, skip_serializing_if = "Option::is_none")
179    )]
180    pub install_flow: Option<String>,
181    /// Flow to run when upgrading an existing install.
182    #[cfg_attr(
183        feature = "serde",
184        serde(default, skip_serializing_if = "Option::is_none")
185    )]
186    pub upgrade_flow: Option<String>,
187    /// Component responsible for install/upgrade orchestration.
188    #[cfg_attr(
189        feature = "serde",
190        serde(default, skip_serializing_if = "Option::is_none")
191    )]
192    pub installer_component: Option<String>,
193}
194
195/// Inline payload for a pack extension entry.
196#[derive(Clone, Debug, PartialEq)]
197#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
198#[cfg_attr(feature = "serde", serde(untagged))]
199#[cfg_attr(feature = "schemars", derive(JsonSchema))]
200pub enum ExtensionInline {
201    /// Provider extension payload embedding provider declarations.
202    Provider(ProviderExtensionInline),
203    /// Arbitrary inline payload for unknown extensions.
204    Other(serde_json::Value),
205}
206
207impl ExtensionInline {
208    /// Returns the provider inline payload if present.
209    pub fn as_provider_inline(&self) -> Option<&ProviderExtensionInline> {
210        match self {
211            ExtensionInline::Provider(value) => Some(value),
212            ExtensionInline::Other(_) => None,
213        }
214    }
215
216    /// Returns a mutable provider inline payload if present.
217    pub fn as_provider_inline_mut(&mut self) -> Option<&mut ProviderExtensionInline> {
218        match self {
219            ExtensionInline::Provider(value) => Some(value),
220            ExtensionInline::Other(_) => None,
221        }
222    }
223}
224
225/// External extension reference embedded in a pack manifest.
226#[derive(Clone, Debug, PartialEq)]
227#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
228#[cfg_attr(feature = "schemars", derive(JsonSchema))]
229pub struct ExtensionRef {
230    /// Extension kind identifier, e.g. `greentic.provider-extension.v1` (only this ID is supported for provider metadata; other keys are treated as unknown extensions).
231    pub kind: String,
232    /// Extension version as a string to avoid semver crate coupling.
233    pub version: String,
234    /// Optional digest pin for the referenced extension payload.
235    #[cfg_attr(
236        feature = "serde",
237        serde(default, skip_serializing_if = "Option::is_none")
238    )]
239    pub digest: Option<String>,
240    /// Optional remote or local location for the extension payload.
241    #[cfg_attr(
242        feature = "serde",
243        serde(default, skip_serializing_if = "Option::is_none")
244    )]
245    pub location: Option<String>,
246    /// Optional inline extension payload for small metadata blobs.
247    #[cfg_attr(
248        feature = "serde",
249        serde(default, skip_serializing_if = "Option::is_none")
250    )]
251    pub inline: Option<ExtensionInline>,
252}
253
254impl PackManifest {
255    /// Returns the inline provider extension payload if present.
256    pub fn provider_extension_inline(&self) -> Option<&ProviderExtensionInline> {
257        self.extensions
258            .as_ref()
259            .and_then(|extensions| extensions.get(PROVIDER_EXTENSION_ID))
260            .and_then(|extension| extension.inline.as_ref())
261            .and_then(ExtensionInline::as_provider_inline)
262    }
263
264    /// Returns a mutable inline provider extension payload if present.
265    pub fn provider_extension_inline_mut(&mut self) -> Option<&mut ProviderExtensionInline> {
266        self.extensions
267            .as_mut()
268            .and_then(|extensions| extensions.get_mut(PROVIDER_EXTENSION_ID))
269            .and_then(|extension| extension.inline.as_mut())
270            .map(|inline| {
271                if let ExtensionInline::Other(value) = inline {
272                    let parsed = serde_json::from_value(value.clone())
273                        .unwrap_or_else(|_| ProviderExtensionInline::default());
274                    *inline = ExtensionInline::Provider(parsed);
275                }
276                inline
277            })
278            .and_then(ExtensionInline::as_provider_inline_mut)
279    }
280
281    /// Ensures the provider extension entry exists and returns its inline payload.
282    pub fn ensure_provider_extension_inline(&mut self) -> &mut ProviderExtensionInline {
283        let extensions = self.extensions.get_or_insert_with(BTreeMap::new);
284        let entry = extensions
285            .entry(PROVIDER_EXTENSION_ID.to_string())
286            .or_insert_with(|| ExtensionRef {
287                kind: PROVIDER_EXTENSION_ID.to_string(),
288                version: "1.0.0".to_string(),
289                digest: None,
290                location: None,
291                inline: Some(ExtensionInline::Provider(ProviderExtensionInline::default())),
292            });
293        if entry.inline.is_none() {
294            entry.inline = Some(ExtensionInline::Provider(ProviderExtensionInline::default()));
295        }
296        let inline = entry
297            .inline
298            .get_or_insert_with(|| ExtensionInline::Provider(ProviderExtensionInline::default()));
299        if let ExtensionInline::Other(value) = inline {
300            let parsed = serde_json::from_value(value.clone())
301                .unwrap_or_else(|_| ProviderExtensionInline::default());
302            *inline = ExtensionInline::Provider(parsed);
303        }
304        match inline {
305            ExtensionInline::Provider(inline) => inline,
306            ExtensionInline::Other(_) => unreachable!("provider inline should be initialised"),
307        }
308    }
309
310    /// Returns the component sources extension payload if present.
311    #[cfg(feature = "serde")]
312    pub fn get_component_sources_v1(
313        &self,
314    ) -> Result<Option<ComponentSourcesV1>, ComponentSourcesError> {
315        let extension = self
316            .extensions
317            .as_ref()
318            .and_then(|extensions| extensions.get(EXT_COMPONENT_SOURCES_V1));
319        let inline = match extension.and_then(|entry| entry.inline.as_ref()) {
320            Some(ExtensionInline::Other(value)) => value,
321            Some(_) => return Err(ComponentSourcesError::UnexpectedInline),
322            None => return Ok(None),
323        };
324        let payload = ComponentSourcesV1::from_extension_value(inline)?;
325        Ok(Some(payload))
326    }
327
328    /// Sets the component sources extension payload.
329    #[cfg(feature = "serde")]
330    pub fn set_component_sources_v1(
331        &mut self,
332        sources: ComponentSourcesV1,
333    ) -> Result<(), ComponentSourcesError> {
334        sources.validate_schema_version()?;
335        let inline = sources.to_extension_value()?;
336        let extensions = self.extensions.get_or_insert_with(BTreeMap::new);
337        extensions.insert(
338            EXT_COMPONENT_SOURCES_V1.to_string(),
339            ExtensionRef {
340                kind: EXT_COMPONENT_SOURCES_V1.to_string(),
341                version: "1.0.0".to_string(),
342                digest: None,
343                location: None,
344                inline: Some(ExtensionInline::Other(inline)),
345            },
346        );
347        Ok(())
348    }
349}