1use serde::Serialize;
2
3use crate::{
4 client::{ServerUrlError, WekanClientFactoryError},
5 command_result::{LogoutScope, RawApiResponseData},
6 config::profiles::ProfileStoreError,
7 exit_code::StableExitCode,
8};
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
11#[serde(rename_all = "snake_case")]
12pub enum ErrorCode {
13 BatchIncomplete,
14 InvalidInput,
15 ConfigurationError,
16 ProfileNotFound,
17 ProfileAlreadyExists,
18 ProfileServerMismatch,
19 ProfileInUse,
20 ProfileHasCredential,
21 InsecureTransport,
22 TransportError,
23 UnexpectedRedirect,
24 ProtocolError,
25 ApiResponseTooLarge,
26 LoginRejected,
27 LoginRateLimited,
28 CredentialNotFound,
29 CredentialExpired,
30 CredentialAlreadyExists,
31 AuthenticationRejected,
32 PermissionDenied,
33 NotFound,
34 RegistrationRejected,
35 RegistrationDisabled,
36 ServerError,
37 CredentialStoreUnavailable,
38 CredentialStoreFailed,
39 InternalError,
40 FileIoError,
41 UpdateFailed,
42}
43
44impl ErrorCode {
45 pub const fn as_str(self) -> &'static str {
46 match self {
47 Self::BatchIncomplete => "batch_incomplete",
48 Self::InvalidInput => "invalid_input",
49 Self::ConfigurationError => "configuration_error",
50 Self::ProfileNotFound => "profile_not_found",
51 Self::ProfileAlreadyExists => "profile_already_exists",
52 Self::ProfileServerMismatch => "profile_server_mismatch",
53 Self::ProfileInUse => "profile_in_use",
54 Self::ProfileHasCredential => "profile_has_credential",
55 Self::InsecureTransport => "insecure_transport",
56 Self::TransportError => "transport_error",
57 Self::UnexpectedRedirect => "unexpected_redirect",
58 Self::ProtocolError => "protocol_error",
59 Self::ApiResponseTooLarge => "api_response_too_large",
60 Self::LoginRejected => "login_rejected",
61 Self::LoginRateLimited => "login_rate_limited",
62 Self::CredentialNotFound => "credential_not_found",
63 Self::CredentialExpired => "credential_expired",
64 Self::CredentialAlreadyExists => "credential_already_exists",
65 Self::AuthenticationRejected => "authentication_rejected",
66 Self::PermissionDenied => "permission_denied",
67 Self::NotFound => "not_found",
68 Self::RegistrationRejected => "registration_rejected",
69 Self::RegistrationDisabled => "registration_disabled",
70 Self::ServerError => "server_error",
71 Self::CredentialStoreUnavailable => "credential_store_unavailable",
72 Self::CredentialStoreFailed => "credential_store_failed",
73 Self::InternalError => "internal_error",
74 Self::FileIoError => "file_io_error",
75 Self::UpdateFailed => "update_failed",
76 }
77 }
78}
79
80#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
81pub struct ErrorDetails {
82 #[serde(skip_serializing_if = "Option::is_none")]
83 pub trello_import_result: Option<crate::command_result::TrelloImportResult>,
84 #[serde(skip_serializing_if = "Option::is_none")]
85 pub batch_result: Option<crate::command_result::CardBatchResult>,
86 #[serde(skip_serializing_if = "Option::is_none")]
87 pub invalid_label_ids: Option<Vec<String>>,
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub file_saved: Option<bool>,
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub destination: Option<String>,
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub response_path: Option<String>,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 pub response_error: Option<crate::client::ResponseDecodeKind>,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub profile: Option<String>,
98 #[serde(skip_serializing_if = "Option::is_none")]
99 pub http_status: Option<u16>,
100 #[serde(skip_serializing_if = "Option::is_none")]
101 pub http_success: Option<bool>,
102 #[serde(skip_serializing_if = "Option::is_none")]
103 pub response_limit_bytes: Option<u64>,
104 #[serde(skip_serializing_if = "Option::is_none")]
105 pub retry_safe: Option<bool>,
106 #[serde(skip_serializing_if = "Option::is_none")]
107 pub mutation_attempted: Option<bool>,
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub wekan_status_code: Option<u16>,
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub server_error: Option<String>,
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub server_reason: Option<String>,
114 #[serde(skip_serializing_if = "Option::is_none")]
115 pub server_message: Option<String>,
116 #[serde(skip_serializing_if = "Option::is_none")]
117 pub server_error_type: Option<String>,
118 #[serde(skip_serializing_if = "Option::is_none")]
119 pub server_is_client_safe: Option<bool>,
120 #[serde(skip_serializing_if = "Option::is_none")]
121 pub account_created: Option<bool>,
122 #[serde(skip_serializing_if = "Option::is_none")]
123 pub session_created: Option<bool>,
124 #[serde(skip_serializing_if = "Option::is_none")]
125 pub two_factor_required: Option<bool>,
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub retry_after_seconds: Option<u64>,
128 #[serde(skip_serializing_if = "Option::is_none")]
129 pub token_expires: Option<String>,
130 #[serde(skip_serializing_if = "Option::is_none")]
131 pub logout_scope: Option<LogoutScope>,
132 #[serde(skip_serializing_if = "Option::is_none")]
133 pub remote_logout_completed: Option<bool>,
134 #[serde(skip_serializing_if = "Option::is_none")]
135 pub credential_stored: Option<bool>,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub profile_created: Option<bool>,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub profile_active: Option<bool>,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub local_credential_removed: Option<bool>,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub user_deleted: Option<bool>,
144 #[serde(skip_serializing_if = "Option::is_none")]
145 pub response: Option<RawApiResponseData>,
146}
147
148#[derive(Clone, Debug, Eq, PartialEq)]
149pub struct AppError {
150 code: ErrorCode,
151 message: String,
152 exit_code: StableExitCode,
153 details: Box<ErrorDetails>,
154}
155
156impl AppError {
157 pub fn new(code: ErrorCode, message: impl Into<String>, exit_code: StableExitCode) -> Self {
158 Self {
159 code,
160 message: message.into(),
161 exit_code,
162 details: Box::default(),
163 }
164 }
165
166 pub fn invalid_input(message: impl Into<String>) -> Self {
167 Self::new(
168 ErrorCode::InvalidInput,
169 message,
170 StableExitCode::InvalidInput,
171 )
172 }
173
174 pub fn configuration(message: impl Into<String>) -> Self {
175 Self::new(
176 ErrorCode::ConfigurationError,
177 message,
178 StableExitCode::Configuration,
179 )
180 }
181
182 pub fn with_details(mut self, details: ErrorDetails) -> Self {
183 self.details = Box::new(details);
184 self
185 }
186
187 pub fn with_profile_context(mut self, profile: String) -> Self {
188 self.details.profile = Some(profile);
189 self
190 }
191
192 pub fn with_session_created(mut self, created: bool) -> Self {
193 self.details.session_created = Some(created);
194 self
195 }
196
197 pub fn with_account_created(mut self, created: bool) -> Self {
198 self.details.account_created = Some(created);
199 self
200 }
201
202 pub fn with_profile_state(mut self, created: bool, active: bool) -> Self {
203 self.details.profile_created = Some(created);
204 self.details.profile_active = Some(active);
205 self
206 }
207
208 pub fn with_credential_stored(mut self, stored: bool) -> Self {
209 self.details.credential_stored = Some(stored);
210 self
211 }
212
213 pub fn with_user_deleted(mut self, deleted: bool) -> Self {
214 self.details.user_deleted = Some(deleted);
215 self
216 }
217
218 pub const fn code(&self) -> ErrorCode {
219 self.code
220 }
221
222 pub fn message(&self) -> &str {
223 &self.message
224 }
225
226 pub const fn exit_code(&self) -> StableExitCode {
227 self.exit_code
228 }
229
230 pub const fn details(&self) -> &ErrorDetails {
231 &self.details
232 }
233}
234
235impl From<ProfileStoreError> for AppError {
236 fn from(error: ProfileStoreError) -> Self {
237 Self::configuration(error.to_string())
238 }
239}
240
241impl std::fmt::Display for AppError {
242 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243 formatter.write_str(&self.message)
244 }
245}
246
247impl std::error::Error for AppError {}
248
249impl From<ServerUrlError> for AppError {
250 fn from(error: ServerUrlError) -> Self {
251 match error {
252 error @ ServerUrlError::InsecureHttp => Self::new(
253 ErrorCode::InsecureTransport,
254 error.to_string(),
255 StableExitCode::Configuration,
256 ),
257 error => Self::configuration(error.to_string()),
258 }
259 }
260}
261
262impl From<WekanClientFactoryError> for AppError {
263 fn from(error: WekanClientFactoryError) -> Self {
264 match error {
265 WekanClientFactoryError::ServerUrl(error) => Self::from(error),
266 WekanClientFactoryError::Build(_) => Self::new(
267 ErrorCode::InternalError,
268 "the HTTP client could not be initialized",
269 StableExitCode::Internal,
270 ),
271 }
272 }
273}