Skip to main content

packc/cli/info/
report.rs

1use greentic_pack::builder::PackMeta;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct InfoReport {
6    pub info_schema_version: u32,
7    pub name: String,
8    pub version: String,
9    pub kind: Option<String>,
10    pub description: Option<String>,
11    pub authors: Vec<String>,
12    pub license: Option<String>,
13    pub homepage: Option<String>,
14    pub support: Option<String>,
15    pub vendor: Option<String>,
16    pub created_at_utc: String,
17    pub signature: SignatureInfo,
18    pub components: Vec<ComponentInfo>,
19    pub entry_flows: Vec<String>,
20    pub imports: Vec<ImportInfo>,
21    pub interfaces: Vec<String>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct SignatureInfo {
26    pub status: SignatureStatus,
27    pub key_id: Option<String>,
28}
29
30#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
31#[serde(rename_all = "lowercase")]
32pub enum SignatureStatus {
33    Signed,
34    Unsigned,
35    Invalid,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct ComponentInfo {
40    pub component_id: String,
41    pub version: String,
42    pub kind: Option<String>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ImportInfo {
47    pub pack_id: String,
48    pub version_req: String,
49}
50
51impl InfoReport {
52    pub fn from_pack_meta_and_signature(meta: &PackMeta, signature: SignatureInfo) -> Self {
53        Self {
54            info_schema_version: 1,
55            name: meta.name.clone(),
56            version: meta.version.to_string(),
57            kind: meta
58                .kind
59                .as_ref()
60                .map(|k| pack_kind_to_string(k).to_string()),
61            description: meta.description.clone(),
62            authors: meta.authors.clone(),
63            license: meta.license.clone(),
64            homepage: meta.homepage.clone(),
65            support: meta.support.clone(),
66            vendor: meta.vendor.clone(),
67            created_at_utc: meta.created_at_utc.clone(),
68            signature,
69            components: meta
70                .components
71                .iter()
72                .map(|c| ComponentInfo {
73                    component_id: c.component_id.clone(),
74                    version: c.version.clone(),
75                    kind: c.kind.clone(),
76                })
77                .collect(),
78            entry_flows: meta.entry_flows.clone(),
79            imports: meta
80                .imports
81                .iter()
82                .map(|i| ImportInfo {
83                    pack_id: i.pack_id.clone(),
84                    version_req: i.version_req.clone(),
85                })
86                .collect(),
87            interfaces: meta
88                .interfaces
89                .iter()
90                .map(|b| format!("{}:{}@{}", b.package, b.world, b.version))
91                .collect(),
92        }
93    }
94}
95
96fn pack_kind_to_string(k: &greentic_pack::PackKind) -> &'static str {
97    use greentic_pack::PackKind as K;
98    // Keeps the mapping kebab-case to align with upstream's
99    // `#[serde(rename_all = "kebab-case")]` contract; a new variant added upstream
100    // will fail to compile here, forcing an explicit info rendering decision.
101    match k {
102        K::Application => "application",
103        K::SourceProvider => "source-provider",
104        K::Scanner => "scanner",
105        K::Signing => "signing",
106        K::Attestation => "attestation",
107        K::PolicyEngine => "policy-engine",
108        K::OciProvider => "oci-provider",
109        K::BillingProvider => "billing-provider",
110        K::SearchProvider => "search-provider",
111        K::RecommendationProvider => "recommendation-provider",
112        K::DistributionBundle => "distribution-bundle",
113        K::RolloutStrategy => "rollout-strategy",
114        K::DwApplication => "dw-application",
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn from_pack_meta_projects_all_fields() {
124        use greentic_pack::PackKind;
125        use greentic_pack::builder::{ComponentDescriptor, ImportRef, PackMeta};
126        use greentic_pack::repo::InterfaceBinding;
127
128        let meta = PackMeta {
129            pack_version: 1,
130            pack_id: "ex".into(),
131            version: semver::Version::parse("1.2.3").unwrap(),
132            name: "ex".into(),
133            kind: Some(PackKind::Application),
134            description: Some("demo".into()),
135            authors: vec!["Alice".into()],
136            license: Some("MIT".into()),
137            homepage: None,
138            support: None,
139            vendor: None,
140            imports: vec![ImportRef {
141                pack_id: "greentic/core".into(),
142                version_req: "^0.6.0".into(),
143            }],
144            entry_flows: vec!["flows/a.ygtc".into()],
145            created_at_utc: "2026-01-01T00:00:00Z".into(),
146            events: None,
147            repo: None,
148            messaging: None,
149            interfaces: vec![InterfaceBinding {
150                package: "greentic".into(),
151                world: "component".into(),
152                version: "0.6.0".into(),
153                note: None,
154            }],
155            annotations: Default::default(),
156            distribution: None,
157            components: vec![ComponentDescriptor {
158                component_id: "c".into(),
159                version: "0.1.0".into(),
160                digest: "sha256:x".into(),
161                artifact_path: "components/c.wasm".into(),
162                kind: Some("messaging".into()),
163                artifact_type: Some("component/wasm".into()),
164                tags: vec![],
165                platform: None,
166                entrypoint: None,
167            }],
168        };
169        let sig = SignatureInfo {
170            status: SignatureStatus::Unsigned,
171            key_id: None,
172        };
173
174        let report = InfoReport::from_pack_meta_and_signature(&meta, sig);
175
176        assert_eq!(report.info_schema_version, 1);
177        assert_eq!(report.name, "ex");
178        assert_eq!(report.version, "1.2.3");
179        assert_eq!(report.kind.as_deref(), Some("application"));
180        assert_eq!(report.authors, vec!["Alice".to_string()]);
181        assert_eq!(report.license.as_deref(), Some("MIT"));
182        assert_eq!(report.created_at_utc, "2026-01-01T00:00:00Z");
183        assert_eq!(report.components.len(), 1);
184        assert_eq!(report.components[0].component_id, "c");
185        assert_eq!(report.components[0].version, "0.1.0");
186        assert_eq!(report.components[0].kind.as_deref(), Some("messaging"));
187        assert_eq!(report.entry_flows, vec!["flows/a.ygtc".to_string()]);
188        assert_eq!(report.imports.len(), 1);
189        assert_eq!(report.imports[0].pack_id, "greentic/core");
190        assert_eq!(report.imports[0].version_req, "^0.6.0");
191        assert_eq!(
192            report.interfaces,
193            vec!["greentic:component@0.6.0".to_string()]
194        );
195        assert_eq!(report.signature.status, SignatureStatus::Unsigned);
196    }
197
198    #[test]
199    fn json_has_schema_version_one() {
200        let report = InfoReport {
201            info_schema_version: 1,
202            name: "x".into(),
203            version: "0.1.0".into(),
204            kind: None,
205            description: None,
206            authors: vec![],
207            license: None,
208            homepage: None,
209            support: None,
210            vendor: None,
211            created_at_utc: "2026-01-01T00:00:00Z".into(),
212            signature: SignatureInfo {
213                status: SignatureStatus::Unsigned,
214                key_id: None,
215            },
216            components: vec![],
217            entry_flows: vec![],
218            imports: vec![],
219            interfaces: vec![],
220        };
221        let v: serde_json::Value = serde_json::to_value(&report).unwrap();
222        assert_eq!(v["info_schema_version"], 1);
223        assert_eq!(v["signature"]["status"], "unsigned");
224    }
225}