use std::collections::HashMap;
use std::marker::PhantomData;
use std::sync::Arc;
use async_trait::async_trait;
use rskit_errors::AppResult;
use rskit_provider::{Provider, RequestResponse};
use tokio_util::sync::CancellationToken;
use crate::Dag;
pub struct ToolConfig<I, O> {
pub name: &'static str,
pub input_fn: Box<dyn Fn(I) -> HashMap<String, serde_json::Value> + Send + Sync>,
pub output_fn: Box<dyn Fn(HashMap<String, serde_json::Value>) -> AppResult<O> + Send + Sync>,
}
pub struct DagTool<I, O> {
dag: Arc<Dag>,
config: ToolConfig<I, O>,
_phantom: PhantomData<fn(I) -> O>,
}
pub fn as_tool<I, O>(dag: Arc<Dag>, config: ToolConfig<I, O>) -> DagTool<I, O>
where
I: Send + 'static,
O: Send + 'static,
{
DagTool {
dag,
config,
_phantom: PhantomData,
}
}
impl<I: Send + 'static, O: Send + 'static> Provider for DagTool<I, O> {
fn name(&self) -> &'static str {
self.config.name
}
}
#[async_trait]
impl<I, O> RequestResponse<I, O> for DagTool<I, O>
where
I: Send + 'static,
O: Send + 'static,
{
async fn execute(&self, input: I) -> AppResult<O> {
let initial_inputs = (self.config.input_fn)(input);
let cancel = CancellationToken::new();
let outputs = self.dag.execute_with_inputs(initial_inputs, cancel).await?;
(self.config.output_fn)(outputs)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::DagNode;
use rskit_errors::{AppError, ErrorCode};
use std::future::Future;
use std::pin::Pin;
struct EchoNode {
id: String,
}
impl DagNode for EchoNode {
fn id(&self) -> &str {
&self.id
}
fn execute(
&self,
inputs: HashMap<String, serde_json::Value>,
_cancel: CancellationToken,
) -> Pin<Box<dyn Future<Output = AppResult<serde_json::Value>> + Send + '_>> {
Box::pin(async move { Ok(serde_json::to_value(&inputs).unwrap()) })
}
}
struct AddNode {
id: String,
value: i64,
}
impl DagNode for AddNode {
fn id(&self) -> &str {
&self.id
}
fn execute(
&self,
inputs: HashMap<String, serde_json::Value>,
_cancel: CancellationToken,
) -> Pin<Box<dyn Future<Output = AppResult<serde_json::Value>> + Send + '_>> {
Box::pin(async move {
let sum: i64 = inputs
.values()
.filter_map(serde_json::Value::as_i64)
.sum::<i64>()
+ self.value;
Ok(serde_json::json!(sum))
})
}
}
struct FailNode {
id: String,
}
impl DagNode for FailNode {
fn id(&self) -> &str {
&self.id
}
fn execute(
&self,
_inputs: HashMap<String, serde_json::Value>,
_cancel: CancellationToken,
) -> Pin<Box<dyn Future<Output = AppResult<serde_json::Value>> + Send + '_>> {
Box::pin(async { Err(AppError::new(ErrorCode::Internal, "node failed")) })
}
}
#[tokio::test]
async fn execute_with_inputs_root_receives_inputs() {
let dag = Dag::new().add_node(EchoNode { id: "root".into() });
let cancel = CancellationToken::new();
let mut initial = HashMap::new();
initial.insert("x".to_owned(), serde_json::json!(42));
let outputs = dag.execute_with_inputs(initial, cancel).await.unwrap();
let root_output: HashMap<String, serde_json::Value> =
serde_json::from_value(outputs["root"].clone()).unwrap();
assert_eq!(root_output["x"], serde_json::json!(42));
}
#[tokio::test]
async fn as_tool_basic_execution() {
let dag = Arc::new(
Dag::new()
.add_node(AddNode {
id: "a".into(),
value: 0,
})
.add_node(AddNode {
id: "b".into(),
value: 5,
})
.add_edge("a", "b")
.unwrap(),
);
let tool = as_tool(
dag,
ToolConfig {
name: "add-pipeline",
input_fn: Box::new(|input: i64| {
let mut m = HashMap::new();
m.insert("seed".to_owned(), serde_json::json!(input));
m
}),
output_fn: Box::new(|outputs| {
outputs["b"]
.as_i64()
.ok_or_else(|| AppError::new(ErrorCode::Internal, "missing b"))
}),
},
);
let result = RequestResponse::execute(&tool, 10).await.unwrap();
assert_eq!(result, 15);
}
#[tokio::test]
async fn as_tool_provider_name() {
let dag = Arc::new(Dag::new().add_node(EchoNode { id: "n".into() }));
let tool: DagTool<(), ()> = as_tool(
dag,
ToolConfig {
name: "my-tool",
input_fn: Box::new(|()| HashMap::new()),
output_fn: Box::new(|_| Ok(())),
},
);
assert_eq!(Provider::name(&tool), "my-tool");
}
#[tokio::test]
async fn as_tool_error_propagation() {
let dag = Arc::new(Dag::new().add_node(FailNode { id: "fail".into() }));
let tool = as_tool(
dag,
ToolConfig {
name: "failing",
input_fn: Box::new(|()| HashMap::new()),
output_fn: Box::new(|_| Ok(())),
},
);
let result = RequestResponse::execute(&tool, ()).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err().code(), ErrorCode::Internal);
}
}