Skip to main content

ascend_tools/
error.rs

1use std::time::SystemTimeError;
2
3/// Result type for the Ascend SDK.
4pub type Result<T> = std::result::Result<T, Error>;
5
6/// Public error type for the Ascend SDK.
7#[derive(Debug, thiserror::Error)]
8#[non_exhaustive]
9pub enum Error {
10    #[error(
11        "{field} is required, set {env_var} or pass --{flag}\n\n  hint: if you don't have an Ascend Instance yet, run `ascend-tools signup`\n        then create a service account in Settings and set the env vars"
12    )]
13    MissingConfig {
14        field: String,
15        env_var: String,
16        flag: String,
17    },
18
19    #[error("failed to decode service account key from base64")]
20    InvalidServiceAccountKeyEncoding,
21
22    #[error("service account key must be 32 bytes (Ed25519 seed), got {got}")]
23    InvalidServiceAccountKeyLength { got: usize },
24
25    #[error("expected 32-byte Ed25519 seed, got {got} bytes")]
26    InvalidEd25519SeedLength { got: usize },
27
28    #[error("failed to sign JWT")]
29    JwtSignFailed {
30        #[source]
31        source: jsonwebtoken::errors::Error,
32    },
33
34    #[error(
35        "internal synchronization error: {name} mutex poisoned, client state may be inconsistent — recreate client"
36    )]
37    MutexPoisoned { name: &'static str },
38
39    #[error("system clock before Unix epoch")]
40    SystemClockBeforeUnixEpoch {
41        #[source]
42        source: SystemTimeError,
43    },
44
45    #[error("{context}: {source}")]
46    RequestFailed {
47        context: String,
48        #[source]
49        source: ureq::Error,
50    },
51
52    #[error("failed to read response body for {context}: {source}")]
53    ResponseReadFailed {
54        context: String,
55        #[source]
56        source: ureq::Error,
57    },
58
59    #[error("failed to parse JSON for {context}: {source}")]
60    JsonParseFailed {
61        context: String,
62        #[source]
63        source: serde_json::Error,
64    },
65
66    #[error("failed to serialize JSON for {context}: {source}")]
67    JsonSerializeFailed {
68        context: String,
69        #[source]
70        source: serde_json::Error,
71    },
72
73    #[error("missing `{field}` in {context}")]
74    MissingField {
75        context: &'static str,
76        field: &'static str,
77    },
78
79    #[error("API error (HTTP {status}): {message}")]
80    ApiError { status: u16, message: String },
81
82    #[error(
83        "workspace/deployment is paused, use --resume (CLI) or resume=True (SDK) to resume before running"
84    )]
85    RuntimePaused,
86
87    #[error("workspace/deployment is starting, not yet ready to accept flow runs")]
88    RuntimeStarting,
89
90    #[error("workspace/deployment is in error state and cannot run flows")]
91    RuntimeInErrorState,
92
93    #[error("workspace/deployment health is '{health}', expected 'running'")]
94    RuntimeUnexpectedHealth { health: String },
95
96    #[error("workspace/deployment has no health status, it may be initializing")]
97    RuntimeHealthMissing,
98
99    #[error("no {kind} found with title '{title}'")]
100    NotFound { kind: String, title: String },
101
102    #[error("no {kind} found matching '{title}', available: {}", .available.join(", "))]
103    NotFoundWithOptions {
104        kind: String,
105        title: String,
106        available: Vec<String>,
107    },
108
109    #[error("multiple {kind}s found with title '{title}', use --uuid to specify one: {}", .matches.iter().map(|(uuid, title)| format!("{uuid} ({title})")).collect::<Vec<_>>().join(", "))]
110    AmbiguousTitle {
111        kind: String,
112        title: String,
113        matches: Vec<(String, String)>,
114    },
115
116    #[error("SSE stream error: {context}")]
117    SseParseError { context: String },
118
119    #[error("Otto stream ended unexpectedly: {context}")]
120    OttoStreamEndedUnexpectedly { context: String },
121}
122
123impl Error {
124    /// Returns the HTTP status code if this is an API error, or `None` otherwise.
125    pub fn http_status(&self) -> Option<u16> {
126        match self {
127            Self::ApiError { status, .. } => Some(*status),
128            _ => None,
129        }
130    }
131}
132
133pub(crate) trait UreqResultExt<T> {
134    fn with_request_context(self, context: impl Into<String>) -> Result<T>;
135    fn with_response_read_context(self, context: impl Into<String>) -> Result<T>;
136}
137
138impl<T> UreqResultExt<T> for std::result::Result<T, ureq::Error> {
139    fn with_request_context(self, context: impl Into<String>) -> Result<T> {
140        self.map_err(|source| Error::RequestFailed {
141            context: context.into(),
142            source,
143        })
144    }
145
146    fn with_response_read_context(self, context: impl Into<String>) -> Result<T> {
147        self.map_err(|source| Error::ResponseReadFailed {
148            context: context.into(),
149            source,
150        })
151    }
152}
153
154pub(crate) trait JsonResultExt<T> {
155    fn with_json_parse_context(self, context: impl Into<String>) -> Result<T>;
156    fn with_json_serialize_context(self, context: impl Into<String>) -> Result<T>;
157}
158
159impl<T> JsonResultExt<T> for std::result::Result<T, serde_json::Error> {
160    fn with_json_parse_context(self, context: impl Into<String>) -> Result<T> {
161        self.map_err(|source| Error::JsonParseFailed {
162            context: context.into(),
163            source,
164        })
165    }
166
167    fn with_json_serialize_context(self, context: impl Into<String>) -> Result<T> {
168        self.map_err(|source| Error::JsonSerializeFailed {
169            context: context.into(),
170            source,
171        })
172    }
173}