1use std::time::SystemTimeError;
2
3pub type Result<T> = std::result::Result<T, Error>;
5
6#[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 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}