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