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
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        // a receives {"seed": 10} → sums to 10, b receives {"a": 10} → 10 + 5 = 15
185        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}