Skip to main content

bundle_standard_core/
build.rs

1//! Public build_pack orchestrator: synthesize → ZIP → hash → name.
2
3use crate::errors::PackError;
4use crate::types::{PackInputs, PackOutput};
5use crate::workspace::synthesize_workspace;
6use crate::zip_writer::zip_entries;
7use sha2::{Digest, Sha256};
8
9pub fn build_pack(inputs: &PackInputs<'_>) -> Result<PackOutput, PackError> {
10    let entries = synthesize_workspace(inputs)?;
11    let bytes = zip_entries(&entries)?;
12    let sha256 = hex_sha256(&bytes);
13    let filename = format!(
14        "{}-{}.gtpack",
15        inputs.config.metadata.name, inputs.config.metadata.version
16    );
17    Ok(PackOutput {
18        filename,
19        bytes,
20        sha256,
21    })
22}
23
24fn hex_sha256(bytes: &[u8]) -> String {
25    let mut h = Sha256::new();
26    h.update(bytes);
27    let out = h.finalize();
28    const HEX: &[u8; 16] = b"0123456789abcdef";
29    let mut s = String::with_capacity(64);
30    for b in out {
31        s.push(HEX[(b >> 4) as usize] as char);
32        s.push(HEX[(b & 0x0f) as usize] as char);
33    }
34    s
35}
36
37#[cfg(test)]
38mod tests {
39    use super::*;
40    use crate::types::{CardContentEntry, FlowEntry, I18nConfig, StandardConfig, StandardMetadata};
41    use serde_json::json;
42
43    fn min_inputs<'a>(
44        cfg: &'a StandardConfig,
45        flows: &'a [FlowEntry],
46        cards: &'a [CardContentEntry],
47    ) -> PackInputs<'a> {
48        PackInputs {
49            config: cfg,
50            flows,
51            cards,
52            assets: &[],
53            capabilities: &[],
54        }
55    }
56
57    #[test]
58    fn happy_path() {
59        let cfg = StandardConfig {
60            metadata: StandardMetadata {
61                name: "demo".into(),
62                version: "0.1.0".into(),
63                author: None,
64            },
65            channels: vec!["webchat".into()],
66            embed_ui: "webchat".into(),
67            i18n: I18nConfig::default(),
68            format: "gtpack-legacy".into(),
69        };
70        let flows = vec![FlowEntry {
71            name: "main".into(),
72            yaml: "schema_version: 2".into(),
73        }];
74        let cards = vec![CardContentEntry {
75            id: "welcome".into(),
76            json: json!({"type":"AdaptiveCard"}),
77        }];
78        let out = build_pack(&min_inputs(&cfg, &flows, &cards)).unwrap();
79        assert_eq!(out.filename, "demo-0.1.0.gtpack");
80        assert_eq!(out.sha256.len(), 64);
81        assert!(!out.bytes.is_empty());
82    }
83
84    #[test]
85    fn deterministic_sha() {
86        let cfg = StandardConfig {
87            metadata: StandardMetadata {
88                name: "x".into(),
89                version: "1".into(),
90                author: None,
91            },
92            channels: vec![],
93            embed_ui: "none".into(),
94            i18n: I18nConfig::default(),
95            format: "gtpack-legacy".into(),
96        };
97        let a = build_pack(&min_inputs(&cfg, &[], &[])).unwrap();
98        let b = build_pack(&min_inputs(&cfg, &[], &[])).unwrap();
99        assert_eq!(a.sha256, b.sha256);
100    }
101}