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