Skip to main content

shopify_sdk/rest/
response.rs

1//! Response wrapper for REST resource operations.
2//!
3//! This module provides [`ResourceResponse<T>`], a wrapper that combines
4//! resource data with metadata like pagination and rate limit information.
5//! The wrapper implements `Deref` for ergonomic access to the inner data.
6//!
7//! # Deref Pattern
8//!
9//! `ResourceResponse<T>` implements `Deref<Target = T>`, which means you can
10//! use it like the inner type directly:
11//!
12//! ```rust,ignore
13//! let response: ResourceResponse<Vec<Product>> = Product::all(&client, None).await?;
14//!
15//! // Iterate directly (Vec method via Deref)
16//! for product in response.iter() {
17//!     println!("{}", product.title);
18//! }
19//!
20//! // Access length (Vec method via Deref)
21//! println!("Count: {}", response.len());
22//!
23//! // Index access (Vec trait via Deref)
24//! let first = &response[0];
25//! ```
26//!
27//! # Example
28//!
29//! ```rust,ignore
30//! use shopify_sdk::rest::{RestResource, ResourceResponse};
31//!
32//! // Fetch products with pagination
33//! let response: ResourceResponse<Vec<Product>> = Product::all(&client, None).await?;
34//!
35//! // Access products directly via Deref
36//! for product in response.iter() {
37//!     println!("Product: {}", product.title);
38//! }
39//!
40//! // Check pagination
41//! if response.has_next_page() {
42//!     let page_info = response.next_page_info().unwrap();
43//!     // Fetch next page using page_info...
44//! }
45//!
46//! // Check rate limits
47//! if let Some(limit) = response.rate_limit() {
48//!     println!("API calls: {}/{}", limit.request_count, limit.bucket_size);
49//! }
50//!
51//! // Take ownership of inner data
52//! let products: Vec<Product> = response.into_inner();
53//! ```
54
55use std::ops::{Deref, DerefMut};
56
57use serde::de::DeserializeOwned;
58
59use crate::clients::{ApiCallLimit, HttpResponse, PaginationInfo};
60use crate::rest::ResourceError;
61
62/// A response from a REST resource operation.
63///
64/// This wrapper combines the resource data with metadata from the HTTP
65/// response, including pagination information and rate limit data.
66///
67/// The struct implements `Deref<Target = T>` for transparent access to
68/// the inner data. This allows calling methods on `T` directly through
69/// the response wrapper.
70///
71/// # Type Parameters
72///
73/// * `T` - The type of data contained in the response. For single resources
74///   this is the resource type (e.g., `Product`). For collections, this is
75///   `Vec<ResourceType>` (e.g., `Vec<Product>`).
76///
77/// # Example
78///
79/// ```rust
80/// use shopify_sdk::rest::ResourceResponse;
81/// use shopify_sdk::clients::{ApiCallLimit, PaginationInfo};
82///
83/// // Create a response with a vector of items
84/// let response = ResourceResponse::new(
85///     vec!["item1", "item2", "item3"],
86///     Some(PaginationInfo {
87///         prev_page_info: None,
88///         next_page_info: Some("eyJsYXN0X2lkIjo0fQ".to_string()),
89///     }),
90///     Some(ApiCallLimit { request_count: 1, bucket_size: 40 }),
91///     Some("req-123".to_string()),
92/// );
93///
94/// // Access items via Deref
95/// assert_eq!(response.len(), 3);
96/// assert_eq!(response[0], "item1");
97///
98/// // Access metadata
99/// assert!(response.has_next_page());
100/// assert!(!response.has_prev_page());
101/// ```
102#[derive(Debug, Clone)]
103pub struct ResourceResponse<T> {
104    /// The resource data.
105    data: T,
106    /// Pagination information from the Link header.
107    pagination: Option<PaginationInfo>,
108    /// Rate limit information from the API call limit header.
109    rate_limit: Option<ApiCallLimit>,
110    /// Request ID from the X-Request-Id header.
111    request_id: Option<String>,
112}
113
114impl<T> ResourceResponse<T> {
115    /// Creates a new `ResourceResponse` with the given data and metadata.
116    ///
117    /// # Arguments
118    ///
119    /// * `data` - The resource data
120    /// * `pagination` - Pagination info from Link header
121    /// * `rate_limit` - Rate limit info from API call limit header
122    /// * `request_id` - Request ID from X-Request-Id header
123    #[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    /// Consumes the response and returns the inner data.
139    ///
140    /// Use this when you need ownership of the data and no longer
141    /// need the response metadata.
142    ///
143    /// # Example
144    ///
145    /// ```rust
146    /// use shopify_sdk::rest::ResourceResponse;
147    ///
148    /// let response = ResourceResponse::new(
149    ///     vec![1, 2, 3],
150    ///     None,
151    ///     None,
152    ///     None,
153    /// );
154    /// let data: Vec<i32> = response.into_inner();
155    /// assert_eq!(data, vec![1, 2, 3]);
156    /// ```
157    #[must_use]
158    pub fn into_inner(self) -> T {
159        self.data
160    }
161
162    /// Returns a reference to the inner data.
163    ///
164    /// Note: In most cases, you can use Deref coercion instead of
165    /// calling this method explicitly.
166    #[must_use]
167    pub const fn data(&self) -> &T {
168        &self.data
169    }
170
171    /// Returns a mutable reference to the inner data.
172    ///
173    /// Note: In most cases, you can use `DerefMut` coercion instead of
174    /// calling this method explicitly.
175    #[must_use]
176    pub fn data_mut(&mut self) -> &mut T {
177        &mut self.data
178    }
179
180    /// Returns `true` if there is a next page of results.
181    ///
182    /// # Example
183    ///
184    /// ```rust
185    /// use shopify_sdk::rest::ResourceResponse;
186    /// use shopify_sdk::clients::PaginationInfo;
187    ///
188    /// let response = ResourceResponse::new(
189    ///     vec!["item"],
190    ///     Some(PaginationInfo {
191    ///         prev_page_info: None,
192    ///         next_page_info: Some("token".to_string()),
193    ///     }),
194    ///     None,
195    ///     None,
196    /// );
197    /// assert!(response.has_next_page());
198    /// ```
199    #[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    /// Returns `true` if there is a previous page of results.
207    #[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    /// Returns the page info token for the next page, if available.
215    ///
216    /// Use this token with the `page_info` query parameter to fetch
217    /// the next page of results.
218    #[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    /// Returns the page info token for the previous page, if available.
226    #[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    /// Returns the pagination info, if available.
234    #[must_use]
235    pub const fn pagination(&self) -> Option<&PaginationInfo> {
236        self.pagination.as_ref()
237    }
238
239    /// Returns the rate limit information, if available.
240    ///
241    /// # Example
242    ///
243    /// ```rust
244    /// use shopify_sdk::rest::ResourceResponse;
245    /// use shopify_sdk::clients::ApiCallLimit;
246    ///
247    /// let response = ResourceResponse::new(
248    ///     "data",
249    ///     None,
250    ///     Some(ApiCallLimit { request_count: 5, bucket_size: 40 }),
251    ///     None,
252    /// );
253    ///
254    /// let limit = response.rate_limit().unwrap();
255    /// assert_eq!(limit.request_count, 5);
256    /// assert_eq!(limit.bucket_size, 40);
257    /// ```
258    #[must_use]
259    pub const fn rate_limit(&self) -> Option<&ApiCallLimit> {
260        self.rate_limit.as_ref()
261    }
262
263    /// Returns the request ID from the response headers.
264    ///
265    /// Useful for debugging and error reporting.
266    #[must_use]
267    pub fn request_id(&self) -> Option<&str> {
268        self.request_id.as_deref()
269    }
270
271    /// Maps the inner data to a new type.
272    ///
273    /// Useful for transforming the response data while preserving metadata.
274    #[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    /// Creates a `ResourceResponse` from an HTTP response.
290    ///
291    /// Extracts the data from the response body under the given key,
292    /// along with pagination and rate limit information.
293    ///
294    /// # Arguments
295    ///
296    /// * `response` - The HTTP response
297    /// * `key` - The key in the response body containing the data
298    ///
299    /// # Errors
300    ///
301    /// Returns [`ResourceError::Http`] if the data cannot be deserialized.
302    ///
303    /// # Example
304    ///
305    /// ```rust,ignore
306    /// use shopify_sdk::rest::ResourceResponse;
307    ///
308    /// // Assuming response.body = {"product": {"id": 123, "title": "Test"}}
309    /// let response: ResourceResponse<Product> = ResourceResponse::from_http_response(
310    ///     http_response,
311    ///     "product",
312    /// )?;
313    /// ```
314    pub fn from_http_response(response: HttpResponse, key: &str) -> Result<Self, ResourceError> {
315        // Extract request_id before any potential moves
316        let request_id = response.request_id().map(ToString::to_string);
317
318        // Extract the data from the response body
319        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        // Deserialize the data
330        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        // Build pagination info
341        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
359/// Provides transparent access to the inner data.
360///
361/// This allows methods of `T` to be called directly on `ResourceResponse<T>`.
362impl<T> Deref for ResourceResponse<T> {
363    type Target = T;
364
365    fn deref(&self) -> &Self::Target {
366        &self.data
367    }
368}
369
370/// Provides mutable access to the inner data.
371impl<T> DerefMut for ResourceResponse<T> {
372    fn deref_mut(&mut self) -> &mut Self::Target {
373        &mut self.data
374    }
375}
376
377// Verify ResourceResponse is Send + Sync when T is Send + Sync
378const _: 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        // Vec methods via Deref
426        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        // Mutate via DerefMut
436        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        // Iterate via Deref to Vec
551        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        // Access fields via Deref
565        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}