1use crate::prelude::*;
2use crate::op::Op;
6use crate::wrappers::logging::LoggingWrapper;
7use crate::{DryContext, OpError, WetContext};
8use std::panic::Location;
9
10pub 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 let trigger_name = get_caller_trigger_name();
22
23 let logged_op = LoggingWrapper::new(op, trigger_name);
25
26 logged_op.perform(dry, wet).await
28}
29
30#[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
47pub 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 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 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
104pub fn wrap_nested_exception(error: Box<dyn std::error::Error + Send + Sync>) -> OpError {
107 OpError::Other(error)
108}
109
110pub 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 #[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 #[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 #[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 #[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 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 #[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}