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