use crate::core::graph::WorkflowGraph;
use anyhow::Result;
use serde_json::{json, Value};
use std::collections::HashMap;
use tracing::{debug, warn};
pub fn expand_decorators(wf: &mut WorkflowGraph) -> Result<()> {
if wf.decorator_applications.is_empty() {
return Ok(());
}
let applications = wf.decorator_applications.clone();
debug!(
"[macro_expand] Processing {} decorator application(s)",
applications.len()
);
for app in &applications {
debug!(
"[macro_expand] @{}({:?}) on [{}]",
app.decorator_fn, app.args, app.target_node_id
);
let deco_fn = wf.functions.get(&app.decorator_fn).cloned();
if let Some(deco_fn_def) = deco_fn {
let annotations =
extract_annotations_from_body(&deco_fn_def.body, &deco_fn_def.params, &app.args);
if !annotations.is_empty() {
if let Some(target_fn) = wf.functions.get_mut(&app.target_node_id) {
for (key, value) in &annotations {
debug!(
"[macro_expand] Annotating [{}] with {}: {}",
app.target_node_id, key, value
);
target_fn.annotations.insert(key.clone(), value.clone());
}
} else {
warn!(
"[macro_expand] Target function '{}' not found for @{}",
app.target_node_id, app.decorator_fn
);
}
}
} else {
let annotations = resolve_builtin_decorator(&app.decorator_fn, &app.args);
if !annotations.is_empty() {
if let Some(target_fn) = wf.functions.get_mut(&app.target_node_id) {
for (key, value) in &annotations {
debug!(
"[macro_expand] Built-in @{} annotating [{}] with {}: {}",
app.decorator_fn, app.target_node_id, key, value
);
target_fn.annotations.insert(key.clone(), value.clone());
}
}
} else {
warn!(
"[macro_expand] Decorator function '{}' not found",
app.decorator_fn
);
}
}
}
wf.decorator_applications.clear();
Ok(())
}
fn extract_annotations_from_body(
body: &WorkflowGraph,
params: &[String],
args: &[String],
) -> HashMap<String, Value> {
let mut annotations = HashMap::new();
let param_map: HashMap<&str, &str> = params
.iter()
.filter(|p| *p != "item") .enumerate()
.filter_map(|(i, p)| args.get(i).map(|a| (p.as_str(), a.as_str())))
.collect();
for (_idx, node) in body.graph.node_indices().map(|i| (i, &body.graph[i])) {
let action = match &node.node_type {
crate::core::graph::NodeType::Task(a) => Some(a),
crate::core::graph::NodeType::AssignCall { action, .. } => Some(action),
_ => None,
};
if let Some(action) = action {
let is_annotate = action.name == "annotate" || action.name.ends_with(".annotate");
if !is_annotate {
continue;
}
let key_raw = action
.params
.get("key")
.or_else(|| action.params.get("arg0"));
let val_raw = action
.params
.get("value")
.or_else(|| action.params.get("arg1"));
if let (Some(key_raw), Some(val_raw)) = (key_raw, val_raw) {
let key = resolve_param(key_raw, ¶m_map);
let val = resolve_param_to_value(val_raw, ¶m_map);
annotations.insert(key, val);
}
}
}
annotations
}
fn resolve_param(raw: &str, param_map: &HashMap<&str, &str>) -> String {
let trimmed = raw.trim().trim_matches('"');
if let Some(val) = param_map.get(trimmed) {
val.trim_matches('"').to_string()
} else {
trimmed.to_string()
}
}
fn resolve_param_to_value(raw: &str, param_map: &HashMap<&str, &str>) -> Value {
let trimmed = raw.trim();
if trimmed.starts_with('{') {
let mut resolved = trimmed.to_string();
for (param, arg) in param_map {
let arg_clean = arg.trim_matches('"');
let patterns = [
(format!(": {}", param), format!(": \"{}\"", arg_clean)),
(format!(":{}", param), format!(":\"{}\"", arg_clean)),
(format!(" {} ", param), format!(" \"{}\" ", arg_clean)),
(format!(" {}}}", param), format!(" \"{}\"}}", arg_clean)),
(format!(" {},", param), format!(" \"{}\",", arg_clean)),
];
for (from, to) in &patterns {
resolved = resolved.replace(from.as_str(), to.as_str());
}
}
debug!(
"[macro_expand] resolve_param_to_value: resolved JSON = {}",
resolved
);
if let Ok(val) = serde_json::from_str::<Value>(&resolved) {
return val;
}
}
if let Some(val) = param_map.get(trimmed) {
let clean = val.trim_matches('"');
if let Ok(v) = serde_json::from_str::<Value>(clean) {
return v;
}
return json!(clean);
}
let clean = trimmed.trim_matches('"');
json!(clean)
}
fn resolve_builtin_decorator(name: &str, args: &[String]) -> HashMap<String, Value> {
let mut annotations = HashMap::new();
match name {
"get" | "post" | "put" | "delete" | "patch" | "head" | "options" => {
let method = name.to_uppercase();
let path = args
.first()
.map(|s| s.trim_matches('"').to_string())
.unwrap_or_else(|| "/".to_string());
annotations.insert(
"route".to_string(),
json!({ "method": method, "path": path }),
);
}
"test" => {
annotations.insert("test".to_string(), json!(true));
}
"cron" => {
if let Some(schedule) = args.first() {
annotations.insert(
"cron".to_string(),
json!({ "schedule": schedule.trim_matches('"') }),
);
}
}
_ => {}
}
annotations
}