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 batch_result: Option<crate::command_result::CardBatchResult>,
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub invalid_label_ids: Option<Vec<String>>,
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub file_saved: Option<bool>,
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub destination: Option<String>,
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub response_path: Option<String>,
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub response_error: Option<crate::client::ResponseDecodeKind>,
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub profile: Option<String>,
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub http_status: Option<u16>,
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub http_success: Option<bool>,
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub response_limit_bytes: Option<u64>,
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub retry_safe: Option<bool>,
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub mutation_attempted: Option<bool>,
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub mutation_confirmed: 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 outcome_unknown: 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    // Outer `None` omits this field for unrelated errors; inner `None` renders
133    // JSON null when a logout outcome is genuinely unknown.
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub remote_logout_completed: Option<Option<bool>>,
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub credential_stored: Option<bool>,
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub profile_created: Option<bool>,
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub profile_active: Option<bool>,
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub local_credential_removed: Option<bool>,
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub user_deleted: Option<bool>,
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub response: Option<RawApiResponseData>,
148}
149
150#[derive(Clone, Debug, Eq, PartialEq)]
151pub struct AppError {
152    code: ErrorCode,
153    message: String,
154    exit_code: StableExitCode,
155    details: Box<ErrorDetails>,
156}
157
158impl AppError {
159    pub fn new(code: ErrorCode, message: impl Into<String>, exit_code: StableExitCode) -> Self {
160        Self {
161            code,
162            message: message.into(),
163            exit_code,
164            details: Box::default(),
165        }
166    }
167
168    pub fn invalid_input(message: impl Into<String>) -> Self {
169        Self::new(
170            ErrorCode::InvalidInput,
171            message,
172            StableExitCode::InvalidInput,
173        )
174    }
175
176    pub fn configuration(message: impl Into<String>) -> Self {
177        Self::new(
178            ErrorCode::ConfigurationError,
179            message,
180            StableExitCode::Configuration,
181        )
182    }
183
184    pub fn with_details(mut self, details: ErrorDetails) -> Self {
185        self.details = Box::new(details);
186        self
187    }
188
189    pub fn with_profile_context(mut self, profile: String) -> Self {
190        self.details.profile = Some(profile);
191        self
192    }
193
194    pub fn with_session_created(mut self, created: bool) -> Self {
195        self.details.session_created = Some(created);
196        self
197    }
198
199    pub fn with_account_created(mut self, created: bool) -> Self {
200        self.details.account_created = Some(created);
201        self
202    }
203
204    pub fn with_profile_state(mut self, created: bool, active: bool) -> Self {
205        self.details.profile_created = Some(created);
206        self.details.profile_active = Some(active);
207        self
208    }
209
210    pub fn with_credential_stored(mut self, stored: bool) -> Self {
211        self.details.credential_stored = Some(stored);
212        self
213    }
214
215    pub fn with_user_deleted(mut self, deleted: bool) -> Self {
216        self.details.user_deleted = Some(deleted);
217        self
218    }
219
220    pub const fn code(&self) -> ErrorCode {
221        self.code
222    }
223
224    pub fn message(&self) -> &str {
225        &self.message
226    }
227
228    pub const fn exit_code(&self) -> StableExitCode {
229        self.exit_code
230    }
231
232    pub const fn details(&self) -> &ErrorDetails {
233        &self.details
234    }
235}
236
237impl From<ProfileStoreError> for AppError {
238    fn from(error: ProfileStoreError) -> Self {
239        Self::configuration(error.to_string())
240    }
241}
242
243impl std::fmt::Display for AppError {
244    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245        formatter.write_str(&self.message)
246    }
247}
248
249impl std::error::Error for AppError {}
250
251impl From<ServerUrlError> for AppError {
252    fn from(error: ServerUrlError) -> Self {
253        match error {
254            error @ ServerUrlError::InsecureHttp => Self::new(
255                ErrorCode::InsecureTransport,
256                error.to_string(),
257                StableExitCode::Configuration,
258            ),
259            error => Self::configuration(error.to_string()),
260        }
261    }
262}
263
264impl From<WekanClientFactoryError> for AppError {
265    fn from(error: WekanClientFactoryError) -> Self {
266        match error {
267            WekanClientFactoryError::ServerUrl(error) => Self::from(error),
268            WekanClientFactoryError::Build(_) => Self::new(
269                ErrorCode::InternalError,
270                "the HTTP client could not be initialized",
271                StableExitCode::Internal,
272            ),
273        }
274    }
275}