use crate::types::ExecutionError;
#[async_trait::async_trait]
pub trait CodeExecutor: Send + Sync {
async fn execute(
&self,
code: &str,
variables: Option<&serde_json::Value>,
) -> Result<serde_json::Value, ExecutionError>;
}
#[cfg(feature = "js-runtime")]
async fn compile_and_execute<H: crate::executor::HttpExecutor + 'static>(
config: &crate::executor::ExecutionConfig,
http: H,
code: &str,
variables: Option<&serde_json::Value>,
setup: impl FnOnce(&mut crate::executor::PlanExecutor<H>),
adapter: &str,
) -> Result<serde_json::Value, ExecutionError> {
let mut compiler = crate::executor::PlanCompiler::with_config(config);
let plan = compiler
.compile_code(code)
.map_err(|e| ExecutionError::RuntimeError {
message: format!("Compilation failed: {e}"),
})?;
let mut executor = crate::executor::PlanExecutor::new(http, config.clone());
if let Some(vars) = variables {
executor.set_variable("args", vars.clone());
}
setup(&mut executor);
let result = executor.execute(&plan).await?;
tracing::debug!(
adapter,
api_calls = result.api_calls.len(),
execution_time_ms = result.execution_time_ms,
"plan executed"
);
Ok(result.value)
}
#[cfg(feature = "js-runtime")]
pub struct JsCodeExecutor<H> {
http: H,
config: crate::executor::ExecutionConfig,
}
#[cfg(feature = "js-runtime")]
impl<H: crate::executor::HttpExecutor + Clone> JsCodeExecutor<H> {
pub fn new(http: H, config: crate::executor::ExecutionConfig) -> Self {
Self { http, config }
}
}
#[cfg(feature = "js-runtime")]
#[async_trait::async_trait]
impl<H: crate::executor::HttpExecutor + Clone + 'static> CodeExecutor for JsCodeExecutor<H> {
async fn execute(
&self,
code: &str,
variables: Option<&serde_json::Value>,
) -> Result<serde_json::Value, ExecutionError> {
compile_and_execute(
&self.config,
self.http.clone(),
code,
variables,
|_| {},
"js",
)
.await
}
}
#[cfg(feature = "js-runtime")]
pub struct SdkCodeExecutor<S> {
sdk: S,
config: crate::executor::ExecutionConfig,
}
#[cfg(feature = "js-runtime")]
impl<S: crate::executor::SdkExecutor + Clone + 'static> SdkCodeExecutor<S> {
pub fn new(sdk: S, config: crate::executor::ExecutionConfig) -> Self {
Self { sdk, config }
}
}
#[cfg(feature = "js-runtime")]
#[async_trait::async_trait]
impl<S: crate::executor::SdkExecutor + Clone + 'static> CodeExecutor for SdkCodeExecutor<S> {
async fn execute(
&self,
code: &str,
variables: Option<&serde_json::Value>,
) -> Result<serde_json::Value, ExecutionError> {
let sdk = self.sdk.clone();
compile_and_execute(
&self.config,
NoopHttpExecutor,
code,
variables,
move |ex| {
ex.set_sdk_executor(sdk);
},
"sdk",
)
.await
}
}
#[cfg(feature = "mcp-code-mode")]
pub struct McpCodeExecutor<M> {
mcp: M,
config: crate::executor::ExecutionConfig,
}
#[cfg(feature = "mcp-code-mode")]
impl<M: crate::executor::McpExecutor + Clone + 'static> McpCodeExecutor<M> {
pub fn new(mcp: M, config: crate::executor::ExecutionConfig) -> Self {
Self { mcp, config }
}
}
#[cfg(feature = "mcp-code-mode")]
#[async_trait::async_trait]
impl<M: crate::executor::McpExecutor + Clone + 'static> CodeExecutor for McpCodeExecutor<M> {
async fn execute(
&self,
code: &str,
variables: Option<&serde_json::Value>,
) -> Result<serde_json::Value, ExecutionError> {
let mcp = self.mcp.clone();
compile_and_execute(
&self.config,
NoopHttpExecutor,
code,
variables,
move |ex| {
ex.set_mcp_executor(mcp);
},
"mcp",
)
.await
}
}
#[cfg(feature = "js-runtime")]
#[derive(Clone)]
struct NoopHttpExecutor;
#[cfg(feature = "js-runtime")]
#[async_trait::async_trait]
impl crate::executor::HttpExecutor for NoopHttpExecutor {
async fn execute_request(
&self,
method: &str,
path: &str,
_body: Option<serde_json::Value>,
) -> Result<serde_json::Value, ExecutionError> {
Err(ExecutionError::RuntimeError {
message: format!(
"HTTP calls not supported in this executor mode (attempted {method} {path}). \
Use JsCodeExecutor for HTTP-based execution."
),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
struct EchoExecutor;
#[async_trait::async_trait]
impl CodeExecutor for EchoExecutor {
async fn execute(
&self,
code: &str,
variables: Option<&serde_json::Value>,
) -> Result<serde_json::Value, ExecutionError> {
Ok(json!({
"code": code,
"variables": variables,
}))
}
}
#[tokio::test]
async fn code_executor_echo() {
let executor = EchoExecutor;
let result = executor.execute("SELECT 1", None).await.unwrap();
assert_eq!(result["code"], "SELECT 1");
}
#[tokio::test]
async fn code_executor_with_variables() {
let executor = EchoExecutor;
let vars = json!({"limit": 10});
let result = executor
.execute("query { users }", Some(&vars))
.await
.unwrap();
assert_eq!(result["variables"]["limit"], 10);
}
#[tokio::test]
async fn code_executor_returns_error() {
struct FailingExecutor;
#[async_trait::async_trait]
impl CodeExecutor for FailingExecutor {
async fn execute(
&self,
_code: &str,
_variables: Option<&serde_json::Value>,
) -> Result<serde_json::Value, ExecutionError> {
Err(ExecutionError::BackendError(
"database unavailable".to_string(),
))
}
}
let executor = FailingExecutor;
let result = executor.execute("SELECT 1", None).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("database unavailable"));
}
fn _assert_send_sync<T: Send + Sync>() {}
fn _code_executor_is_send_sync() {
_assert_send_sync::<EchoExecutor>();
}
}