Skip to main content

ops_rs/
op.rs

1use crate::prelude::*;
2use crate::{DryContext, OpMetadata, WetContext};
3use async_trait::async_trait;
4
5#[async_trait]
6pub trait Op<T>: Send + Sync {
7    async fn perform(&self, dry: &mut DryContext, wet: &mut WetContext) -> OpResult<T>;
8
9    fn metadata(&self) -> OpMetadata;
10
11    /// Optional rollback method for ops that need custom cleanup logic.
12    /// Default implementation is a no-op for backward compatibility.
13    /// Called automatically by batch operations when a later op fails.
14    async fn rollback(&self, _dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<()> {
15        Ok(())
16    }
17}
18
19#[cfg(test)]
20mod tests {
21    use super::*;
22    use serde_json::json;
23
24    struct TestOp {
25        value: i32,
26    }
27
28    #[async_trait]
29    impl Op<i32> for TestOp {
30        async fn perform(&self, _dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<i32> {
31            Ok(self.value)
32        }
33
34        fn metadata(&self) -> OpMetadata {
35            OpMetadata::builder("TestOp")
36                .description("Simple test op")
37                .output_schema(json!({ "type": "integer" }))
38                .build()
39        }
40    }
41
42    // TEST0001: Run Op::perform and verify the returned value matches what the op was configured with
43    #[tokio::test]
44    async fn test0001_op_execution() {
45        let op = TestOp { value: 42 };
46        let mut dry = DryContext::new();
47        let mut wet = WetContext::new();
48
49        let result = op.perform(&mut dry, &mut wet).await;
50        assert_eq!(result.unwrap(), 42);
51    }
52
53    // TEST0002: Verify Op reads from DryContext and produces a formatted result using that data
54    #[tokio::test]
55    async fn test0002_op_with_contexts() {
56        struct ContextUsingOp;
57
58        #[async_trait]
59        impl Op<String> for ContextUsingOp {
60            async fn perform(
61                &self,
62                dry: &mut DryContext,
63                _wet: &mut WetContext,
64            ) -> OpResult<String> {
65                let name = dry.get_required::<String>("name")?;
66                Ok(format!("Hello, {}!", name))
67            }
68
69            fn metadata(&self) -> OpMetadata {
70                OpMetadata::builder("ContextUsingOp")
71                    .input_schema(json!({
72                        "type": "object",
73                        "properties": {
74                            "name": { "type": "string" }
75                        },
76                        "required": ["name"]
77                    }))
78                    .output_schema(json!({ "type": "string" }))
79                    .build()
80            }
81        }
82
83        let op = ContextUsingOp;
84        let mut dry = DryContext::new().with_value("name", "World");
85        let mut wet = WetContext::new();
86
87        let result = op.perform(&mut dry, &mut wet).await;
88        assert_eq!(result.unwrap(), "Hello, World!");
89    }
90
91    // TEST0003: Confirm that the default rollback implementation is a no-op that always succeeds
92    #[tokio::test]
93    async fn test0003_op_default_rollback() {
94        struct SimpleOp;
95
96        #[async_trait]
97        impl Op<()> for SimpleOp {
98            async fn perform(&self, _dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<()> {
99                Ok(())
100            }
101
102            fn metadata(&self) -> OpMetadata {
103                OpMetadata::builder("SimpleOp").build()
104            }
105        }
106
107        let op = SimpleOp;
108        let mut dry = DryContext::new();
109        let mut wet = WetContext::new();
110
111        // Default rollback should be a no-op and succeed
112        let result = op.rollback(&mut dry, &mut wet).await;
113        assert!(result.is_ok());
114    }
115
116    // TEST0004: Verify a custom rollback implementation is called and sets the rolled_back flag
117    #[tokio::test]
118    async fn test0004_op_custom_rollback() {
119        use std::sync::{Arc, Mutex};
120
121        struct RollbackTrackingOp {
122            performed: Arc<Mutex<bool>>,
123            rolled_back: Arc<Mutex<bool>>,
124        }
125
126        #[async_trait]
127        impl Op<()> for RollbackTrackingOp {
128            async fn perform(&self, _dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<()> {
129                *self.performed.lock().unwrap() = true;
130                Ok(())
131            }
132
133            async fn rollback(&self, _dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<()> {
134                *self.rolled_back.lock().unwrap() = true;
135                Ok(())
136            }
137
138            fn metadata(&self) -> OpMetadata {
139                OpMetadata::builder("RollbackTrackingOp").build()
140            }
141        }
142
143        let performed = Arc::new(Mutex::new(false));
144        let rolled_back = Arc::new(Mutex::new(false));
145
146        let op = RollbackTrackingOp {
147            performed: performed.clone(),
148            rolled_back: rolled_back.clone(),
149        };
150
151        let mut dry = DryContext::new();
152        let mut wet = WetContext::new();
153
154        // Perform the operation
155        op.perform(&mut dry, &mut wet).await.unwrap();
156        assert!(*performed.lock().unwrap());
157        assert!(!*rolled_back.lock().unwrap());
158
159        // Rollback the operation
160        op.rollback(&mut dry, &mut wet).await.unwrap();
161        assert!(*performed.lock().unwrap());
162        assert!(*rolled_back.lock().unwrap());
163    }
164}