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::InvalidScript {
message: e.caller_message(),
})?;
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: crate::executor::ResolvedPath<'_>,
_body: Option<serde_json::Value>,
) -> Result<serde_json::Value, ExecutionError> {
Err(ExecutionError::RuntimeError {
message: format!(
"HTTP calls not supported in this executor mode (attempted a {method} request). \
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,
}))
}
}
#[cfg(feature = "js-runtime")]
async fn compile_and_run(code: &str) -> Result<serde_json::Value, ExecutionError> {
use crate::executor::{ExecutionConfig, MockHttpExecutor};
compile_and_execute(
&ExecutionConfig::default(),
MockHttpExecutor::new_dry_run(),
code,
None,
|_| {},
"test",
)
.await
}
#[cfg(feature = "js-runtime")]
#[tokio::test]
async fn a_script_that_does_not_compile_is_invalid_script_not_a_runtime_fault() {
let err = compile_and_run("const r = await api.get('/search/' + args.q); return r;")
.await
.expect_err("a concatenated path does not compile");
assert!(
matches!(err, ExecutionError::InvalidScript { .. }),
"a compile error is the caller's, got: {err:?}"
);
assert!(
err.to_string().contains("template literal"),
"the model must read what to change: {err}"
);
}
#[cfg(feature = "js-runtime")]
#[tokio::test]
async fn a_syntax_error_does_not_echo_the_submitted_code() {
let err = compile_and_run("const SECRETTOKEN = = 1;")
.await
.expect_err("a syntax error");
match err {
ExecutionError::InvalidScript { message } => {
assert!(
!message.contains("SECRETTOKEN"),
"echoed the code: {message}"
);
assert!(message.contains("syntax error"), "{message}");
},
other => panic!("expected InvalidScript, got {other:?}"),
}
}
#[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>();
}
#[cfg(feature = "js-runtime")]
#[tokio::test]
async fn noop_http_executor_error_names_the_method_and_not_the_path() {
use crate::executor::{HttpExecutor, ResolvedPath};
let path = ResolvedPath::from_checked("/secret/inventory/endpoint")
.expect("a clean resolved path");
let rendered = NoopHttpExecutor
.execute_request("GET", path, None)
.await
.expect_err("the noop executor always refuses")
.to_string();
assert!(rendered.contains("GET"), "must name the method: {rendered}");
assert!(
!rendered.contains("/secret/inventory/endpoint"),
"must NOT echo the resolved path: {rendered}"
);
}
}