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