greentic-flow 0.4.63

Generic YGTC flow schema/loader/IR for self-describing component nodes.
Documentation
use anyhow::{Result, anyhow};
use serde_json::Value;
use std::time::{SystemTime, UNIX_EPOCH};

pub const META_NAMESPACE: &str = "greentic";

pub fn now_epoch_seconds() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0)
}

fn ensure_object(value: &mut Option<Value>) -> &mut serde_json::Map<String, Value> {
    if !matches!(value, Some(Value::Object(_))) {
        *value = Some(Value::Object(serde_json::Map::new()));
    }
    match value.as_mut().unwrap() {
        Value::Object(map) => map,
        _ => unreachable!(),
    }
}

fn ensure_child_map<'a>(
    parent: &'a mut serde_json::Map<String, Value>,
    key: &str,
) -> &'a mut serde_json::Map<String, Value> {
    let entry = parent
        .entry(key.to_string())
        .or_insert_with(|| Value::Object(serde_json::Map::new()));
    match entry {
        Value::Object(map) => map,
        _ => {
            *entry = Value::Object(serde_json::Map::new());
            match entry {
                Value::Object(map) => map,
                _ => unreachable!(),
            }
        }
    }
}

pub fn ensure_greentic_meta(meta: &mut Option<Value>) -> &mut serde_json::Map<String, Value> {
    let root = ensure_object(meta);
    ensure_child_map(root, META_NAMESPACE)
}

pub fn set_component_entry(
    meta: &mut Option<Value>,
    node_id: &str,
    component_id: &str,
    abi_version: &str,
    digest: Option<&str>,
    exported_ops: &[String],
    contract: Option<&ComponentContractMeta>,
) {
    let greentic = ensure_greentic_meta(meta);
    let components = ensure_child_map(greentic, "components");
    let mut added_at = now_epoch_seconds();
    if let Some(Value::Object(existing)) = components.get(node_id)
        && let Some(Value::Number(number)) = existing.get("added_at")
        && let Some(value) = number.as_u64()
    {
        added_at = value;
    }
    let mut entry = serde_json::Map::new();
    entry.insert(
        "component_id".to_string(),
        Value::String(component_id.to_string()),
    );
    entry.insert(
        "abi_version".to_string(),
        Value::String(abi_version.to_string()),
    );
    if let Some(contract) = contract {
        entry.insert(
            "describe_hash".to_string(),
            Value::String(contract.describe_hash.clone()),
        );
        entry.insert(
            "operation_id".to_string(),
            Value::String(contract.operation_id.clone()),
        );
        entry.insert(
            "schema_hash".to_string(),
            Value::String(contract.schema_hash.clone()),
        );
        if let Some(version) = &contract.component_version {
            entry.insert(
                "component_version".to_string(),
                Value::String(version.clone()),
            );
        }
        if let Some(world) = &contract.world {
            entry.insert("world".to_string(), Value::String(world.clone()));
        }
        if let Some(config_schema_cbor) = &contract.config_schema_cbor {
            entry.insert(
                "config_schema_cbor".to_string(),
                Value::String(config_schema_cbor.clone()),
            );
        }
    }
    if let Some(d) = digest {
        entry.insert("resolved_digest".to_string(), Value::String(d.to_string()));
    }
    entry.insert(
        "exported_ops_seen".to_string(),
        Value::Array(
            exported_ops
                .iter()
                .map(|s| Value::String(s.clone()))
                .collect(),
        ),
    );
    entry.insert(
        "added_at".to_string(),
        Value::Number(serde_json::Number::from(added_at)),
    );
    entry.insert(
        "updated_at".to_string(),
        Value::Number(serde_json::Number::from(now_epoch_seconds())),
    );
    components.insert(node_id.to_string(), Value::Object(entry));
}

pub struct ComponentContractMeta {
    pub describe_hash: String,
    pub operation_id: String,
    pub schema_hash: String,
    pub component_version: Option<String>,
    pub world: Option<String>,
    pub config_schema_cbor: Option<String>,
}

pub fn clear_component_entry(meta: &mut Option<Value>, node_id: &str) {
    let Some(Value::Object(root)) = meta else {
        return;
    };
    let Some(Value::Object(greentic)) = root.get_mut(META_NAMESPACE) else {
        return;
    };
    if let Some(Value::Object(components)) = greentic.get_mut("components") {
        components.remove(node_id);
    }
    if let Some(Value::Object(secrets)) = greentic.get_mut("secrets_hints") {
        secrets.remove(node_id);
    }
    if let Some(Value::Object(bindings)) = greentic.get_mut("bindings_hints") {
        bindings.remove(node_id);
    }
}

pub fn ensure_hints_empty(meta: &mut Option<Value>, node_id: &str) {
    let greentic = ensure_greentic_meta(meta);
    {
        let secrets = ensure_child_map(greentic, "secrets_hints");
        secrets
            .entry(node_id.to_string())
            .or_insert_with(|| Value::Array(Vec::new()));
    }
    {
        let bindings = ensure_child_map(greentic, "bindings_hints");
        bindings
            .entry(node_id.to_string())
            .or_insert_with(|| Value::Array(Vec::new()));
    }
}

pub fn find_node_for_component(meta: &Option<Value>, component_id: &str) -> Result<String> {
    let Some(Value::Object(root)) = meta else {
        return Err(anyhow!("flow metadata missing; provide --step"));
    };
    let Some(Value::Object(greentic)) = root.get(META_NAMESPACE) else {
        return Err(anyhow!("flow metadata missing; provide --step"));
    };
    let Some(Value::Object(components)) = greentic.get("components") else {
        return Err(anyhow!("flow metadata missing; provide --step"));
    };
    let mut matches = Vec::new();
    for (node_id, entry) in components {
        if let Value::Object(obj) = entry
            && obj
                .get("component_id")
                .and_then(Value::as_str)
                .is_some_and(|id| id == component_id)
        {
            matches.push(node_id.clone());
        }
    }
    match matches.len() {
        0 => Err(anyhow!(
            "no node found for component id '{component_id}'; provide --step"
        )),
        1 => Ok(matches.remove(0)),
        _ => Err(anyhow!(
            "multiple nodes found for component id '{component_id}'; provide --step"
        )),
    }
}