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