use std::sync::Arc;
use async_trait::async_trait;
use dataflow_rs::engine::error::DataflowError;
use dataflow_rs::engine::functions::AsyncFunctionHandler;
use dataflow_rs::engine::message::Message;
use dataflow_rs::engine::task_context::TaskContext;
use dataflow_rs::engine::task_outcome::TaskOutcome;
use dataflow_rs::{Template, TemplateCompiler};
use serde::Deserialize;
use serde_json::Value;
use super::schema::{FieldKind, FieldSchema};
const META_CALL_DEPTH: &str = "_orion_call_depth";
const META_CALL_CHAIN: &str = "_orion_call_chain";
#[derive(Debug, Deserialize)]
pub struct ChannelCallInput {
#[serde(default)]
pub channel: String,
#[serde(default)]
pub channel_logic: Option<Template>,
#[serde(default, alias = "response_path")]
pub output: Option<String>,
#[serde(default)]
pub data: Option<Value>,
#[serde(default)]
pub data_logic: Option<Template>,
#[serde(default)]
pub timeout_ms: Option<u64>,
}
pub struct ChannelCallHandler {
pub engine: Arc<crate::engine::EngineHandle>,
pub channel_registry: Arc<crate::channel::ChannelRegistry>,
pub max_call_depth: u32,
pub default_timeout_ms: u64,
}
#[async_trait]
impl AsyncFunctionHandler for ChannelCallHandler {
type Input = ChannelCallInput;
fn compile_input(input: &mut Self::Input, c: &TemplateCompiler) -> dataflow_rs::Result<()> {
if let Some(t) = input.channel_logic.as_mut() {
t.compile(c, "channel_call.channel_logic")?;
}
if let Some(t) = input.data_logic.as_mut() {
t.compile(c, "channel_call.data_logic")?;
}
Ok(())
}
async fn execute(
&self,
ctx: &mut TaskContext<'_>,
input: &ChannelCallInput,
) -> dataflow_rs::Result<TaskOutcome> {
let target_channel = resolve_target(ctx, input)?;
let label = target_channel.clone();
crate::engine::profile::record("channel_call", Some(&label), async move {
let parent_depth = ctx
.message()
.metadata()
.get(META_CALL_DEPTH)
.and_then(|v| v.as_i64())
.map(|n| n as u64)
.unwrap_or(0);
let parent_chain: Vec<String> = ctx
.message()
.metadata()
.get(META_CALL_CHAIN)
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
if parent_depth >= self.max_call_depth as u64 {
return Err(DataflowError::Validation(format!(
"channel_call: max call depth {} exceeded (chain: {})",
self.max_call_depth,
format_chain(&parent_chain, &target_channel),
)));
}
if parent_chain.contains(&target_channel) {
return Err(DataflowError::Validation(format!(
"channel_call: cycle detected: {}",
format_chain(&parent_chain, &target_channel),
)));
}
let call_data: Value = if let Some(ref logic) = input.data_logic {
logic.eval_into(ctx)?
} else if let Some(ref data) = input.data {
data.clone()
} else {
(&*ctx.message().payload_arc().clone()).into()
};
let child_depth = parent_depth + 1;
let mut child_chain = parent_chain;
child_chain.push(target_channel.clone());
let mut child_meta: Value = ctx.message().metadata().into();
if !child_meta.is_object() {
child_meta = serde_json::json!({});
}
let calling_channel = child_meta
.get("channel")
.and_then(Value::as_str)
.unwrap_or("channel_call")
.to_string();
child_meta["channel"] = Value::String(target_channel.clone());
child_meta[META_CALL_DEPTH] = serde_json::json!(child_depth);
child_meta[META_CALL_CHAIN] = serde_json::json!(child_chain);
let target_runtime = self
.channel_registry
.require_serviceable(&target_channel)
.map_err(|e| {
DataflowError::function_execution(
format!("channel_call to '{target_channel}': {e}"),
None,
)
})?;
if target_runtime.is_none() {
return Err(DataflowError::function_execution(
format!("channel_call to '{target_channel}': channel not found or not active"),
None,
));
}
let header_lookup = |name: &str| {
child_meta
.get("headers")
.and_then(|h| h.get(name))
.and_then(Value::as_str)
.map(str::to_string)
};
let admission = crate::channel::guards::admit(crate::channel::guards::GuardRequest {
transport: crate::channel::guards::Transport::ChannelCall,
channel: &target_channel,
runtime: &target_runtime,
data: &call_data,
metadata: &child_meta,
datalogic: ctx.datalogic(),
origin: None,
caller_identity: &calling_channel,
header: &header_lookup,
raw_body: None,
dedup_key_fallback: None,
dedup_owner: None,
default_timeout_ms: Some(self.default_timeout_ms),
max_timeout_ms: None,
})
.await
.map_err(|e| guard_refusal(&target_channel, e))?;
let _backpressure_permit = admission.backpressure_permit;
let mut child_message = Message::builder()
.payload_json(&call_data)
.metadata_json(&child_meta)
.build();
let engine = self.engine.load();
let timeout_ms = input
.timeout_ms
.or(admission.timeout_ms)
.unwrap_or(self.default_timeout_ms);
match crate::engine::run_for_channel(
&engine,
&target_channel,
&mut child_message,
Some(timeout_ms),
None,
None,
)
.await
{
Ok((inner, _)) => inner.map_err(|e| {
DataflowError::function_execution(
format!("channel_call to '{target_channel}' failed: {e}"),
None,
)
})?,
Err(ms) => {
return Err(DataflowError::Timeout(format!(
"channel_call to '{target_channel}' timed out after {ms}ms"
)));
}
}
let result_data_json: Value = child_message.data().into();
let output = input.output.as_deref().unwrap_or("data");
ctx.set_json(output, &result_data_json);
Ok(TaskOutcome::Success)
})
.await
}
}
fn resolve_target(ctx: &TaskContext<'_>, input: &ChannelCallInput) -> dataflow_rs::Result<String> {
let target = if let Some(ref logic) = input.channel_logic {
let result: Value = logic.eval_into(ctx)?;
result.as_str().map(|s| s.to_string()).ok_or_else(|| {
DataflowError::Validation("channel_logic must evaluate to a string".to_string())
})?
} else {
input.channel.clone()
};
if target.is_empty() {
return Err(DataflowError::Validation(
"channel_call: target channel name must not be empty".into(),
));
}
Ok(target)
}
fn guard_refusal(target: &str, e: crate::errors::OrionError) -> DataflowError {
let (status, _code, detail) = e.response_parts();
let message = format!("channel_call to '{target}': {detail}");
if status == axum::http::StatusCode::BAD_REQUEST {
DataflowError::Validation(message)
} else {
crate::errors::channel_refused_dataflow_error(status, message)
}
}
fn format_chain(chain: &[String], target: &str) -> String {
let mut parts: Vec<&str> = chain.iter().map(|s| s.as_str()).collect();
parts.push(target);
parts.join(" -> ")
}
pub(super) const CHANNEL_CALL_FIELDS: &[FieldSchema] = &[
FieldSchema {
name: "channel",
description: "Target channel name to invoke. Mutually exclusive with channel_logic.",
kind: FieldKind::String,
required: false,
resolvable: false,
alias: None,
},
FieldSchema {
name: "channel_logic",
description: "JSONLogic expression evaluating to the target channel name.",
kind: FieldKind::Any,
required: false,
resolvable: false,
alias: None,
},
FieldSchema {
name: "data",
description: "Static payload to pass to the target channel.",
kind: FieldKind::Any,
required: false,
resolvable: false,
alias: None,
},
FieldSchema {
name: "data_logic",
description: "JSONLogic expression evaluating to the payload to pass.",
kind: FieldKind::Any,
required: false,
resolvable: false,
alias: None,
},
FieldSchema {
name: "output",
description: "Dotted path where the called channel's response is stored. Defaults to \"data\". (Was `response_path` before 1.0; still accepted.)",
kind: FieldKind::String,
required: false,
resolvable: false,
alias: None,
},
FieldSchema {
name: "timeout_ms",
description: "Per-call timeout in milliseconds.",
kind: FieldKind::Number,
required: false,
resolvable: false,
alias: None,
},
];