1use std::collections::HashMap;
2use std::marker::PhantomData;
3use std::sync::Arc;
4
5use async_trait::async_trait;
6use rskit_errors::AppResult;
7use rskit_provider::{Provider, RequestResponse};
8use tokio_util::sync::CancellationToken;
9
10use crate::Dag;
11
12pub struct ToolConfig<I, O> {
14 pub name: &'static str,
16 pub input_fn: Box<dyn Fn(I) -> HashMap<String, serde_json::Value> + Send + Sync>,
18 pub output_fn: Box<dyn Fn(HashMap<String, serde_json::Value>) -> AppResult<O> + Send + Sync>,
20}
21
22pub struct DagTool<I, O> {
24 dag: Arc<Dag>,
25 config: ToolConfig<I, O>,
26 _phantom: PhantomData<fn(I) -> O>,
27}
28
29pub fn as_tool<I, O>(dag: Arc<Dag>, config: ToolConfig<I, O>) -> DagTool<I, O>
34where
35 I: Send + 'static,
36 O: Send + 'static,
37{
38 DagTool {
39 dag,
40 config,
41 _phantom: PhantomData,
42 }
43}
44
45impl<I: Send + 'static, O: Send + 'static> Provider for DagTool<I, O> {
46 fn name(&self) -> &'static str {
47 self.config.name
48 }
49}
50
51#[async_trait]
52impl<I, O> RequestResponse<I, O> for DagTool<I, O>
53where
54 I: Send + 'static,
55 O: Send + 'static,
56{
57 async fn execute(&self, input: I) -> AppResult<O> {
58 let initial_inputs = (self.config.input_fn)(input);
59 let cancel = CancellationToken::new();
60 let outputs = self.dag.execute_with_inputs(initial_inputs, cancel).await?;
61 (self.config.output_fn)(outputs)
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68 use crate::DagNode;
69 use rskit_errors::{AppError, ErrorCode};
70 use std::future::Future;
71 use std::pin::Pin;
72
73 struct EchoNode {
74 id: String,
75 }
76
77 impl DagNode for EchoNode {
78 fn id(&self) -> &str {
79 &self.id
80 }
81
82 fn execute(
83 &self,
84 inputs: HashMap<String, serde_json::Value>,
85 _cancel: CancellationToken,
86 ) -> Pin<Box<dyn Future<Output = AppResult<serde_json::Value>> + Send + '_>> {
87 Box::pin(async move { Ok(serde_json::to_value(&inputs).unwrap()) })
88 }
89 }
90
91 struct AddNode {
92 id: String,
93 value: i64,
94 }
95
96 impl DagNode for AddNode {
97 fn id(&self) -> &str {
98 &self.id
99 }
100
101 fn execute(
102 &self,
103 inputs: HashMap<String, serde_json::Value>,
104 _cancel: CancellationToken,
105 ) -> Pin<Box<dyn Future<Output = AppResult<serde_json::Value>> + Send + '_>> {
106 Box::pin(async move {
107 let sum: i64 = inputs
108 .values()
109 .filter_map(serde_json::Value::as_i64)
110 .sum::<i64>()
111 + self.value;
112 Ok(serde_json::json!(sum))
113 })
114 }
115 }
116
117 struct FailNode {
118 id: String,
119 }
120
121 impl DagNode for FailNode {
122 fn id(&self) -> &str {
123 &self.id
124 }
125
126 fn execute(
127 &self,
128 _inputs: HashMap<String, serde_json::Value>,
129 _cancel: CancellationToken,
130 ) -> Pin<Box<dyn Future<Output = AppResult<serde_json::Value>> + Send + '_>> {
131 Box::pin(async { Err(AppError::new(ErrorCode::Internal, "node failed")) })
132 }
133 }
134
135 #[tokio::test]
136 async fn execute_with_inputs_root_receives_inputs() {
137 let dag = Dag::new().add_node(EchoNode { id: "root".into() });
138
139 let cancel = CancellationToken::new();
140 let mut initial = HashMap::new();
141 initial.insert("x".to_owned(), serde_json::json!(42));
142
143 let outputs = dag.execute_with_inputs(initial, cancel).await.unwrap();
144 let root_output: HashMap<String, serde_json::Value> =
145 serde_json::from_value(outputs["root"].clone()).unwrap();
146 assert_eq!(root_output["x"], serde_json::json!(42));
147 }
148
149 #[tokio::test]
150 async fn as_tool_basic_execution() {
151 let dag = Arc::new(
152 Dag::new()
153 .add_node(AddNode {
154 id: "a".into(),
155 value: 0,
156 })
157 .add_node(AddNode {
158 id: "b".into(),
159 value: 5,
160 })
161 .add_edge("a", "b")
162 .unwrap(),
163 );
164
165 let tool = as_tool(
166 dag,
167 ToolConfig {
168 name: "add-pipeline",
169 input_fn: Box::new(|input: i64| {
170 let mut m = HashMap::new();
171 m.insert("seed".to_owned(), serde_json::json!(input));
172 m
173 }),
174 output_fn: Box::new(|outputs| {
175 outputs["b"]
176 .as_i64()
177 .ok_or_else(|| AppError::new(ErrorCode::Internal, "missing b"))
178 }),
179 },
180 );
181
182 let result = RequestResponse::execute(&tool, 10).await.unwrap();
183 assert_eq!(result, 15);
185 }
186
187 #[tokio::test]
188 async fn as_tool_provider_name() {
189 let dag = Arc::new(Dag::new().add_node(EchoNode { id: "n".into() }));
190
191 let tool: DagTool<(), ()> = as_tool(
192 dag,
193 ToolConfig {
194 name: "my-tool",
195 input_fn: Box::new(|()| HashMap::new()),
196 output_fn: Box::new(|_| Ok(())),
197 },
198 );
199
200 assert_eq!(Provider::name(&tool), "my-tool");
201 }
202
203 #[tokio::test]
204 async fn as_tool_error_propagation() {
205 let dag = Arc::new(Dag::new().add_node(FailNode { id: "fail".into() }));
206
207 let tool = as_tool(
208 dag,
209 ToolConfig {
210 name: "failing",
211 input_fn: Box::new(|()| HashMap::new()),
212 output_fn: Box::new(|_| Ok(())),
213 },
214 );
215
216 let result = RequestResponse::execute(&tool, ()).await;
217 assert!(result.is_err());
218 assert_eq!(result.unwrap_err().code(), ErrorCode::Internal);
219 }
220}