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    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn from_pack_meta_projects_all_fields() {
123        use greentic_pack::PackKind;
124        use greentic_pack::builder::{ComponentDescriptor, ImportRef, PackMeta};
125        use greentic_pack::repo::InterfaceBinding;
126
127        let meta = PackMeta {
128            pack_version: 1,
129            pack_id: "ex".into(),
130            version: semver::Version::parse("1.2.3").unwrap(),
131            name: "ex".into(),
132            kind: Some(PackKind::Application),
133            description: Some("demo".into()),
134            authors: vec!["Alice".into()],
135            license: Some("MIT".into()),
136            homepage: None,
137            support: None,
138            vendor: None,
139            imports: vec![ImportRef {
140                pack_id: "greentic/core".into(),
141                version_req: "^0.6.0".into(),
142            }],
143            entry_flows: vec!["flows/a.ygtc".into()],
144            created_at_utc: "2026-01-01T00:00:00Z".into(),
145            events: None,
146            repo: None,
147            messaging: None,
148            interfaces: vec![InterfaceBinding {
149                package: "greentic".into(),
150                world: "component".into(),
151                version: "0.6.0".into(),
152                note: None,
153            }],
154            annotations: Default::default(),
155            distribution: None,
156            components: vec![ComponentDescriptor {
157                component_id: "c".into(),
158                version: "0.1.0".into(),
159                digest: "sha256:x".into(),
160                artifact_path: "components/c.wasm".into(),
161                kind: Some("messaging".into()),
162                artifact_type: Some("component/wasm".into()),
163                tags: vec![],
164                platform: None,
165                entrypoint: None,
166            }],
167        };
168        let sig = SignatureInfo {
169            status: SignatureStatus::Unsigned,
170            key_id: None,
171        };
172
173        let report = InfoReport::from_pack_meta_and_signature(&meta, sig);
174
175        assert_eq!(report.info_schema_version, 1);
176        assert_eq!(report.name, "ex");
177        assert_eq!(report.version, "1.2.3");
178        assert_eq!(report.kind.as_deref(), Some("application"));
179        assert_eq!(report.authors, vec!["Alice".to_string()]);
180        assert_eq!(report.license.as_deref(), Some("MIT"));
181        assert_eq!(report.created_at_utc, "2026-01-01T00:00:00Z");
182        assert_eq!(report.components.len(), 1);
183        assert_eq!(report.components[0].component_id, "c");
184        assert_eq!(report.components[0].version, "0.1.0");
185        assert_eq!(report.components[0].kind.as_deref(), Some("messaging"));
186        assert_eq!(report.entry_flows, vec!["flows/a.ygtc".to_string()]);
187        assert_eq!(report.imports.len(), 1);
188        assert_eq!(report.imports[0].pack_id, "greentic/core");
189        assert_eq!(report.imports[0].version_req, "^0.6.0");
190        assert_eq!(
191            report.interfaces,
192            vec!["greentic:component@0.6.0".to_string()]
193        );
194        assert_eq!(report.signature.status, SignatureStatus::Unsigned);
195    }
196
197    #[test]
198    fn json_has_schema_version_one() {
199        let report = InfoReport {
200            info_schema_version: 1,
201            name: "x".into(),
202            version: "0.1.0".into(),
203            kind: None,
204            description: None,
205            authors: vec![],
206            license: None,
207            homepage: None,
208            support: None,
209            vendor: None,
210            created_at_utc: "2026-01-01T00:00:00Z".into(),
211            signature: SignatureInfo {
212                status: SignatureStatus::Unsigned,
213                key_id: None,
214            },
215            components: vec![],
216            entry_flows: vec![],
217            imports: vec![],
218            interfaces: vec![],
219        };
220        let v: serde_json::Value = serde_json::to_value(&report).unwrap();
221        assert_eq!(v["info_schema_version"], 1);
222        assert_eq!(v["signature"]["status"], "unsigned");
223    }
224}