1use std::ops::{Deref, DerefMut};
56
57use serde::de::DeserializeOwned;
58
59use crate::clients::{ApiCallLimit, HttpResponse, PaginationInfo};
60use crate::rest::ResourceError;
61
62#[derive(Debug, Clone)]
103pub struct ResourceResponse<T> {
104 data: T,
106 pagination: Option<PaginationInfo>,
108 rate_limit: Option<ApiCallLimit>,
110 request_id: Option<String>,
112}
113
114impl<T> ResourceResponse<T> {
115 #[must_use]
124 pub const fn new(
125 data: T,
126 pagination: Option<PaginationInfo>,
127 rate_limit: Option<ApiCallLimit>,
128 request_id: Option<String>,
129 ) -> Self {
130 Self {
131 data,
132 pagination,
133 rate_limit,
134 request_id,
135 }
136 }
137
138 #[must_use]
158 pub fn into_inner(self) -> T {
159 self.data
160 }
161
162 #[must_use]
167 pub const fn data(&self) -> &T {
168 &self.data
169 }
170
171 #[must_use]
176 pub fn data_mut(&mut self) -> &mut T {
177 &mut self.data
178 }
179
180 #[must_use]
200 pub fn has_next_page(&self) -> bool {
201 self.pagination
202 .as_ref()
203 .is_some_and(|p| p.next_page_info.is_some())
204 }
205
206 #[must_use]
208 pub fn has_prev_page(&self) -> bool {
209 self.pagination
210 .as_ref()
211 .is_some_and(|p| p.prev_page_info.is_some())
212 }
213
214 #[must_use]
219 pub fn next_page_info(&self) -> Option<&str> {
220 self.pagination
221 .as_ref()
222 .and_then(|p| p.next_page_info.as_deref())
223 }
224
225 #[must_use]
227 pub fn prev_page_info(&self) -> Option<&str> {
228 self.pagination
229 .as_ref()
230 .and_then(|p| p.prev_page_info.as_deref())
231 }
232
233 #[must_use]
235 pub const fn pagination(&self) -> Option<&PaginationInfo> {
236 self.pagination.as_ref()
237 }
238
239 #[must_use]
259 pub const fn rate_limit(&self) -> Option<&ApiCallLimit> {
260 self.rate_limit.as_ref()
261 }
262
263 #[must_use]
267 pub fn request_id(&self) -> Option<&str> {
268 self.request_id.as_deref()
269 }
270
271 #[must_use]
275 pub fn map<U, F>(self, f: F) -> ResourceResponse<U>
276 where
277 F: FnOnce(T) -> U,
278 {
279 ResourceResponse {
280 data: f(self.data),
281 pagination: self.pagination,
282 rate_limit: self.rate_limit,
283 request_id: self.request_id,
284 }
285 }
286}
287
288impl<T: DeserializeOwned> ResourceResponse<T> {
289 pub fn from_http_response(response: HttpResponse, key: &str) -> Result<Self, ResourceError> {
315 let request_id = response.request_id().map(ToString::to_string);
317
318 let data_value = response.body.get(key).ok_or_else(|| {
320 ResourceError::Http(crate::clients::HttpError::Response(
321 crate::clients::HttpResponseError {
322 code: response.code,
323 message: format!("Missing key '{key}' in response body"),
324 error_reference: request_id.clone(),
325 },
326 ))
327 })?;
328
329 let data: T = serde_json::from_value(data_value.clone()).map_err(|e| {
331 ResourceError::Http(crate::clients::HttpError::Response(
332 crate::clients::HttpResponseError {
333 code: response.code,
334 message: format!("Failed to deserialize '{key}': {e}"),
335 error_reference: request_id.clone(),
336 },
337 ))
338 })?;
339
340 let pagination = if response.prev_page_info.is_some() || response.next_page_info.is_some() {
342 Some(PaginationInfo {
343 prev_page_info: response.prev_page_info,
344 next_page_info: response.next_page_info,
345 })
346 } else {
347 None
348 };
349
350 Ok(Self {
351 data,
352 pagination,
353 rate_limit: response.api_call_limit,
354 request_id,
355 })
356 }
357}
358
359impl<T> Deref for ResourceResponse<T> {
363 type Target = T;
364
365 fn deref(&self) -> &Self::Target {
366 &self.data
367 }
368}
369
370impl<T> DerefMut for ResourceResponse<T> {
372 fn deref_mut(&mut self) -> &mut Self::Target {
373 &mut self.data
374 }
375}
376
377const _: fn() = || {
379 const fn assert_send_sync<T: Send + Sync>() {}
380 assert_send_sync::<ResourceResponse<String>>();
381 assert_send_sync::<ResourceResponse<Vec<String>>>();
382};
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387 use serde::{Deserialize, Serialize};
388 use serde_json::json;
389 use std::collections::HashMap;
390
391 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
392 struct TestProduct {
393 id: u64,
394 title: String,
395 }
396
397 #[test]
398 fn test_resource_response_stores_data_and_metadata() {
399 let pagination = PaginationInfo {
400 prev_page_info: Some("prev".to_string()),
401 next_page_info: Some("next".to_string()),
402 };
403 let rate_limit = ApiCallLimit {
404 request_count: 5,
405 bucket_size: 40,
406 };
407
408 let response = ResourceResponse::new(
409 vec!["item1", "item2"],
410 Some(pagination),
411 Some(rate_limit),
412 Some("req-123".to_string()),
413 );
414
415 assert_eq!(response.data.len(), 2);
416 assert!(response.pagination.is_some());
417 assert!(response.rate_limit.is_some());
418 assert_eq!(response.request_id, Some("req-123".to_string()));
419 }
420
421 #[test]
422 fn test_deref_allows_direct_access_to_inner_data() {
423 let response = ResourceResponse::new(vec!["item1", "item2", "item3"], None, None, None);
424
425 assert_eq!(response.len(), 3);
427 assert!(!response.is_empty());
428 assert_eq!(response.first(), Some(&"item1"));
429 }
430
431 #[test]
432 fn test_deref_mut_allows_mutable_access() {
433 let mut response = ResourceResponse::new(vec!["item1", "item2"], None, None, None);
434
435 response.push("item3");
437 assert_eq!(response.len(), 3);
438
439 response[0] = "modified";
440 assert_eq!(response[0], "modified");
441 }
442
443 #[test]
444 fn test_into_inner_returns_owned_data() {
445 let response = ResourceResponse::new(vec![1, 2, 3], None, None, None);
446
447 let data: Vec<i32> = response.into_inner();
448 assert_eq!(data, vec![1, 2, 3]);
449 }
450
451 #[test]
452 fn test_has_next_page_returns_correct_boolean() {
453 let response_with_next = ResourceResponse::new(
454 "data",
455 Some(PaginationInfo {
456 prev_page_info: None,
457 next_page_info: Some("token".to_string()),
458 }),
459 None,
460 None,
461 );
462 assert!(response_with_next.has_next_page());
463
464 let response_without_next = ResourceResponse::new(
465 "data",
466 Some(PaginationInfo {
467 prev_page_info: Some("prev".to_string()),
468 next_page_info: None,
469 }),
470 None,
471 None,
472 );
473 assert!(!response_without_next.has_next_page());
474
475 let response_no_pagination: ResourceResponse<&str> =
476 ResourceResponse::new("data", None, None, None);
477 assert!(!response_no_pagination.has_next_page());
478 }
479
480 #[test]
481 fn test_has_prev_page_returns_correct_boolean() {
482 let response_with_prev = ResourceResponse::new(
483 "data",
484 Some(PaginationInfo {
485 prev_page_info: Some("token".to_string()),
486 next_page_info: None,
487 }),
488 None,
489 None,
490 );
491 assert!(response_with_prev.has_prev_page());
492
493 let response_without_prev = ResourceResponse::new(
494 "data",
495 Some(PaginationInfo {
496 prev_page_info: None,
497 next_page_info: Some("next".to_string()),
498 }),
499 None,
500 None,
501 );
502 assert!(!response_without_prev.has_prev_page());
503 }
504
505 #[test]
506 fn test_next_page_info_returns_option_str() {
507 let response = ResourceResponse::new(
508 "data",
509 Some(PaginationInfo {
510 prev_page_info: None,
511 next_page_info: Some("eyJsYXN0X2lkIjo0fQ".to_string()),
512 }),
513 None,
514 None,
515 );
516
517 assert_eq!(response.next_page_info(), Some("eyJsYXN0X2lkIjo0fQ"));
518 }
519
520 #[test]
521 fn test_prev_page_info_returns_option_str() {
522 let response = ResourceResponse::new(
523 "data",
524 Some(PaginationInfo {
525 prev_page_info: Some("eyJsYXN0X2lkIjoxfQ".to_string()),
526 next_page_info: None,
527 }),
528 None,
529 None,
530 );
531
532 assert_eq!(response.prev_page_info(), Some("eyJsYXN0X2lkIjoxfQ"));
533 }
534
535 #[test]
536 fn test_resource_response_vec_allows_iteration_via_deref() {
537 let products = vec![
538 TestProduct {
539 id: 1,
540 title: "Product 1".to_string(),
541 },
542 TestProduct {
543 id: 2,
544 title: "Product 2".to_string(),
545 },
546 ];
547
548 let response = ResourceResponse::new(products, None, None, None);
549
550 let titles: Vec<&str> = response.iter().map(|p| p.title.as_str()).collect();
552 assert_eq!(titles, vec!["Product 1", "Product 2"]);
553 }
554
555 #[test]
556 fn test_resource_response_single_allows_field_access_via_deref() {
557 let product = TestProduct {
558 id: 123,
559 title: "Test Product".to_string(),
560 };
561
562 let response = ResourceResponse::new(product, None, None, None);
563
564 assert_eq!(response.id, 123);
566 assert_eq!(response.title, "Test Product");
567 }
568
569 #[test]
570 fn test_rate_limit_returns_api_call_limit() {
571 let rate_limit = ApiCallLimit {
572 request_count: 10,
573 bucket_size: 80,
574 };
575
576 let response = ResourceResponse::new("data", None, Some(rate_limit), None);
577
578 let limit = response.rate_limit().unwrap();
579 assert_eq!(limit.request_count, 10);
580 assert_eq!(limit.bucket_size, 80);
581 }
582
583 #[test]
584 fn test_request_id_returns_option_str() {
585 let response = ResourceResponse::new("data", None, None, Some("abc-123-xyz".to_string()));
586
587 assert_eq!(response.request_id(), Some("abc-123-xyz"));
588
589 let response_no_id: ResourceResponse<&str> =
590 ResourceResponse::new("data", None, None, None);
591 assert_eq!(response_no_id.request_id(), None);
592 }
593
594 #[test]
595 fn test_from_http_response_deserializes_data() {
596 let mut headers = HashMap::new();
597 headers.insert("x-request-id".to_string(), vec!["req-456".to_string()]);
598 headers.insert(
599 "x-shopify-shop-api-call-limit".to_string(),
600 vec!["5/40".to_string()],
601 );
602
603 let body = json!({
604 "product": {
605 "id": 123,
606 "title": "Test Product"
607 }
608 });
609
610 let http_response = HttpResponse::new(200, headers, body);
611
612 let response: ResourceResponse<TestProduct> =
613 ResourceResponse::from_http_response(http_response, "product").unwrap();
614
615 assert_eq!(response.id, 123);
616 assert_eq!(response.title, "Test Product");
617 assert_eq!(response.request_id(), Some("req-456"));
618 assert!(response.rate_limit().is_some());
619 }
620
621 #[test]
622 fn test_from_http_response_preserves_pagination() {
623 let mut headers = HashMap::new();
624 headers.insert(
625 "link".to_string(),
626 vec![
627 r#"<https://shop.myshopify.com/admin/api/2024-10/products.json?page_info=next123>; rel="next""#
628 .to_string(),
629 ],
630 );
631
632 let body = json!({
633 "products": [
634 {"id": 1, "title": "Product 1"},
635 {"id": 2, "title": "Product 2"}
636 ]
637 });
638
639 let http_response = HttpResponse::new(200, headers, body);
640
641 let response: ResourceResponse<Vec<TestProduct>> =
642 ResourceResponse::from_http_response(http_response, "products").unwrap();
643
644 assert!(response.has_next_page());
645 assert_eq!(response.next_page_info(), Some("next123"));
646 }
647
648 #[test]
649 fn test_map_transforms_data_preserving_metadata() {
650 let response = ResourceResponse::new(
651 vec![1, 2, 3],
652 Some(PaginationInfo {
653 prev_page_info: None,
654 next_page_info: Some("next".to_string()),
655 }),
656 Some(ApiCallLimit {
657 request_count: 1,
658 bucket_size: 40,
659 }),
660 Some("req-123".to_string()),
661 );
662
663 let mapped: ResourceResponse<Vec<String>> =
664 response.map(|v| v.iter().map(|n| n.to_string()).collect());
665
666 assert_eq!(*mapped, vec!["1", "2", "3"]);
667 assert!(mapped.has_next_page());
668 assert!(mapped.rate_limit().is_some());
669 assert_eq!(mapped.request_id(), Some("req-123"));
670 }
671}