zebflow 0.11.14

Reactive web automation platform โ€” pipelines, templates, and real-time collaboration in a single binary.
Documentation
//! Small helpers shared by built-in framework nodes.

use std::hash::{Hash, Hasher};

use serde_json::Value;

use crate::language::{
    COMPILE_TARGET_BACKEND, CompileOptions, ExecutionContext, LanguageEngine, ModuleSource,
    SourceKind,
};
use crate::pipeline::PipelineError;
use crate::pipeline::expr::build_expression_scope_input;

/// A node's answer added to the payload it was given, the rest kept
/// (`docs/contracts/node-conventions.md` ยง5): each top-level key of `answer`
/// replaces the payload's key of that name.
pub fn with_answer(input: &Value, answer: Value) -> Value {
    let mut payload = match input {
        Value::Object(map) => map.clone(),
        _ => serde_json::Map::new(),
    };
    if let Value::Object(answer) = answer {
        payload.extend(answer);
    }
    Value::Object(payload)
}

pub fn metadata_scope(metadata: &Value) -> Result<(&str, &str, &str, &str), PipelineError> {
    let owner = metadata
        .get("owner")
        .and_then(Value::as_str)
        .ok_or_else(|| {
            PipelineError::new(
                "FW_NODE_SCOPE",
                "missing metadata.owner for project-scoped node",
            )
        })?;
    let project = metadata
        .get("project")
        .and_then(Value::as_str)
        .ok_or_else(|| {
            PipelineError::new(
                "FW_NODE_SCOPE",
                "missing metadata.project for project-scoped node",
            )
        })?;
    let pipeline = metadata
        .get("pipeline")
        .and_then(Value::as_str)
        .unwrap_or_default();
    let request_id = metadata
        .get("request_id")
        .and_then(Value::as_str)
        .unwrap_or_default();
    Ok((owner, project, pipeline, request_id))
}

pub fn resolve_path<'a>(root: &'a Value, path: &str) -> Option<&'a Value> {
    let path = path.trim();
    if path.is_empty() {
        return Some(root);
    }

    let mut current = root;
    for segment in path.split('.') {
        let segment = segment.trim();
        if segment.is_empty() {
            continue;
        }
        current = match current {
            Value::Object(map) => map.get(segment)?,
            Value::Array(arr) => {
                let idx: usize = segment.parse().ok()?;
                arr.get(idx)?
            }
            _ => return None,
        };
    }
    Some(current)
}



pub fn eval_deno_expr(
    language: &dyn LanguageEngine,
    expr: &str,
    input: &Value,
    metadata: &Value,
) -> Result<Value, PipelineError> {
    let expr = expr.trim();
    if expr.is_empty() {
        return Err(PipelineError::new(
            "FW_NODE_BINDING_EXPR",
            "binding expression must not be empty",
        ));
    }

    let mut hasher = std::collections::hash_map::DefaultHasher::new();
    expr.hash(&mut hasher);
    let expr = &crate::pipeline::expr::optional_nodes_paths(expr);
    let source = format!(
        "var __scope = input;\n\
         var $input = __scope.$input;\n\
         var $item = __scope.$item;\n\
         var $index = __scope.$index;\n\
         var $count = __scope.$count;\n\
         var $trigger = __scope.$trigger || null;\n\
         var $nodes = __scope.$nodes || {{}};\n\
         return ((function(input, $input, $item, $index, $count, $trigger, $nodes) {{\n\
           return ({expr});\n\
         }})($input, $input, $item, $index, $count, $trigger, $nodes));"
    );
    let module = ModuleSource {
        id: format!("pipeline:binding:{:x}", hasher.finish()),
        source_path: None,
        kind: SourceKind::Tsx,
        code: source,
    };
    let ir = language
        .parse(&module)
        .map_err(|err| PipelineError::new("FW_NODE_BINDING_PARSE", err.to_string()))?;
    let compiled = language
        .compile(
            &ir,
            &CompileOptions {
                target: COMPILE_TARGET_BACKEND.to_string(),
                optimize_level: 1,
                emit_trace_hints: false,
            },
        )
        .map_err(|err| PipelineError::new("FW_NODE_BINDING_COMPILE", err.to_string()))?;
    let ctx = ExecutionContext {
        project: metadata
            .get("project")
            .and_then(Value::as_str)
            .unwrap_or_default()
            .to_string(),
        pipeline: metadata
            .get("pipeline")
            .and_then(Value::as_str)
            .unwrap_or_default()
            .to_string(),
        request_id: metadata
            .get("request_id")
            .and_then(Value::as_str)
            .unwrap_or_default()
            .to_string(),
        trigger: metadata.get("trigger").cloned().unwrap_or(Value::Null),
        metadata: metadata.clone(),
    };
    language
        .run(
            &compiled,
            build_expression_scope_input(input, metadata),
            &ctx,
        )
        .map(|out| out.value)
        .map_err(|err| PipelineError::new("FW_NODE_BINDING_RUN", err.to_string()))
}

/// A filename the author chose, as a stem: alphanumerics, `-` and `_` kept,
/// anything else `_`, the extension dropped (the node adds its own), at most
/// 200 characters. Empty when nothing survives โ€” the caller falls back to a
/// UUID.
pub fn filename_stem(name: &str) -> String {
    let name = name.trim();
    let stem = std::path::Path::new(name).file_stem().and_then(|s| s.to_str()).unwrap_or(name);
    let sanitized: String = stem.chars().map(|c| if c.is_alphanumeric() || c == '-' || c == '_' { c } else { '_' }).collect();
    sanitized.trim_matches('_').chars().take(200).collect()
}