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