use serde_json::json;
use crate::error::DurableError;
pub fn wrap_handler_result(
result: Result<serde_json::Value, DurableError>,
) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
match result {
Ok(value) => {
let result_str = serde_json::to_string(&value)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
Ok(json!({
"Status": "SUCCEEDED",
"Result": result_str,
}))
}
Err(ref err) if is_suspension(err) => {
Ok(json!({ "Status": "PENDING" }))
}
Err(err) => {
let error_type = err.code().to_string();
let error_message = err.to_string();
Ok(json!({
"Status": "FAILED",
"Error": {
"ErrorType": error_type,
"ErrorMessage": error_message,
},
}))
}
}
}
fn is_suspension(err: &DurableError) -> bool {
matches!(
err,
DurableError::StepRetryScheduled { .. }
| DurableError::WaitSuspended { .. }
| DurableError::CallbackSuspended { .. }
| DurableError::InvokeSuspended { .. }
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::DurableError;
#[test]
fn success_wraps_in_succeeded_envelope() {
let value = serde_json::json!({"order_id": "123", "status": "ok"});
let output = wrap_handler_result(Ok(value.clone())).unwrap();
assert_eq!(output["Status"], "SUCCEEDED");
let result_str = output["Result"]
.as_str()
.expect("Result should be a string");
let parsed: serde_json::Value = serde_json::from_str(result_str).unwrap();
assert_eq!(parsed["order_id"], "123");
}
#[test]
fn step_retry_scheduled_returns_pending() {
let err = DurableError::step_retry_scheduled("charge_payment");
let output = wrap_handler_result(Err(err)).unwrap();
assert_eq!(output["Status"], "PENDING");
assert!(output.get("Error").is_none());
assert!(output.get("Result").is_none());
}
#[test]
fn wait_suspended_returns_pending() {
let err = DurableError::wait_suspended("cooldown");
let output = wrap_handler_result(Err(err)).unwrap();
assert_eq!(output["Status"], "PENDING");
}
#[test]
fn callback_suspended_returns_pending() {
let err = DurableError::callback_suspended("approval", "cb-123");
let output = wrap_handler_result(Err(err)).unwrap();
assert_eq!(output["Status"], "PENDING");
}
#[test]
fn invoke_suspended_returns_pending() {
let err = DurableError::invoke_suspended("call_processor");
let output = wrap_handler_result(Err(err)).unwrap();
assert_eq!(output["Status"], "PENDING");
}
#[test]
fn step_timeout_returns_failed() {
let err = DurableError::step_timeout("slow_op");
let output = wrap_handler_result(Err(err)).unwrap();
assert_eq!(output["Status"], "FAILED");
assert!(output.get("Error").is_some());
let error_obj = &output["Error"];
assert_eq!(error_obj["ErrorType"], "STEP_TIMEOUT");
assert!(
error_obj["ErrorMessage"]
.as_str()
.unwrap()
.contains("timed out"),
"ErrorMessage should contain 'timed out'"
);
}
#[test]
fn checkpoint_failed_returns_failed() {
let err = DurableError::checkpoint_failed(
"op",
std::io::Error::new(std::io::ErrorKind::Other, "network error"),
);
let output = wrap_handler_result(Err(err)).unwrap();
assert_eq!(output["Status"], "FAILED");
assert_eq!(output["Error"]["ErrorType"], "CHECKPOINT_FAILED");
}
#[test]
fn replay_mismatch_returns_failed() {
let err = DurableError::replay_mismatch("Step", "Wait", 3);
let output = wrap_handler_result(Err(err)).unwrap();
assert_eq!(output["Status"], "FAILED");
assert_eq!(output["Error"]["ErrorType"], "REPLAY_MISMATCH");
}
#[test]
fn failed_status_has_no_result_field() {
let err = DurableError::step_timeout("op");
let output = wrap_handler_result(Err(err)).unwrap();
assert!(output.get("Result").is_none());
}
#[test]
fn succeeded_status_result_is_json_string() {
let output = wrap_handler_result(Ok(serde_json::json!({"key": "value"}))).unwrap();
assert_eq!(output["Status"], "SUCCEEDED");
assert!(
output["Result"].is_string(),
"Result must be a JSON string, not an object"
);
}
#[test]
fn null_value_wraps_correctly() {
let output = wrap_handler_result(Ok(serde_json::Value::Null)).unwrap();
assert_eq!(output["Status"], "SUCCEEDED");
assert_eq!(output["Result"].as_str().unwrap(), "null");
}
}