Skip to main content

ops_rs/
ops.rs

1use crate::prelude::*;
2// OPS utility module - Central execution and utility functions
3// Implements Java OPS class functionality with Rust enhancements
4
5use crate::op::Op;
6use crate::wrappers::logging::LoggingWrapper;
7use crate::{DryContext, OpError, WetContext};
8use std::panic::Location;
9
10/// Central execution function with automatic logging wrapper
11/// Equivalent to Java OPS.perform() method
12pub async fn perform<T>(
13    op: Box<dyn Op<T>>,
14    dry: &mut DryContext,
15    wet: &mut WetContext,
16) -> OpResult<T>
17where
18    T: Send + 'static,
19{
20    // Get caller information for dynamic op naming
21    let trigger_name = get_caller_trigger_name();
22
23    // Wrap op with logging (matches Java behavior)
24    let logged_op = LoggingWrapper::new(op, trigger_name);
25
26    // Execute with logging
27    logged_op.perform(dry, wet).await
28}
29
30/// Stack trace analysis to get caller class name
31/// Equivalent to Java getCallerCallerClassName()
32#[track_caller]
33pub fn get_caller_trigger_name() -> String {
34    let location = Location::caller();
35    format!(
36        "{}::{}",
37        location
38            .file()
39            .split('/')
40            .last()
41            .unwrap_or("unknown")
42            .replace(".rs", ""),
43        location.line()
44    )
45}
46
47/// Wrap nested op exception with context
48/// Equivalent to Java wrapNestedOpException(String, Exception)
49pub fn wrap_nested_op_exception(trigger_name: &str, error: OpError) -> OpError {
50    match error {
51        OpError::ExecutionFailed(msg) => {
52            OpError::ExecutionFailed(format!("Op '{}' failed: {}", trigger_name, msg))
53        }
54        OpError::Timeout { timeout_ms } => OpError::ExecutionFailed(format!(
55            "Op '{}' timed out after {}ms",
56            trigger_name, timeout_ms
57        )),
58        OpError::Context(msg) => {
59            OpError::Context(format!("Op '{}' context error: {}", trigger_name, msg))
60        }
61        OpError::BatchFailed(msg) => {
62            OpError::BatchFailed(format!("Batch op '{}' failed: {}", trigger_name, msg))
63        }
64        // Classified failures keep their identity through wrapping — the
65        // wrap adds human context to the CHAIN, never touches class/code.
66        OpError::WrappedClassified {
67            chain,
68            code,
69            class,
70            reason,
71            arg_urn,
72        } => OpError::WrappedClassified {
73            chain: format!("Batch op '{}' failed: {}", trigger_name, chain),
74            code,
75            class,
76            reason,
77            arg_urn,
78        },
79        OpError::Classified {
80            code,
81            class,
82            message,
83            arg_urn,
84        } => OpError::WrappedClassified {
85            chain: format!("Op '{}' failed: {}: {}", trigger_name, code, message),
86            code,
87            class,
88            reason: message,
89            arg_urn,
90        },
91        OpError::Aborted(reason) => {
92            // Aborted errors should preserve their nature and not be wrapped as execution failures
93            OpError::Aborted(format!("Op '{}' aborted: {}", trigger_name, reason))
94        }
95        OpError::Trigger(msg) => {
96            OpError::Trigger(format!("Op '{}' internal error: {}", trigger_name, msg))
97        }
98        OpError::Other(boxed_error) => {
99            OpError::ExecutionFailed(format!("Op '{}' failed: {}", trigger_name, boxed_error))
100        }
101    }
102}
103
104/// Wrap nested op exception without op name
105/// Equivalent to Java wrapNestedOpException(Exception)
106pub fn wrap_nested_exception(error: Box<dyn std::error::Error + Send + Sync>) -> OpError {
107    OpError::Other(error)
108}
109
110/// Convert any error to OpError with context
111/// Equivalent to Java wrapNestedRuntimeException(Exception)
112pub fn wrap_runtime_exception(error: Box<dyn std::error::Error + Send + Sync>) -> OpError {
113    OpError::ExecutionFailed(format!("Runtime error: {}", error))
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    struct TestOp;
121
122    #[async_trait]
123    impl Op<i32> for TestOp {
124        async fn perform(&self, _dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<i32> {
125            Ok(42)
126        }
127
128        fn metadata(&self) -> OpMetadata {
129            OpMetadata::builder("TestOp").build()
130        }
131    }
132
133    // TEST0005: Confirm the perform() utility wraps an op with automatic logging and returns its result
134    #[tokio::test]
135    async fn test0005_perform_with_auto_logging() {
136        let mut dry = DryContext::new();
137        let mut wet = WetContext::new();
138
139        let op = Box::new(TestOp);
140
141        let result = perform(op, &mut dry, &mut wet).await;
142        assert!(result.is_ok());
143        assert_eq!(result.unwrap(), 42);
144    }
145
146    // TEST0006: Verify get_caller_trigger_name() returns a string containing the module path with "::"
147    #[test]
148    fn test0006_caller_trigger_name() {
149        let name = get_caller_trigger_name();
150        assert!(name.contains("ops"));
151        assert!(name.contains("::"));
152    }
153
154    // TEST0007: Confirm wrap_nested_op_exception wraps an error with the op name in the message
155    #[test]
156    fn test0007_wrap_nested_op_exception() {
157        let original_error = OpError::ExecutionFailed("original error".to_string());
158        let wrapped = wrap_nested_op_exception("TestOp", original_error);
159
160        match wrapped {
161            OpError::ExecutionFailed(msg) => {
162                assert!(msg.contains("TestOp"));
163                assert!(msg.contains("original error"));
164            }
165            _ => panic!("Expected ExecutionFailed error"),
166        }
167    }
168
169    // TEST1903: wrapping preserves a classified failure's identity — the
170    // wrap enriches the human CHAIN only, never the class/code/reason
171    // (docs/failure-taxonomy.md).
172    #[test]
173    fn test1903_wrap_preserves_classification() {
174        use crate::failure::AttributionClass;
175
176        let classified = OpError::Classified {
177            code: "CONTEXT_OVERFLOW".to_string(),
178            class: AttributionClass::Input,
179            message: "prompt too large".to_string(),
180            arg_urn: Some("media:prompt;textable".to_string()),
181        };
182        let wrapped = wrap_nested_op_exception("GenerateOp", classified);
183        match wrapped {
184            OpError::WrappedClassified {
185                chain,
186                code,
187                class,
188                reason,
189                arg_urn,
190            } => {
191                assert!(chain.contains("GenerateOp"), "the wrap names the op");
192                assert!(chain.contains("prompt too large"));
193                assert_eq!(code, "CONTEXT_OVERFLOW");
194                assert_eq!(class, AttributionClass::Input);
195                assert_eq!(reason, "prompt too large");
196                assert_eq!(
197                    arg_urn.as_deref(),
198                    Some("media:prompt;textable"),
199                    "the origin's argument attribution survives wrapping verbatim"
200                );
201            }
202            other => panic!("expected WrappedClassified, got {:?}", other),
203        }
204
205        // Re-wrapping an already-wrapped classified failure only grows the
206        // chain — the identity fields are untouched.
207        let rewrapped = wrap_nested_op_exception(
208            "OuterBatch",
209            OpError::WrappedClassified {
210                chain: "Op 'GenerateOp' failed: CONTEXT_OVERFLOW: prompt too large".to_string(),
211                code: "CONTEXT_OVERFLOW".to_string(),
212                class: AttributionClass::Input,
213                reason: "prompt too large".to_string(),
214                arg_urn: Some("media:prompt;textable".to_string()),
215            },
216        );
217        match rewrapped {
218            OpError::WrappedClassified {
219                chain,
220                code,
221                class,
222                reason,
223                arg_urn,
224            } => {
225                assert!(chain.contains("OuterBatch"));
226                assert!(chain.contains("GenerateOp"));
227                assert_eq!(code, "CONTEXT_OVERFLOW");
228                assert_eq!(class, AttributionClass::Input);
229                assert_eq!(reason, "prompt too large");
230                assert_eq!(arg_urn.as_deref(), Some("media:prompt;textable"));
231            }
232            other => panic!("expected WrappedClassified, got {:?}", other),
233        }
234    }
235
236    // TEST0008: Verify wrap_runtime_exception converts a boxed std error into an OpError::ExecutionFailed
237    #[test]
238    fn test0008_wrap_runtime_exception() {
239        let error = Box::new(std::io::Error::new(std::io::ErrorKind::Other, "test error"));
240        let wrapped = wrap_runtime_exception(error);
241
242        match wrapped {
243            OpError::ExecutionFailed(msg) => {
244                assert!(msg.contains("Runtime error"));
245                assert!(msg.contains("test error"));
246            }
247            _ => panic!("Expected ExecutionFailed error"),
248        }
249    }
250}