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