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