Skip to main content

rskit_dag/
as_tool.rs

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
12/// Configuration for wrapping a DAG as a [`RequestResponse`] provider.
13pub struct ToolConfig<I, O> {
14    /// Provider name.
15    pub name: &'static str,
16    /// Maps input to initial inputs for root DAG nodes.
17    pub input_fn: Box<dyn Fn(I) -> HashMap<String, serde_json::Value> + Send + Sync>,
18    /// Extracts output from DAG execution results.
19    pub output_fn: Box<dyn Fn(HashMap<String, serde_json::Value>) -> AppResult<O> + Send + Sync>,
20}
21
22/// Wraps a DAG execution as a [`RequestResponse`] provider.
23pub struct DagTool<I, O> {
24    dag: Arc<Dag>,
25    config: ToolConfig<I, O>,
26    _phantom: PhantomData<fn(I) -> O>,
27}
28
29/// Wrap a DAG execution as a [`RequestResponse`] provider.
30///
31/// The returned [`DagTool`] maps an input of type `I` into initial DAG inputs
32/// via `config.input_fn`, executes the DAG, then extracts an output of type `O`
33/// via `config.output_fn`.
34pub 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        // a receives {"seed": 10} → sums to 10, b receives {"a": 10} → 10 + 5 = 15
181        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}