greentic-flow 0.4.63

Generic YGTC flow schema/loader/IR for self-describing component nodes.
Documentation
use std::collections::HashSet;

fn slugify(raw: &str) -> String {
    let mut out = String::new();
    for (idx, ch) in raw.chars().enumerate() {
        let safe = if ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' {
            ch
        } else {
            '_'
        };
        if idx == 0 && !(safe.is_ascii_alphabetic() || safe == '_') {
            out.push('_');
        }
        out.push(safe);
    }
    if out.is_empty() { "_".to_string() } else { out }
}

fn is_placeholder(hint: Option<&str>) -> bool {
    match hint {
        None => true,
        Some(h) => is_placeholder_value(h),
    }
}

pub fn is_placeholder_value(hint: &str) -> bool {
    let trimmed = hint.trim();
    trimmed.is_empty()
        || matches!(
            trimmed,
            "STEP" | "NODE" | "COMPONENT_STEP" | "INSERT_NODE" | "NEW_NODE"
        )
}

pub fn generate_node_id<'a, I: Iterator<Item = &'a str>>(
    hint: Option<&str>,
    anchor: &str,
    existing: I,
) -> String {
    let used: HashSet<String> = existing.map(|s| s.to_string()).collect();
    let base = if !is_placeholder(hint) {
        let slug = slugify(hint.unwrap());
        if !slug.is_empty() {
            slug
        } else {
            "node".to_string()
        }
    } else {
        let mut parts = vec!["node".to_string()];
        parts.push("after".to_string());
        parts.push(slugify(anchor));
        parts.join("__")
    };

    if !used.contains(&base) && !is_placeholder(hint) {
        return base;
    }

    let mut candidate = base.clone();
    let mut idx = 2usize;
    while used.contains(&candidate) {
        candidate = format!("{base}__{idx}");
        idx += 1;
    }
    candidate
}