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: 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 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}