Skip to main content

redevplugin_worker_sdk/
error.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::fmt;
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
7pub enum ErrorCode {
8    InvalidArgument,
9    PermissionDenied,
10    NotFound,
11    NotEmpty,
12    CrossDevice,
13    AlreadyExists,
14    ResourceClosed,
15    Canceled,
16    Timeout,
17    WouldBlock,
18    IoError,
19    MountUnavailable,
20    NetworkError,
21    ResourceLimit,
22    Internal,
23    RuntimeUnavailable,
24    RedirectRequiresReplay,
25    #[serde(other)]
26    Unknown,
27}
28
29impl ErrorCode {
30    pub fn as_str(&self) -> &'static str {
31        match self {
32            Self::InvalidArgument => "INVALID_ARGUMENT",
33            Self::PermissionDenied => "PERMISSION_DENIED",
34            Self::NotFound => "NOT_FOUND",
35            Self::NotEmpty => "NOT_EMPTY",
36            Self::CrossDevice => "CROSS_DEVICE",
37            Self::AlreadyExists => "ALREADY_EXISTS",
38            Self::ResourceClosed => "RESOURCE_CLOSED",
39            Self::Canceled => "CANCELED",
40            Self::Timeout => "TIMEOUT",
41            Self::WouldBlock => "WOULD_BLOCK",
42            Self::IoError => "IO_ERROR",
43            Self::MountUnavailable => "MOUNT_UNAVAILABLE",
44            Self::NetworkError => "NETWORK_ERROR",
45            Self::ResourceLimit => "RESOURCE_LIMIT",
46            Self::Internal => "INTERNAL",
47            Self::RuntimeUnavailable => "RUNTIME_UNAVAILABLE",
48            Self::RedirectRequiresReplay => "REDIRECT_REQUIRES_REPLAY",
49            Self::Unknown => "UNKNOWN",
50        }
51    }
52}
53
54#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
55pub struct Error {
56    pub code: ErrorCode,
57    pub message: String,
58    #[serde(default)]
59    pub retryable: bool,
60    #[serde(default)]
61    pub details: Value,
62}
63
64impl Error {
65    pub(crate) fn internal(message: impl Into<String>) -> Self {
66        Self {
67            code: ErrorCode::Internal,
68            message: message.into(),
69            retryable: false,
70            details: Value::Null,
71        }
72    }
73
74    pub(crate) fn from_abi_status(status: i32) -> Self {
75        let code = match status {
76            -1 => ErrorCode::InvalidArgument,
77            -2 => ErrorCode::PermissionDenied,
78            -3 => ErrorCode::NotFound,
79            -4 => ErrorCode::AlreadyExists,
80            -5 => ErrorCode::ResourceClosed,
81            -6 => ErrorCode::Canceled,
82            -7 => ErrorCode::Timeout,
83            -8 => ErrorCode::WouldBlock,
84            -9 => ErrorCode::IoError,
85            -10 => ErrorCode::NetworkError,
86            -11 => ErrorCode::ResourceLimit,
87            -12 => ErrorCode::Internal,
88            -13 => ErrorCode::RuntimeUnavailable,
89            -14 => ErrorCode::RedirectRequiresReplay,
90            -15 => ErrorCode::MountUnavailable,
91            _ => ErrorCode::Unknown,
92        };
93        Self {
94            code,
95            message: format!("Worker API hostcall failed with ABI status {status}"),
96            retryable: false,
97            details: Value::Null,
98        }
99    }
100}
101
102impl fmt::Display for Error {
103    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
104        write!(formatter, "{:?}: {}", self.code, self.message)
105    }
106}
107
108impl std::error::Error for Error {}
109
110pub type Result<T> = std::result::Result<T, Error>;
111
112#[cfg(test)]
113mod tests {
114    use super::{Error, ErrorCode};
115
116    #[test]
117    fn preserves_selected_file_codes_and_unknown_mutation() {
118        for (code, expected) in [
119            ("NOT_EMPTY", ErrorCode::NotEmpty),
120            ("CROSS_DEVICE", ErrorCode::CrossDevice),
121        ] {
122            let raw = serde_json::json!({"code":code,"message":"file operation","retryable":false,"details":{"mutation_outcome":"unknown"}});
123            let decoded: Error = serde_json::from_value(raw).unwrap();
124            assert_eq!(decoded.code, expected);
125            assert_eq!(decoded.details["mutation_outcome"], "unknown");
126            assert_eq!(decoded.code.as_str(), code);
127        }
128    }
129
130    #[test]
131    fn preserves_mount_unavailable_abi_status() {
132        assert_eq!(
133            Error::from_abi_status(-15).code,
134            ErrorCode::MountUnavailable
135        );
136    }
137}