greentic-component 0.5.2

High-level component loader and store for Greentic components
Documentation
use std::fs;
use std::path::PathBuf;
use std::process::Command;

use anyhow::{Context, Result, bail};
use blake3::Hasher;
use serde_json::json;
use tempfile::TempDir;

const FIXTURE_CARGO_TOML: &str = r#"[package]
name = "contract_fixture"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
greentic-interfaces-guest = { version = "0.4", default-features = false, features = ["component-node"] }
serde_json = "1"
"#;

const FIXTURE_LIB_RS: &str = r#"use greentic_interfaces_guest::component::node::{InvokeResult, NodeError};
use greentic_interfaces_guest::component_entrypoint;
use serde_json::Value;

#[cfg(target_arch = "wasm32")]
#[used]
#[unsafe(link_section = ".greentic.wasi")]
static WASI_TARGET_MARKER: [u8; 13] = *b"wasm32-wasip2";

fn manifest() -> String {
    serde_json::json!({
        "component": {
            "name": "Contract Fixture",
            "org": "greentic",
            "version": "0.1.0",
            "world": "greentic:component/component@0.5.0"
        }
    })
    .to_string()
}

fn invoke(_op: String, input: String) -> InvokeResult {
    let parsed: Value = serde_json::from_str(&input).unwrap_or(Value::Null);
    let is_valid = match parsed.as_object() {
        Some(map) => {
            map.len() == 1
                && map
                    .get("message")
                    .and_then(|value| value.as_str())
                    .is_some()
        }
        None => false,
    };
    if is_valid {
        InvokeResult::Ok("{}".to_string())
    } else {
        InvokeResult::Err(NodeError {
            code: "INVALID_INPUT".to_string(),
            message: "expected object with message".to_string(),
            retryable: false,
            backoff_ms: None,
            details: None,
        })
    }
}

component_entrypoint!({
    manifest: manifest,
    invoke: invoke,
    invoke_stream: false,
});
"#;

fn main() -> Result<()> {
    let fixture_dir =
        PathBuf::from("crates/greentic-component/tests/contract/fixtures/component_v0_5_0");
    fs::create_dir_all(&fixture_dir)?;

    let temp = TempDir::new().context("create temp dir")?;
    let temp_path = temp.path();
    fs::write(temp_path.join("Cargo.toml"), FIXTURE_CARGO_TOML)?;
    fs::create_dir_all(temp_path.join("src"))?;
    fs::write(temp_path.join("src/lib.rs"), FIXTURE_LIB_RS)?;

    let cargo_bin = std::env::var_os("CARGO")
        .map(PathBuf::from)
        .unwrap_or_else(|| "cargo".into());
    let status = Command::new(&cargo_bin)
        .arg("build")
        .arg("--release")
        .arg("--target")
        .arg("wasm32-wasip2")
        .arg("--offline")
        .current_dir(temp_path)
        .status()
        .with_context(|| format!("failed to run {}", cargo_bin.display()))?;
    if !status.success() {
        bail!("cargo build failed with status {}", status);
    }

    let wasm_path = temp_path.join("target/wasm32-wasip2/release/contract_fixture.wasm");
    let wasm_bytes =
        fs::read(&wasm_path).with_context(|| format!("read wasm {}", wasm_path.display()))?;
    fs::write(fixture_dir.join("component.wasm"), &wasm_bytes)?;

    let hash = blake3_hash(&wasm_bytes);
    let manifest = json!({
        "id": "com.greentic.contract.fixture",
        "name": "Contract Fixture",
        "version": "0.1.0",
        "world": "greentic:component/component@0.5.0",
        "describe_export": "describe",
        "operations": [
            { "name": "handle_message", "input_schema": {}, "output_schema": {} }
        ],
        "default_operation": "handle_message",
        "supports": ["messaging"],
        "profiles": {
            "default": "stateless",
            "supported": ["stateless"]
        },
        "config_schema": {
            "type": "object",
            "properties": {},
            "required": [],
            "additionalProperties": false
        },
        "dev_flows": {
            "default": {
                "format": "flow-ir-json",
                "graph": {
                    "nodes": [
                        { "id": "start", "type": "start" },
                        { "id": "end", "type": "end" }
                    ],
                    "edges": [
                        { "from": "start", "to": "end" }
                    ]
                }
            }
        },
        "capabilities": {
            "wasi": {
                "filesystem": {
                    "mode": "none",
                    "mounts": []
                },
                "random": true,
                "clocks": true
            },
            "host": {
                "messaging": {
                    "inbound": true,
                    "outbound": true
                },
                "telemetry": {
                    "scope": "tenant"
                }
            }
        },
        "limits": {
            "memory_mb": 64,
            "wall_time_ms": 1000,
            "fuel": 10,
            "files": 2
        },
        "telemetry": {
            "span_prefix": "contract.fixture",
            "attributes": {
                "component": "fixture"
            },
            "emit_node_spans": true
        },
        "provenance": {
            "builder": "greentic-component",
            "git_commit": "abcdef1",
            "toolchain": "rustc",
            "built_at_utc": "2024-01-01T00:00:00Z"
        },
        "artifacts": { "component_wasm": "component.wasm" },
        "hashes": { "component_wasm": hash }
    });
    fs::write(
        fixture_dir.join("component.manifest.json"),
        serde_json::to_string_pretty(&manifest)?,
    )?;

    println!("wrote contract fixture to {}", fixture_dir.display());
    Ok(())
}

fn blake3_hash(bytes: &[u8]) -> String {
    let mut hasher = Hasher::new();
    hasher.update(bytes);
    format!("blake3:{}", hex::encode(hasher.finalize().as_bytes()))
}