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