use crate::errors::PackError;
use crate::types::{PackInputs, StandardConfig};
use greentic_types::{PackId, PackKind, PackManifest, PackSignatures, encode_pack_manifest};
use semver::Version;
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();
entries.push(("manifest.cbor".into(), build_manifest_cbor(inputs.config)?));
entries.push((
"bundle.yaml".into(),
bundle_yaml(inputs.config).into_bytes(),
));
for flow in inputs.flows {
entries.push((
format!("flows/{}.ygtc", flow.name),
flow.yaml.as_bytes().to_vec(),
));
}
for card in inputs.cards {
let pretty = serde_json::to_vec_pretty(&card.json)?;
entries.push((format!("assets/cards/{}.json", card.id), pretty));
}
for (rel, bytes) in inputs.assets {
entries.push((format!("assets/{rel}"), bytes.clone()));
}
entries.push((
"tenants/default/tenant.gmap".into(),
tenant_gmap(inputs.capabilities).into_bytes(),
));
entries.sort_by(|a, b| a.0.cmp(&b.0));
Ok(entries)
}
fn build_manifest_cbor(config: &StandardConfig) -> Result<Vec<u8>, PackError> {
let pack_id: PackId =
config
.metadata
.name
.parse()
.map_err(|e: greentic_types::GreenticError| {
PackError::InvalidConfig(format!("metadata.name as PackId: {e}"))
})?;
let version = Version::parse(&config.metadata.version)
.map_err(|e| PackError::InvalidConfig(format!("metadata.version as semver: {e}")))?;
let publisher = config
.metadata
.author
.as_deref()
.unwrap_or("Greentic")
.to_string();
let manifest = PackManifest {
schema_version: "pack-v1".into(),
pack_id,
name: Some(config.metadata.name.clone()),
version,
kind: PackKind::Application,
publisher,
components: Vec::new(),
flows: Vec::new(),
dependencies: Vec::new(),
capabilities: Vec::new(),
secret_requirements: Vec::new(),
signatures: PackSignatures::default(),
bootstrap: None,
extensions: None,
};
encode_pack_manifest(&manifest)
.map_err(|e| PackError::ManifestCbor(format!("encode_pack_manifest: {e}")))
}
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");
}
}