greentic-flow-builder 0.1.0

AI-powered Adaptive Card flow builder with visual graph editor and demo runner
Documentation
//! Load external template packs from local paths, URLs, or OCI registries.

use crate::template::PackMetadata;
use anyhow::{Context, bail};
use serde_json::Value;
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};

pub struct ExternalPack {
    pub metadata: PackMetadata,
    pub primitives: BTreeMap<String, String>, // name -> source
    pub themes: BTreeMap<String, Value>,
    pub presets: BTreeMap<String, PresetFile>,
}

pub struct PresetFile {
    pub source: String,
    pub schema: Value,
}

pub fn load_external_pack(path: &Path) -> anyhow::Result<ExternalPack> {
    let pack_dir = if path.is_dir() {
        path.to_path_buf()
    } else if path.extension().and_then(|s| s.to_str()) == Some("gz") {
        extract_targz(path)?
    } else {
        bail!("unsupported pack format: {}", path.display())
    };

    let metadata = load_pack_metadata(&pack_dir)?;
    let primitives = load_primitives(&pack_dir)?;
    let themes = load_themes(&pack_dir)?;
    let presets = load_presets(&pack_dir)?;

    Ok(ExternalPack {
        metadata,
        primitives,
        themes,
        presets,
    })
}

fn load_pack_metadata(pack_dir: &Path) -> anyhow::Result<PackMetadata> {
    let json_path = pack_dir.join("pack.json");
    let content = fs::read_to_string(&json_path)
        .with_context(|| format!("reading {}", json_path.display()))?;
    serde_json::from_str(&content).context("parsing pack.json")
}

fn load_primitives(pack_dir: &Path) -> anyhow::Result<BTreeMap<String, String>> {
    let dir = pack_dir.join("primitives");
    if !dir.exists() {
        return Ok(BTreeMap::new());
    }
    let mut map = BTreeMap::new();
    for entry in fs::read_dir(&dir)? {
        let entry = entry?;
        let path = entry.path();
        if path.extension().and_then(|s| s.to_str()) == Some("hbs") {
            let name = path
                .file_stem()
                .and_then(|s| s.to_str())
                .context("primitive filename invalid")?
                .to_string();
            let source =
                fs::read_to_string(&path).with_context(|| format!("reading {}", path.display()))?;
            map.insert(name, source);
        }
    }
    Ok(map)
}

fn load_themes(pack_dir: &Path) -> anyhow::Result<BTreeMap<String, Value>> {
    let dir = pack_dir.join("themes");
    if !dir.exists() {
        return Ok(BTreeMap::new());
    }
    let mut map = BTreeMap::new();
    for entry in fs::read_dir(&dir)? {
        let entry = entry?;
        let path = entry.path();
        if path.extension().and_then(|s| s.to_str()) == Some("json") {
            let name = path
                .file_stem()
                .and_then(|s| s.to_str())
                .context("theme filename invalid")?
                .to_string();
            let content =
                fs::read_to_string(&path).with_context(|| format!("reading {}", path.display()))?;
            let value: Value = serde_json::from_str(&content)
                .with_context(|| format!("parsing {}", path.display()))?;
            map.insert(name, value);
        }
    }
    Ok(map)
}

fn load_presets(pack_dir: &Path) -> anyhow::Result<BTreeMap<String, PresetFile>> {
    let dir = pack_dir.join("presets");
    if !dir.exists() {
        return Ok(BTreeMap::new());
    }
    let mut map = BTreeMap::new();
    load_presets_recursive(&dir, &mut map)?;
    Ok(map)
}

fn load_presets_recursive(
    dir: &Path,
    map: &mut BTreeMap<String, PresetFile>,
) -> anyhow::Result<()> {
    for entry in fs::read_dir(dir)? {
        let entry = entry?;
        let path = entry.path();
        if path.is_dir() {
            load_presets_recursive(&path, map)?;
            continue;
        }
        if path.extension().and_then(|s| s.to_str()) == Some("hbs") {
            let name = path
                .file_stem()
                .and_then(|s| s.to_str())
                .context("preset filename invalid")?
                .to_string();
            let source =
                fs::read_to_string(&path).with_context(|| format!("reading {}", path.display()))?;
            let schema_path = path.with_extension("schema.json");
            let schema: Value = if schema_path.exists() {
                let content = fs::read_to_string(&schema_path)
                    .with_context(|| format!("reading {}", schema_path.display()))?;
                serde_json::from_str(&content)?
            } else {
                Value::Null
            };
            map.insert(name, PresetFile { source, schema });
        }
    }
    Ok(())
}

fn extract_targz(archive: &Path) -> anyhow::Result<PathBuf> {
    use flate2::read::GzDecoder;
    use tar::Archive;

    let tmp = std::env::temp_dir().join(format!("flow-builder-pack-{}", std::process::id()));
    fs::create_dir_all(&tmp)?;
    let file = fs::File::open(archive)?;
    let gz = GzDecoder::new(file);
    let mut tar = Archive::new(gz);
    tar.unpack(&tmp)?;
    Ok(tmp)
}