Skip to main content

wekan_cli/
error.rs

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}