floxide_transform/
lib.rs

1//! # Floxide Transform
2//!
3//! Transform node abstractions for the floxide framework.
4//!
5//! This crate provides the `TransformNode` trait and related utilities for working with
6//! transformation-oriented workflow nodes that follow a functional programming approach.
7//!
8//! ## Key Components
9//!
10//! - `TransformNode`: A trait for nodes that transform input data to output data
11//! - `TransformContext`: A simple context wrapper for TransformNode input
12//! - `TransformNodeAdapter`: Adapter to convert a TransformNode to a LifecycleNode
13//! - Helper functions for creating and converting transform nodes
14//!
15//! ## Migration from floxide-async
16//!
17//! This crate was previously named `floxide-async` and has been renamed to better
18//! reflect its purpose. If you were using `floxide-async`, update your imports
19//! from `floxide_async` to `floxide_transform`.
20
21use async_trait::async_trait;
22use floxide_core::{error::FloxideError, ActionType, LifecycleNode, NodeId};
23use futures::future::BoxFuture;
24use std::fmt::Debug;
25use std::marker::PhantomData;
26use uuid::Uuid;
27
28/// A simplified transform node trait for functional data transformations
29///
30/// The `TransformNode` trait provides a simplified interface for creating nodes
31/// that follow a functional transformation pattern with explicit input and output types.
32/// Unlike the more general `LifecycleNode`, which operates on a shared context,
33/// a `TransformNode` transforms data directly from input to output.
34///
35/// Each `TransformNode` implements a three-phase lifecycle:
36/// 1. `prep`: Validates and prepares the input data
37/// 2. `exec`: Performs the main transformation from input to output
38/// 3. `post`: Post-processes the output data
39///
40/// ## Benefits of TransformNode
41///
42/// - Simpler API focusing on data transformation
43/// - Direct error types specific to the node (vs. generic FloxideError)
44/// - Functional programming style with explicit input/output
45/// - Easier to compose and reason about
46///
47/// ## Example
48///
49/// ```rust
50/// use async_trait::async_trait;
51/// use floxide_transform::TransformNode;
52/// use std::error::Error;
53///
54/// // Custom error type
55/// #[derive(Debug)]
56/// struct MyError(String);
57/// impl std::fmt::Display for MyError {
58///     fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
59///         write!(f, "{}", self.0)
60///     }
61/// }
62/// impl Error for MyError {}
63///
64/// // A transform node that converts strings to uppercase
65/// struct UppercaseTransformer;
66///
67/// #[async_trait]
68/// impl TransformNode<String, String, MyError> for UppercaseTransformer {
69///     async fn prep(&self, input: String) -> Result<String, MyError> {
70///         if input.trim().is_empty() {
71///             return Err(MyError("Input cannot be empty".to_string()));
72///         }
73///         Ok(input)
74///     }
75///
76///     async fn exec(&self, input: String) -> Result<String, MyError> {
77///         Ok(input.to_uppercase())
78///     }
79///
80///     async fn post(&self, output: String) -> Result<String, MyError> {
81///         Ok(format!("Processed: {}", output))
82///     }
83/// }
84/// ```
85#[async_trait]
86pub trait TransformNode<Input, Output, Error>: Send + Sync
87where
88    Input: Send + 'static,
89    Output: Send + 'static,
90    Error: std::error::Error + Send + Sync + 'static,
91{
92    /// Preparation phase
93    async fn prep(&self, input: Input) -> Result<Input, Error>;
94
95    /// Execution phase
96    async fn exec(&self, input: Input) -> Result<Output, Error>;
97
98    /// Post-execution phase
99    async fn post(&self, output: Output) -> Result<Output, Error>;
100}
101
102/// Adapter to convert a TransformNode to a LifecycleNode
103pub struct TransformNodeAdapter<TN, Input, Output, Error, Action>
104where
105    TN: TransformNode<Input, Output, Error>,
106    Input: Clone + Send + Sync + 'static,
107    Output: Clone + Send + Sync + 'static,
108    Error: std::error::Error + Send + Sync + 'static,
109    Action: ActionType + Default + Send + Sync + 'static,
110{
111    node: TN,
112    id: NodeId,
113    _phantom: PhantomData<(Input, Output, Error, Action)>,
114}
115
116impl<TN, Input, Output, Error, Action> TransformNodeAdapter<TN, Input, Output, Error, Action>
117where
118    TN: TransformNode<Input, Output, Error>,
119    Input: Clone + Send + Sync + 'static,
120    Output: Clone + Send + Sync + 'static,
121    Error: std::error::Error + Send + Sync + 'static,
122    Action: ActionType + Default + Send + Sync + 'static,
123{
124    /// Create a new adapter for a TransformNode
125    pub fn new(node: TN) -> Self {
126        Self {
127            node,
128            id: Uuid::new_v4().to_string(),
129            _phantom: PhantomData,
130        }
131    }
132
133    /// Create a new adapter with a specific ID
134    pub fn with_id(node: TN, id: impl Into<String>) -> Self {
135        Self {
136            node,
137            id: id.into(),
138            _phantom: PhantomData,
139        }
140    }
141}
142
143impl<TN, Input, Output, Error, Action> Debug
144    for TransformNodeAdapter<TN, Input, Output, Error, Action>
145where
146    TN: TransformNode<Input, Output, Error> + Debug,
147    Input: Clone + Send + Sync + 'static,
148    Output: Clone + Send + Sync + 'static,
149    Error: std::error::Error + Send + Sync + 'static,
150    Action: ActionType + Default + Send + Sync + 'static,
151{
152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        f.debug_struct("TransformNodeAdapter")
154            .field("node", &self.node)
155            .field("id", &self.id)
156            .finish()
157    }
158}
159
160/// Context wrapper for TransformNode
161#[derive(Debug, Clone)]
162pub struct TransformContext<Input> {
163    pub input: Input,
164}
165
166impl<Input> TransformContext<Input> {
167    /// Create a new transform context
168    pub fn new(input: Input) -> Self {
169        Self { input }
170    }
171}
172
173#[async_trait]
174impl<TN, Input, Output, Error, Action> LifecycleNode<TransformContext<Input>, Action>
175    for TransformNodeAdapter<TN, Input, Output, Error, Action>
176where
177    TN: TransformNode<Input, Output, Error> + Send + Sync + 'static,
178    Input: Clone + Send + Sync + 'static,
179    Output: Clone + Send + Sync + 'static,
180    Error: std::error::Error + Send + Sync + 'static + Into<FloxideError>,
181    Action: ActionType + Default + Send + Sync + 'static,
182{
183    type PrepOutput = Input;
184    type ExecOutput = Output;
185
186    fn id(&self) -> NodeId {
187        self.id.clone()
188    }
189
190    async fn prep(
191        &self,
192        ctx: &mut TransformContext<Input>,
193    ) -> Result<Self::PrepOutput, FloxideError> {
194        self.node
195            .prep(ctx.input.clone())
196            .await
197            .map_err(|e| e.into())
198    }
199
200    async fn exec(&self, prep_result: Self::PrepOutput) -> Result<Self::ExecOutput, FloxideError> {
201        self.node.exec(prep_result).await.map_err(|e| e.into())
202    }
203
204    async fn post(
205        &self,
206        _prep_result: Self::PrepOutput,
207        exec_result: Self::ExecOutput,
208        _ctx: &mut TransformContext<Input>,
209    ) -> Result<Action, FloxideError> {
210        let _result = self.node.post(exec_result).await.map_err(|e| e.into())?;
211        Ok(Action::default())
212    }
213}
214
215/// Create a new transform node from closures
216pub fn transform_node<P, E, Po, I, O, Err>(
217    prep_fn: P,
218    exec_fn: E,
219    post_fn: Po,
220) -> impl TransformNode<I, O, Err>
221where
222    I: Clone + Send + Sync + 'static,
223    O: Clone + Send + Sync + 'static,
224    Err: std::error::Error + Send + Sync + 'static,
225    P: Fn(I) -> BoxFuture<'static, Result<I, Err>> + Send + Sync + 'static,
226    E: Fn(I) -> BoxFuture<'static, Result<O, Err>> + Send + Sync + 'static,
227    Po: Fn(O) -> BoxFuture<'static, Result<O, Err>> + Send + Sync + 'static,
228{
229    struct ClosureTransformNode<P, E, Po, I, O, Err> {
230        prep_fn: P,
231        exec_fn: E,
232        post_fn: Po,
233        _phantom: PhantomData<(I, O, Err)>,
234    }
235
236    impl<P, E, Po, I, O, Err> Debug for ClosureTransformNode<P, E, Po, I, O, Err> {
237        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238            f.debug_struct("ClosureTransformNode").finish()
239        }
240    }
241
242    #[async_trait]
243    impl<P, E, Po, I, O, Err> TransformNode<I, O, Err> for ClosureTransformNode<P, E, Po, I, O, Err>
244    where
245        I: Clone + Send + Sync + 'static,
246        O: Clone + Send + Sync + 'static,
247        Err: std::error::Error + Send + Sync + 'static,
248        P: Fn(I) -> BoxFuture<'static, Result<I, Err>> + Send + Sync + 'static,
249        E: Fn(I) -> BoxFuture<'static, Result<O, Err>> + Send + Sync + 'static,
250        Po: Fn(O) -> BoxFuture<'static, Result<O, Err>> + Send + Sync + 'static,
251    {
252        async fn prep(&self, input: I) -> Result<I, Err> {
253            (self.prep_fn)(input).await
254        }
255
256        async fn exec(&self, input: I) -> Result<O, Err> {
257            (self.exec_fn)(input).await
258        }
259
260        async fn post(&self, output: O) -> Result<O, Err> {
261            (self.post_fn)(output).await
262        }
263    }
264
265    ClosureTransformNode {
266        prep_fn,
267        exec_fn,
268        post_fn,
269        _phantom: PhantomData,
270    }
271}
272
273/// Convert a TransformNode to a LifecycleNode
274pub fn to_lifecycle_node<TN, I, O, Err, A>(
275    transform_node: TN,
276) -> impl LifecycleNode<TransformContext<I>, A, PrepOutput = I, ExecOutput = O>
277where
278    TN: TransformNode<I, O, Err> + Send + Sync + 'static,
279    I: Clone + Send + Sync + 'static,
280    O: Clone + Send + Sync + 'static,
281    Err: std::error::Error + Send + Sync + 'static + Into<FloxideError>,
282    A: ActionType + Default + Send + Sync + 'static,
283{
284    TransformNodeAdapter::<TN, I, O, Err, A>::new(transform_node)
285}
286
287/// Create a transform node from closures using the async syntax
288pub fn create_transform_node<I, O, Err>(
289    prep_fn: impl Fn(I) -> BoxFuture<'static, Result<I, Err>> + Send + Sync + 'static,
290    exec_fn: impl Fn(I) -> BoxFuture<'static, Result<O, Err>> + Send + Sync + 'static,
291    post_fn: impl Fn(O) -> BoxFuture<'static, Result<O, Err>> + Send + Sync + 'static,
292) -> impl TransformNode<I, O, Err>
293where
294    I: Clone + Send + Sync + 'static,
295    O: Clone + Send + Sync + 'static,
296    Err: std::error::Error + Send + Sync + 'static,
297{
298    transform_node(prep_fn, exec_fn, post_fn)
299}