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