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