1use serde::{Deserialize, Serialize};
7use std::fmt;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
15pub enum ClientErrorCode {
16 LicenseNotFound,
19 LicenseExpired,
21 LicenseRevoked,
23 LicenseSuspended,
25 LicenseBlacklisted,
27 LicenseInactive,
29
30 AlreadyBound,
33 NotBound,
35 HardwareMismatch,
37
38 FeatureNotIncluded,
41 QuotaExceeded,
43
44 GracePeriodExpired,
47
48 InternalError,
51
52 #[serde(other)]
55 Unknown,
56}
57
58impl ClientErrorCode {
59 pub fn default_message(&self) -> &'static str {
61 match self {
62 ClientErrorCode::LicenseNotFound => "License not found",
63 ClientErrorCode::LicenseExpired => "License has expired",
64 ClientErrorCode::LicenseRevoked => "License has been revoked",
65 ClientErrorCode::LicenseSuspended => "License is suspended",
66 ClientErrorCode::LicenseBlacklisted => "License has been blacklisted",
67 ClientErrorCode::LicenseInactive => "License is not active",
68 ClientErrorCode::AlreadyBound => "License is already bound to another device",
69 ClientErrorCode::NotBound => "License is not bound to any device",
70 ClientErrorCode::HardwareMismatch => "Hardware ID does not match",
71 ClientErrorCode::FeatureNotIncluded => "Feature not included in license",
72 ClientErrorCode::QuotaExceeded => "Usage quota exceeded",
73 ClientErrorCode::GracePeriodExpired => {
74 "Grace period expired - please connect to license server"
75 }
76 ClientErrorCode::InternalError => "Internal server error",
77 ClientErrorCode::Unknown => "Unknown error",
78 }
79 }
80
81 pub fn is_license_invalid(&self) -> bool {
83 matches!(
84 self,
85 ClientErrorCode::LicenseNotFound
86 | ClientErrorCode::LicenseExpired
87 | ClientErrorCode::LicenseRevoked
88 | ClientErrorCode::LicenseBlacklisted
89 | ClientErrorCode::GracePeriodExpired
90 )
91 }
92
93 pub fn requires_online(&self) -> bool {
95 matches!(
96 self,
97 ClientErrorCode::GracePeriodExpired | ClientErrorCode::LicenseSuspended
98 )
99 }
100}
101
102impl fmt::Display for ClientErrorCode {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 write!(f, "{}", self.default_message())
105 }
106}
107
108#[derive(Debug, Clone, Deserialize)]
110pub struct ServerErrorBody {
111 pub code: ClientErrorCode,
113 pub message: String,
115 #[serde(default)]
117 pub details: Option<serde_json::Value>,
118}
119
120#[derive(Debug, Clone, Deserialize)]
133pub struct ServerErrorResponse {
134 pub error: ServerErrorBody,
135}
136
137#[derive(Debug, Clone)]
142pub struct ClientApiError {
143 pub code: ClientErrorCode,
145 pub message: String,
147 pub details: Option<serde_json::Value>,
149}
150
151impl ClientApiError {
152 pub fn new(code: ClientErrorCode, message: impl Into<String>) -> Self {
154 Self {
155 code,
156 message: message.into(),
157 details: None,
158 }
159 }
160
161 pub fn with_details(
163 code: ClientErrorCode,
164 message: impl Into<String>,
165 details: serde_json::Value,
166 ) -> Self {
167 Self {
168 code,
169 message: message.into(),
170 details: Some(details),
171 }
172 }
173
174 pub fn grace_period_expired() -> Self {
176 Self::new(
177 ClientErrorCode::GracePeriodExpired,
178 "Offline grace period has expired. Please connect to the license server.",
179 )
180 }
181
182 pub fn is_license_invalid(&self) -> bool {
184 self.code.is_license_invalid()
185 }
186
187 pub fn requires_online(&self) -> bool {
189 self.code.requires_online()
190 }
191}
192
193impl fmt::Display for ClientApiError {
194 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195 write!(f, "{}: {}", self.code, self.message)
196 }
197}
198
199impl std::error::Error for ClientApiError {}
200
201impl From<ServerErrorResponse> for ClientApiError {
202 fn from(resp: ServerErrorResponse) -> Self {
203 Self {
204 code: resp.error.code,
205 message: resp.error.message,
206 details: resp.error.details,
207 }
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn parse_server_error_response() {
217 let json = r#"{
218 "error": {
219 "code": "LICENSE_NOT_FOUND",
220 "message": "The requested license does not exist",
221 "details": null
222 }
223 }"#;
224
225 let resp: ServerErrorResponse = serde_json::from_str(json).unwrap();
226 assert_eq!(resp.error.code, ClientErrorCode::LicenseNotFound);
227 assert_eq!(resp.error.message, "The requested license does not exist");
228 assert!(resp.error.details.is_none());
229 }
230
231 #[test]
232 fn parse_already_bound_error() {
233 let json = r#"{
234 "error": {
235 "code": "ALREADY_BOUND",
236 "message": "License is already bound to device 'Work Laptop'",
237 "details": {"device_name": "Work Laptop"}
238 }
239 }"#;
240
241 let resp: ServerErrorResponse = serde_json::from_str(json).unwrap();
242 let err: ClientApiError = resp.into();
243
244 assert_eq!(err.code, ClientErrorCode::AlreadyBound);
245 assert!(err.message.contains("Work Laptop"));
246 assert!(err.details.is_some());
247 }
248
249 #[test]
250 fn parse_unknown_error_code() {
251 let json = r#"{
252 "error": {
253 "code": "SOME_FUTURE_ERROR",
254 "message": "Some new error type",
255 "details": null
256 }
257 }"#;
258
259 let resp: ServerErrorResponse = serde_json::from_str(json).unwrap();
260 assert_eq!(resp.error.code, ClientErrorCode::Unknown);
261 }
262
263 #[test]
264 fn error_code_is_license_invalid() {
265 assert!(ClientErrorCode::LicenseNotFound.is_license_invalid());
266 assert!(ClientErrorCode::LicenseExpired.is_license_invalid());
267 assert!(ClientErrorCode::LicenseRevoked.is_license_invalid());
268 assert!(ClientErrorCode::GracePeriodExpired.is_license_invalid());
269
270 assert!(!ClientErrorCode::AlreadyBound.is_license_invalid());
271 assert!(!ClientErrorCode::NotBound.is_license_invalid());
272 assert!(!ClientErrorCode::LicenseSuspended.is_license_invalid());
273 }
274
275 #[test]
276 fn error_code_requires_online() {
277 assert!(ClientErrorCode::GracePeriodExpired.requires_online());
278 assert!(ClientErrorCode::LicenseSuspended.requires_online());
279
280 assert!(!ClientErrorCode::LicenseExpired.requires_online());
281 assert!(!ClientErrorCode::AlreadyBound.requires_online());
282 }
283
284 #[test]
285 fn client_api_error_display() {
286 let err = ClientApiError::new(
287 ClientErrorCode::LicenseExpired,
288 "Your license expired on 2024-01-01",
289 );
290 let display = format!("{}", err);
291 assert!(display.contains("expired"));
292 }
293}