bundle-standard-core 0.5.5

Pure-Rust workspace+ZIP assembly for the bundle-standard recipe.
Documentation
//! Synthesize the bundle workspace tree as in-memory entries.

use crate::errors::PackError;
use crate::types::{PackInputs, StandardConfig};

pub fn synthesize_workspace(inputs: &PackInputs<'_>) -> Result<Vec<(String, Vec<u8>)>, PackError> {
    if inputs.config.format != "gtpack-legacy" {
        return Err(PackError::InvalidFormat(inputs.config.format.clone()));
    }

    let mut entries: Vec<(String, Vec<u8>)> = Vec::new();

    // bundle.yaml
    entries.push((
        "bundle.yaml".into(),
        bundle_yaml(inputs.config).into_bytes(),
    ));

    // flows/<name>.ygtc
    for flow in inputs.flows {
        entries.push((
            format!("flows/{}.ygtc", flow.name),
            flow.yaml.as_bytes().to_vec(),
        ));
    }

    // assets/cards/<id>.json
    for card in inputs.cards {
        let pretty = serde_json::to_vec_pretty(&card.json)?;
        entries.push((format!("assets/cards/{}.json", card.id), pretty));
    }

    // assets/<rel_path> from raw assets
    for (rel, bytes) in inputs.assets {
        entries.push((format!("assets/{rel}"), bytes.clone()));
    }

    // tenants/default/tenant.gmap
    entries.push((
        "tenants/default/tenant.gmap".into(),
        tenant_gmap(inputs.capabilities).into_bytes(),
    ));

    // Sort for deterministic ZIP ordering.
    entries.sort_by(|a, b| a.0.cmp(&b.0));

    Ok(entries)
}

fn bundle_yaml(config: &StandardConfig) -> String {
    let channels: String = config
        .channels
        .iter()
        .map(|c| format!("  - {c}\n"))
        .collect();
    format!(
        "apiVersion: greentic.ai/v1\nkind: BundleWorkspace\nmetadata:\n  name: {}\n  version: {}\nchannels:\n{}",
        config.metadata.name, config.metadata.version, channels,
    )
}

fn tenant_gmap(caps: &[String]) -> String {
    let caps: String = caps.iter().map(|c| format!("  - {c}\n")).collect();
    format!("# generated by bundle-standard-core\ntenant: default\ncapabilities:\n{caps}")
}

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

    fn cfg() -> StandardConfig {
        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(),
        }
    }

    #[test]
    fn entries_sorted() {
        let cfg = cfg();
        let flows = vec![FlowEntry {
            name: "main".into(),
            yaml: "x".into(),
        }];
        let cards = vec![CardContentEntry {
            id: "welcome".into(),
            json: json!({}),
        }];
        let inputs = PackInputs {
            config: &cfg,
            flows: &flows,
            cards: &cards,
            assets: &[],
            capabilities: &[],
        };
        let entries = synthesize_workspace(&inputs).unwrap();
        let names: Vec<&str> = entries.iter().map(|(n, _)| n.as_str()).collect();
        let mut sorted = names.clone();
        sorted.sort();
        assert_eq!(names, sorted);
    }

    #[test]
    fn rejects_non_legacy_format() {
        let mut config = cfg();
        config.format = "apack".into();
        let inputs = PackInputs {
            config: &config,
            flows: &[],
            cards: &[],
            assets: &[],
            capabilities: &[],
        };
        let err = synthesize_workspace(&inputs).unwrap_err();
        assert_eq!(err.code(), "E_INVALID_FORMAT");
    }
}