bundle-standard-core 1.2.0-dev.26017257814

Pure-Rust workspace+ZIP assembly for the bundle-standard recipe.
Documentation
//! Public build_pack orchestrator: synthesize → ZIP → hash → name.

use crate::errors::PackError;
use crate::types::{PackInputs, PackOutput};
use crate::workspace::synthesize_workspace;
use crate::zip_writer::zip_entries;
use sha2::{Digest, Sha256};

pub fn build_pack(inputs: &PackInputs<'_>) -> Result<PackOutput, PackError> {
    let entries = synthesize_workspace(inputs)?;
    let bytes = zip_entries(&entries)?;
    let sha256 = hex_sha256(&bytes);
    let filename = format!(
        "{}-{}.gtpack",
        inputs.config.metadata.name, inputs.config.metadata.version
    );
    Ok(PackOutput {
        filename,
        bytes,
        sha256,
    })
}

fn hex_sha256(bytes: &[u8]) -> String {
    let mut h = Sha256::new();
    h.update(bytes);
    let out = h.finalize();
    const HEX: &[u8; 16] = b"0123456789abcdef";
    let mut s = String::with_capacity(64);
    for b in out {
        s.push(HEX[(b >> 4) as usize] as char);
        s.push(HEX[(b & 0x0f) as usize] as char);
    }
    s
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::{CardContentEntry, FlowEntry, I18nConfig, StandardConfig, StandardMetadata};
    use serde_json::json;

    fn min_inputs<'a>(
        cfg: &'a StandardConfig,
        flows: &'a [FlowEntry],
        cards: &'a [CardContentEntry],
    ) -> PackInputs<'a> {
        PackInputs {
            config: cfg,
            flows,
            cards,
            assets: &[],
            capabilities: &[],
        }
    }

    #[test]
    fn happy_path() {
        let cfg = StandardConfig {
            metadata: StandardMetadata {
                name: "demo".into(),
                version: "0.1.0".into(),
                author: None,
            },
            channels: vec!["webchat".into()],
            embed_ui: "webchat".into(),
            i18n: I18nConfig::default(),
            format: "gtpack-legacy".into(),
        };
        let flows = vec![FlowEntry {
            name: "main".into(),
            yaml: "schema_version: 2".into(),
        }];
        let cards = vec![CardContentEntry {
            id: "welcome".into(),
            json: json!({"type":"AdaptiveCard"}),
        }];
        let out = build_pack(&min_inputs(&cfg, &flows, &cards)).unwrap();
        assert_eq!(out.filename, "demo-0.1.0.gtpack");
        assert_eq!(out.sha256.len(), 64);
        assert!(!out.bytes.is_empty());
    }

    #[test]
    fn deterministic_sha() {
        let cfg = StandardConfig {
            metadata: StandardMetadata {
                name: "x".into(),
                // semver — `manifest.cbor` encoder rejects bare "1".
                version: "1.0.0".into(),
                author: None,
            },
            channels: vec![],
            embed_ui: "none".into(),
            i18n: I18nConfig::default(),
            format: "gtpack-legacy".into(),
        };
        let a = build_pack(&min_inputs(&cfg, &[], &[])).unwrap();
        let b = build_pack(&min_inputs(&cfg, &[], &[])).unwrap();
        assert_eq!(a.sha256, b.sha256);
    }

    #[test]
    fn pack_includes_manifest_cbor() {
        // Verify the new `manifest.cbor` entry decodes back into a
        // PackManifest greentic-start would accept.
        use greentic_types::{PackKind, decode_pack_manifest};
        use std::io::Read;
        let cfg = StandardConfig {
            metadata: StandardMetadata {
                name: "demo".into(),
                version: "0.1.0".into(),
                author: Some("Acme".into()),
            },
            channels: vec!["webchat".into()],
            embed_ui: "webchat".into(),
            i18n: I18nConfig::default(),
            format: "gtpack-legacy".into(),
        };
        let out = build_pack(&min_inputs(&cfg, &[], &[])).unwrap();
        let cursor = std::io::Cursor::new(&out.bytes);
        let mut zip = zip::ZipArchive::new(cursor).unwrap();
        let mut entry = zip.by_name("manifest.cbor").unwrap();
        let mut buf = Vec::new();
        entry.read_to_end(&mut buf).unwrap();
        let m = decode_pack_manifest(&buf).unwrap();
        assert_eq!(m.pack_id.as_str(), "demo");
        assert_eq!(m.version.to_string(), "0.1.0");
        assert!(matches!(m.kind, PackKind::Application));
        assert_eq!(m.publisher, "Acme");
        assert_eq!(m.schema_version, "pack-v1");
        assert!(m.components.is_empty());
        assert!(m.flows.is_empty());
    }
}