Skip to main content

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