use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use serde_json::{Value, json};
use super::scanner::{ExprField, Segment, scan};
use crate::language::{
COMPILE_TARGET_BACKEND, CompileOptions, ExecutionContext, LanguageEngine, ModuleSource,
SourceKind,
};
use crate::pipeline::PipelineError;
pub fn build_expression_scope_input(input: &Value, metadata: &Value) -> Value {
json!({
"$input": input,
"$item": input.get("item").cloned().unwrap_or(Value::Null),
"$index": input.get("index").cloned().unwrap_or(Value::Null),
"$count": input.get("count").cloned().unwrap_or(Value::Null),
"$trigger": metadata.get("trigger").cloned().unwrap_or(Value::Null),
"$nodes": metadata.get("nodes").cloned().unwrap_or_else(|| json!({})),
"$placeholder": metadata.get("placeholder").cloned().unwrap_or_else(|| json!({})),
})
}
pub fn resolve_config_expressions(
mut config: Value,
input: &Value,
metadata: &Value,
language: &Arc<dyn LanguageEngine>,
) -> Result<Value, PipelineError> {
let fields = scan(&config);
if fields.is_empty() {
return Ok(config);
}
let mut exprs: Vec<String> = Vec::new();
for field in &fields {
for seg in &field.segments {
if let Segment::Expr(e) = seg {
if !exprs.contains(e) {
exprs.push(e.clone());
}
}
}
}
let mut body = String::from(
"var __scope = input;\n\
var input = __scope.$input;\n\
var $input = 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\
var $placeholder = __scope.$placeholder || {};\n",
);
for (i, expr) in exprs.iter().enumerate() {
body.push_str(&format!("var _e{i} = null; var _x{i} = null;\n"));
body.push_str(&format!(
"try {{ _e{i} = ({expr}); }} catch (_zfe) {{ _x{i} = String((_zfe && _zfe.message) || _zfe); }}\n"
));
}
body.push_str("return {\n");
for (i, _) in exprs.iter().enumerate() {
body.push_str(&format!(" \"e{i}\": _e{i},\n"));
let comma = if i + 1 < exprs.len() { "," } else { "" };
body.push_str(&format!(" \"x{i}\": _x{i}{comma}\n"));
}
body.push_str("};\n");
let module = ModuleSource {
id: format!("pipeline:expr:{}", hash_str(&body)),
source_path: None,
kind: SourceKind::Tsx,
code: body,
};
let ir = language
.parse(&module)
.map_err(|e| PipelineError::new("FW_EXPR_PARSE", e.to_string()))?;
let compiled = language
.compile(
&ir,
&CompileOptions {
target: COMPILE_TARGET_BACKEND.to_string(),
optimize_level: 1,
emit_trace_hints: false,
},
)
.map_err(|e| PipelineError::new("FW_EXPR_COMPILE", e.to_string()))?;
let scope_input = build_expression_scope_input(input, metadata);
let mut patched_metadata = metadata.clone();
if let Value::Object(ref mut map) = patched_metadata {
map.insert(
"languageRunPatch".to_string(),
json!({ "capabilities": [], "maxOps": 500 }),
);
}
let expr_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: patched_metadata,
};
let run_output = language
.run(&compiled, scope_input, &expr_ctx)
.map_err(|e| {
PipelineError::new(
crate::pipeline::error_class::wrapper_for(
&e.code,
"FW_EXPR_RUN",
"FW_EXPR_ENGINE",
),
e.to_string(),
)
})?;
let result_map = match run_output.value {
Value::Object(m) => m,
_ => return Ok(config), };
for (i, expr) in exprs.iter().enumerate() {
let Some(message) = result_map.get(&format!("x{i}")).and_then(Value::as_str) else {
continue;
};
let where_used = fields
.iter()
.find(|f| {
f.segments
.iter()
.any(|seg| matches!(seg, Segment::Expr(e) if e == expr))
})
.map(|f| f.ptr.clone())
.unwrap_or_default();
return Err(PipelineError::new(
"FW_EXPR_EVAL",
format!("expression {{{{ {expr} }}}} at config{where_used} failed: {message}"),
));
}
for field in &fields {
let replacement = build_replacement(field, &exprs, &result_map);
apply_json_ptr(&mut config, &field.ptr, replacement);
}
Ok(config)
}
fn build_replacement(
field: &ExprField,
exprs: &[String],
results: &serde_json::Map<String, Value>,
) -> Value {
if field.is_whole {
let Segment::Expr(expr) = &field.segments[0] else {
return Value::Null;
};
let idx = exprs.iter().position(|e| e == expr).unwrap_or(0);
results
.get(&format!("e{idx}"))
.cloned()
.unwrap_or(Value::Null)
} else {
let mut s = String::new();
for seg in &field.segments {
match seg {
Segment::Literal(lit) => s.push_str(lit),
Segment::Expr(expr) => {
let idx = exprs.iter().position(|e| e == expr).unwrap_or(0);
let val = results.get(&format!("e{idx}")).unwrap_or(&Value::Null);
let rendered = match val {
Value::String(sv) => sv.clone(),
Value::Null => String::new(),
other => other.to_string(),
};
s.push_str(&rendered);
}
}
}
Value::String(s)
}
}
fn apply_json_ptr(root: &mut Value, ptr: &str, value: Value) {
if ptr.is_empty() {
return;
}
let tokens: Vec<String> = ptr
.trim_start_matches('/')
.split('/')
.map(|s| s.replace("~1", "/").replace("~0", "~"))
.collect();
if tokens.is_empty() {
return;
}
let Some((last, parents)) = tokens.split_last() else {
return;
};
let parent = parents
.iter()
.try_fold(root as &mut Value, |node, tok| match node {
Value::Object(map) => Some(map.entry(tok.clone()).or_insert(Value::Null)),
Value::Array(arr) => tok.parse::<usize>().ok().and_then(|i| arr.get_mut(i)),
_ => None,
});
let Some(parent) = parent else {
return;
};
match parent {
Value::Object(map) => {
map.insert(last.clone(), value);
}
Value::Array(arr) => {
if let Ok(i) = last.parse::<usize>() {
if i < arr.len() {
arr[i] = value;
}
}
}
_ => {}
}
}
fn hash_str(s: &str) -> String {
let mut h = DefaultHasher::new();
s.hash(&mut h);
format!("{:x}", h.finish())
}