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