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.values().filter_map(|v| v.as_i64()).sum::<i64>() + self.value;
109 Ok(serde_json::json!(sum))
110 })
111 }
112 }
113
114 struct FailNode {
115 id: String,
116 }
117
118 impl DagNode for FailNode {
119 fn id(&self) -> &str {
120 &self.id
121 }
122
123 fn execute(
124 &self,
125 _inputs: HashMap<String, serde_json::Value>,
126 _cancel: CancellationToken,
127 ) -> Pin<Box<dyn Future<Output = AppResult<serde_json::Value>> + Send + '_>> {
128 Box::pin(async { Err(AppError::new(ErrorCode::Internal, "node failed")) })
129 }
130 }
131
132 #[tokio::test]
133 async fn execute_with_inputs_root_receives_inputs() {
134 let dag = Dag::new().add_node(EchoNode { id: "root".into() });
135
136 let cancel = CancellationToken::new();
137 let mut initial = HashMap::new();
138 initial.insert("x".to_owned(), serde_json::json!(42));
139
140 let outputs = dag.execute_with_inputs(initial, cancel).await.unwrap();
141 let root_output: HashMap<String, serde_json::Value> =
142 serde_json::from_value(outputs["root"].clone()).unwrap();
143 assert_eq!(root_output["x"], serde_json::json!(42));
144 }
145
146 #[tokio::test]
147 async fn as_tool_basic_execution() {
148 let dag = Arc::new(
149 Dag::new()
150 .add_node(AddNode {
151 id: "a".into(),
152 value: 0,
153 })
154 .add_node(AddNode {
155 id: "b".into(),
156 value: 5,
157 })
158 .add_edge("a", "b")
159 .unwrap(),
160 );
161
162 let tool = as_tool(
163 dag,
164 ToolConfig {
165 name: "add-pipeline",
166 input_fn: Box::new(|input: i64| {
167 let mut m = HashMap::new();
168 m.insert("seed".to_owned(), serde_json::json!(input));
169 m
170 }),
171 output_fn: Box::new(|outputs| {
172 outputs["b"]
173 .as_i64()
174 .ok_or_else(|| AppError::new(ErrorCode::Internal, "missing b"))
175 }),
176 },
177 );
178
179 let result = RequestResponse::execute(&tool, 10).await.unwrap();
180 assert_eq!(result, 15);
182 }
183
184 #[tokio::test]
185 async fn as_tool_provider_name() {
186 let dag = Arc::new(Dag::new().add_node(EchoNode { id: "n".into() }));
187
188 let tool: DagTool<(), ()> = as_tool(
189 dag,
190 ToolConfig {
191 name: "my-tool",
192 input_fn: Box::new(|_| HashMap::new()),
193 output_fn: Box::new(|_| Ok(())),
194 },
195 );
196
197 assert_eq!(Provider::name(&tool), "my-tool");
198 }
199
200 #[tokio::test]
201 async fn as_tool_error_propagation() {
202 let dag = Arc::new(Dag::new().add_node(FailNode { id: "fail".into() }));
203
204 let tool = as_tool(
205 dag,
206 ToolConfig {
207 name: "failing",
208 input_fn: Box::new(|_: ()| HashMap::new()),
209 output_fn: Box::new(|_| Ok(())),
210 },
211 );
212
213 let result = RequestResponse::execute(&tool, ()).await;
214 assert!(result.is_err());
215 assert_eq!(result.unwrap_err().code(), ErrorCode::Internal);
216 }
217}