1use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use super::registry::format_payload_with_kind as format_payload;
7use crate::http::client::HttpError;
8
9#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
13#[serde(rename_all = "snake_case")]
14pub enum PayloadKind {
15 PlayerStatus,
17 Queue,
18 Devices,
19 PlayHistory,
20
21 SearchResults,
23 CombinedSearch,
24 Pins,
25
26 Track,
28 Album,
29 Artist,
30 Playlist,
31 Show,
32 Episode,
33 Audiobook,
34 Chapter,
35 Category,
36 User,
37
38 TrackList,
40 AlbumList,
41 ArtistList,
42 PlaylistList,
43 ShowList,
44 EpisodeList,
45 AudiobookList,
46 ChapterList,
47 CategoryList,
48 TopTracks,
49 TopArtists,
50 ArtistTopTracks,
51 RelatedArtists,
52 NewReleases,
53 FollowedArtists,
54
55 SavedTracks,
57 SavedAlbums,
58 SavedShows,
59 SavedEpisodes,
60 SavedAudiobooks,
61 LibraryCheck,
62
63 Markets,
65 Generic,
66}
67
68#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
70#[serde(rename_all = "snake_case")]
71pub enum ErrorKind {
72 Network,
74 Api,
76 Auth,
78 NotFound,
80 Forbidden,
82 RateLimited,
84 Validation,
86 Storage,
88 Config,
90 Player,
92}
93
94impl ErrorKind {
95 pub fn as_str(&self) -> &'static str {
96 match self {
97 ErrorKind::Network => "network_error",
98 ErrorKind::Api => "api_error",
99 ErrorKind::Auth => "auth_error",
100 ErrorKind::NotFound => "not_found",
101 ErrorKind::Forbidden => "forbidden",
102 ErrorKind::RateLimited => "rate_limited",
103 ErrorKind::Validation => "validation_error",
104 ErrorKind::Storage => "storage_error",
105 ErrorKind::Config => "config_error",
106 ErrorKind::Player => "player_error",
107 }
108 }
109}
110
111impl std::fmt::Display for ErrorKind {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 write!(f, "{}", self.as_str())
114 }
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
118#[serde(rename_all = "snake_case")]
119pub enum Status {
120 Success,
121 Error,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
125#[must_use = "Response should be returned or printed, not ignored"]
126pub struct Response {
127 pub status: Status,
128 pub code: u16,
129 pub message: String,
130 #[serde(skip_serializing_if = "Option::is_none")]
131 pub payload: Option<Value>,
132 #[serde(skip_serializing_if = "Option::is_none")]
133 pub payload_kind: Option<PayloadKind>,
134 #[serde(skip_serializing_if = "Option::is_none")]
135 pub error: Option<ErrorDetail>,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct ErrorDetail {
140 pub kind: String,
141 #[serde(skip_serializing_if = "Option::is_none")]
142 pub details: Option<String>,
143}
144
145impl Response {
146 pub fn success(code: u16, message: impl Into<String>) -> Self {
147 Self {
148 status: Status::Success,
149 code,
150 message: message.into(),
151 payload: None,
152 payload_kind: None,
153 error: None,
154 }
155 }
156
157 pub fn success_with_payload(code: u16, message: impl Into<String>, payload: Value) -> Self {
158 Self {
159 status: Status::Success,
160 code,
161 message: message.into(),
162 payload: Some(payload),
163 payload_kind: None,
164 error: None,
165 }
166 }
167
168 pub fn success_typed(
172 code: u16,
173 message: impl Into<String>,
174 kind: PayloadKind,
175 payload: Value,
176 ) -> Self {
177 Self {
178 status: Status::Success,
179 code,
180 message: message.into(),
181 payload: Some(payload),
182 payload_kind: Some(kind),
183 error: None,
184 }
185 }
186
187 pub fn err(code: u16, message: impl Into<String>, kind: ErrorKind) -> Self {
189 Self {
190 status: Status::Error,
191 code,
192 message: message.into(),
193 payload: None,
194 payload_kind: None,
195 error: Some(ErrorDetail {
196 kind: kind.to_string(),
197 details: None,
198 }),
199 }
200 }
201
202 pub fn err_with_details(
204 code: u16,
205 message: impl Into<String>,
206 kind: ErrorKind,
207 details: impl Into<String>,
208 ) -> Self {
209 Self {
210 status: Status::Error,
211 code,
212 message: message.into(),
213 payload: None,
214 payload_kind: None,
215 error: Some(ErrorDetail {
216 kind: kind.to_string(),
217 details: Some(details.into()),
218 }),
219 }
220 }
221
222 pub fn from_http_error(err: &HttpError, context: &str) -> Self {
224 let kind = match err {
225 HttpError::Network(_) => ErrorKind::Network,
226 HttpError::Unauthorized => ErrorKind::Auth,
227 HttpError::Forbidden => ErrorKind::Forbidden,
228 HttpError::NotFound => ErrorKind::NotFound,
229 HttpError::RateLimited { .. } => ErrorKind::RateLimited,
230 HttpError::Api { .. } => ErrorKind::Api,
231 };
232
233 let status_text = match err.status_code() {
234 400 => "Bad Request",
235 401 => "Unauthorized",
236 403 => "Forbidden",
237 404 => "Not Found",
238 429 => "Rate Limited",
239 500 => "Internal Server Error",
240 502 => "Bad Gateway",
241 503 => "Service Unavailable",
242 _ => "",
243 };
244
245 let message = if status_text.is_empty() {
246 format!("{} ({})", context, err.status_code())
247 } else {
248 format!("{}: {} {}", context, err.status_code(), status_text)
249 };
250
251 Self {
252 status: Status::Error,
253 code: err.status_code(),
254 message,
255 payload: None,
256 payload_kind: None,
257 error: Some(ErrorDetail {
258 kind: kind.to_string(),
259 details: Some(err.user_message().to_string()),
260 }),
261 }
262 }
263
264 pub fn to_json(&self) -> String {
265 serde_json::to_string(self).unwrap_or_else(|_| {
266 r#"{"status":"error","code":500,"message":"Failed to serialize response"}"#.to_string()
267 })
268 }
269}
270
271#[macro_export]
273macro_rules! api_error {
274 ($ctx:expr, $err:expr) => {
275 $crate::io::output::Response::from_http_error(&$err, $ctx)
276 };
277}
278
279#[macro_export]
281macro_rules! storage_error {
282 ($msg:expr, $err:expr) => {
283 $crate::io::output::Response::err_with_details(
284 500,
285 $msg,
286 $crate::io::output::ErrorKind::Storage,
287 $err.to_string(),
288 )
289 };
290}
291
292#[macro_export]
294macro_rules! auth_error {
295 ($msg:expr, $err:expr) => {
296 $crate::io::output::Response::err_with_details(
297 401,
298 $msg,
299 $crate::io::output::ErrorKind::Auth,
300 $err.to_string(),
301 )
302 };
303}
304
305pub fn print_json(response: &Response) {
306 println!("{}", response.to_json());
307}
308
309pub fn print_human(response: &Response) {
310 match &response.status {
311 Status::Error => {
312 eprintln!("Error: {}", response.message);
313 if let Some(err) = &response.error
314 && let Some(details) = &err.details
315 {
316 eprintln!(" {}", details);
317 }
318 }
319 Status::Success => {
320 if let Some(payload) = &response.payload {
321 format_payload(payload, &response.message, response.payload_kind);
322 } else {
323 println!("{}", response.message);
324 }
325 }
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn success_response_serializes() {
335 let resp = Response::success(200, "OK");
336 let json = resp.to_json();
337 assert!(json.contains(r#""status":"success""#));
338 assert!(json.contains(r#""code":200"#));
339 }
340
341 #[test]
342 fn error_response_includes_error_detail() {
343 let resp = Response::err(401, "Unauthorized", ErrorKind::Auth);
344 let json = resp.to_json();
345 assert!(json.contains(r#""status":"error""#));
346 assert!(json.contains(r#""kind":"auth_error""#));
347 }
348
349 #[test]
350 fn payload_skipped_when_none() {
351 let resp = Response::success(200, "OK");
352 let json = resp.to_json();
353 assert!(!json.contains("payload"));
354 }
355
356 #[test]
357 fn payload_included_when_present() {
358 let payload = serde_json::json!({"track": "test"});
359 let resp = Response::success_with_payload(200, "OK", payload);
360 let json = resp.to_json();
361 assert!(json.contains("payload"));
362 assert!(json.contains("track"));
363 }
364
365 #[test]
366 fn error_kind_serializes_to_snake_case() {
367 assert_eq!(ErrorKind::NotFound.as_str(), "not_found");
368 assert_eq!(ErrorKind::RateLimited.as_str(), "rate_limited");
369 assert_eq!(ErrorKind::Api.as_str(), "api_error");
370 }
371
372 #[test]
373 fn success_typed_includes_payload_kind() {
374 let payload = serde_json::json!({"name": "Test Track"});
375 let resp = Response::success_typed(200, "Track info", PayloadKind::Track, payload);
376 assert!(resp.payload_kind.is_some());
377 assert_eq!(resp.payload_kind.unwrap(), PayloadKind::Track);
378 }
379
380 #[test]
381 fn success_typed_serializes_payload_kind() {
382 let payload = serde_json::json!({"name": "Test"});
383 let resp = Response::success_typed(200, "OK", PayloadKind::Playlist, payload);
384 let json = resp.to_json();
385 assert!(json.contains("payload_kind"));
386 assert!(json.contains("playlist"));
387 }
388
389 #[test]
390 fn success_without_typed_has_no_payload_kind() {
391 let payload = serde_json::json!({"name": "Test"});
392 let resp = Response::success_with_payload(200, "OK", payload);
393 assert!(resp.payload_kind.is_none());
394 let json = resp.to_json();
395 assert!(!json.contains("payload_kind"));
396 }
397
398 #[test]
399 fn payload_kind_variants() {
400 let kinds = vec![
402 (PayloadKind::PlayerStatus, "player_status"),
403 (PayloadKind::Queue, "queue"),
404 (PayloadKind::Track, "track"),
405 (PayloadKind::Album, "album"),
406 (PayloadKind::Artist, "artist"),
407 (PayloadKind::Playlist, "playlist"),
408 (PayloadKind::SavedTracks, "saved_tracks"),
409 (PayloadKind::LibraryCheck, "library_check"),
410 ];
411 for (kind, expected) in kinds {
412 let serialized = serde_json::to_string(&kind).unwrap();
413 assert!(
414 serialized.contains(expected),
415 "Expected {} in {}",
416 expected,
417 serialized
418 );
419 }
420 }
421
422 #[test]
423 fn error_response_has_no_payload_kind() {
424 let resp = Response::err(404, "Not found", ErrorKind::NotFound);
425 assert!(resp.payload_kind.is_none());
426 }
427
428 #[test]
429 fn err_with_details_includes_details() {
430 let resp =
431 Response::err_with_details(500, "Storage failed", ErrorKind::Storage, "Disk full");
432 assert!(resp.error.is_some());
433 let error = resp.error.unwrap();
434 assert_eq!(error.kind, "storage_error");
435 assert_eq!(error.details, Some("Disk full".to_string()));
436 }
437
438 #[test]
439 fn all_error_kinds_as_str() {
440 assert_eq!(ErrorKind::Network.as_str(), "network_error");
441 assert_eq!(ErrorKind::Api.as_str(), "api_error");
442 assert_eq!(ErrorKind::Auth.as_str(), "auth_error");
443 assert_eq!(ErrorKind::NotFound.as_str(), "not_found");
444 assert_eq!(ErrorKind::Forbidden.as_str(), "forbidden");
445 assert_eq!(ErrorKind::RateLimited.as_str(), "rate_limited");
446 assert_eq!(ErrorKind::Validation.as_str(), "validation_error");
447 assert_eq!(ErrorKind::Storage.as_str(), "storage_error");
448 assert_eq!(ErrorKind::Config.as_str(), "config_error");
449 assert_eq!(ErrorKind::Player.as_str(), "player_error");
450 }
451
452 #[test]
453 fn error_kind_display() {
454 assert_eq!(format!("{}", ErrorKind::Network), "network_error");
455 assert_eq!(format!("{}", ErrorKind::Auth), "auth_error");
456 }
457
458 #[test]
459 fn from_http_error_unauthorized() {
460 let http_err = HttpError::Unauthorized;
461 let resp = Response::from_http_error(&http_err, "Auth check");
462 assert_eq!(resp.code, 401);
463 assert!(resp.message.contains("Unauthorized"));
464 assert!(resp.error.is_some());
465 }
466
467 #[test]
468 fn from_http_error_not_found() {
469 let http_err = HttpError::NotFound;
470 let resp = Response::from_http_error(&http_err, "Get resource");
471 assert_eq!(resp.code, 404);
472 assert!(resp.message.contains("Not Found"));
473 }
474
475 #[test]
476 fn from_http_error_rate_limited() {
477 let http_err = HttpError::RateLimited {
478 retry_after_secs: 30,
479 };
480 let resp = Response::from_http_error(&http_err, "API call");
481 assert_eq!(resp.code, 429);
482 assert!(resp.message.contains("Rate Limited"));
483 }
484
485 #[test]
486 fn from_http_error_forbidden() {
487 let http_err = HttpError::Forbidden;
488 let resp = Response::from_http_error(&http_err, "Action");
489 assert_eq!(resp.code, 403);
490 assert!(resp.message.contains("Forbidden"));
491 }
492
493 #[test]
494 fn from_http_error_api_error() {
495 let http_err = HttpError::Api {
496 status: 500,
497 message: "Server error".to_string(),
498 };
499 let resp = Response::from_http_error(&http_err, "Request");
500 assert_eq!(resp.code, 500);
501 assert!(resp.message.contains("Internal Server Error"));
502 }
503
504 #[test]
505 fn status_serialization() {
506 let success = serde_json::to_string(&Status::Success).unwrap();
507 assert!(success.contains("success"));
508
509 let error = serde_json::to_string(&Status::Error).unwrap();
510 assert!(error.contains("error"));
511 }
512
513 #[test]
514 fn more_payload_kind_variants() {
515 let kinds = vec![
516 (PayloadKind::Devices, "devices"),
517 (PayloadKind::PlayHistory, "play_history"),
518 (PayloadKind::SearchResults, "search_results"),
519 (PayloadKind::Show, "show"),
520 (PayloadKind::Episode, "episode"),
521 (PayloadKind::Audiobook, "audiobook"),
522 (PayloadKind::Chapter, "chapter"),
523 (PayloadKind::Category, "category"),
524 (PayloadKind::User, "user"),
525 (PayloadKind::TrackList, "track_list"),
526 (PayloadKind::AlbumList, "album_list"),
527 (PayloadKind::Markets, "markets"),
528 (PayloadKind::Generic, "generic"),
529 ];
530 for (kind, expected) in kinds {
531 let serialized = serde_json::to_string(&kind).unwrap();
532 assert!(
533 serialized.contains(expected),
534 "Expected {} in {}",
535 expected,
536 serialized
537 );
538 }
539 }
540
541 #[test]
542 fn print_json_outputs_valid_json() {
543 let resp = Response::success(200, "Test");
544 print_json(&resp);
545 }
546
547 #[test]
548 fn print_human_success_with_payload() {
549 let payload = serde_json::json!({"name": "Test"});
550 let resp = Response::success_with_payload(200, "Message", payload);
551 print_human(&resp);
552 }
553
554 #[test]
555 fn print_human_success_without_payload() {
556 let resp = Response::success(200, "Simple message");
557 print_human(&resp);
558 }
559
560 #[test]
561 fn print_human_error_with_details() {
562 let resp = Response::err_with_details(
563 500,
564 "Operation failed",
565 ErrorKind::Api,
566 "Detailed error info",
567 );
568 print_human(&resp);
569 }
570
571 #[test]
572 fn print_human_error_without_details() {
573 let resp = Response::err(404, "Not found", ErrorKind::NotFound);
574 print_human(&resp);
575 }
576
577 #[test]
578 fn from_http_error_api_includes_details() {
579 let http_err = HttpError::Api {
580 status: 500,
581 message: "Server error".to_string(),
582 };
583 let resp = Response::from_http_error(&http_err, "Request failed");
584 assert!(resp.error.is_some());
585 let error = resp.error.unwrap();
586 assert_eq!(error.kind, "api_error");
587 assert!(error.details.is_some());
588 }
589
590 #[test]
591 fn from_http_error_unusual_status() {
592 let http_err = HttpError::Api {
593 status: 418,
594 message: "I'm a teapot".to_string(),
595 };
596 let resp = Response::from_http_error(&http_err, "Request");
597 assert_eq!(resp.code, 418);
598 assert!(resp.message.contains("418"));
600 }
601
602 #[test]
603 fn from_http_error_bad_gateway() {
604 let http_err = HttpError::Api {
605 status: 502,
606 message: "Bad Gateway".to_string(),
607 };
608 let resp = Response::from_http_error(&http_err, "Request");
609 assert_eq!(resp.code, 502);
610 assert!(resp.message.contains("Bad Gateway"));
611 }
612
613 #[test]
614 fn from_http_error_service_unavailable() {
615 let http_err = HttpError::Api {
616 status: 503,
617 message: "Unavailable".to_string(),
618 };
619 let resp = Response::from_http_error(&http_err, "Request");
620 assert_eq!(resp.code, 503);
621 assert!(resp.message.contains("Service Unavailable"));
622 }
623
624 #[test]
625 fn from_http_error_bad_request() {
626 let http_err = HttpError::Api {
627 status: 400,
628 message: "Invalid params".to_string(),
629 };
630 let resp = Response::from_http_error(&http_err, "Validation");
631 assert_eq!(resp.code, 400);
632 assert!(resp.message.contains("Bad Request"));
633 }
634
635 #[test]
636 fn print_human_with_typed_payload() {
637 let payload = serde_json::json!({"name": "Test Track"});
638 let resp = Response::success_typed(200, "Track", PayloadKind::Track, payload);
639 print_human(&resp);
640 }
641}