Skip to main content

shopify_sdk/rest/resources/v2025_10/
collect.rs

1//! Collect resource implementation.
2//!
3//! This module provides the [`Collect`] resource for managing the connections
4//! between products and custom collections.
5//!
6//! # What is a Collect?
7//!
8//! A Collect is a join table that connects a product to a custom collection.
9//! Each collect represents one product-collection relationship.
10//!
11//! # Limited Operations
12//!
13//! Collect has limited operations:
14//! - **Create**: Add a product to a collection
15//! - **Find**: Get a specific collect
16//! - **List**: List collects (filterable by product or collection)
17//! - **Count**: Count collects
18//! - **Delete**: Remove a product from a collection
19//!
20//! **No Update operation** - to change a collect, delete and recreate it.
21//!
22//! # Example
23//!
24//! ```rust,ignore
25//! use shopify_sdk::rest::{RestResource, ResourceResponse};
26//! use shopify_sdk::rest::resources::v2025_10::{Collect, CollectListParams};
27//!
28//! // Add a product to a collection
29//! let collect = Collect {
30//!     product_id: Some(632910392),
31//!     collection_id: Some(841564295),
32//!     ..Default::default()
33//! };
34//! let saved = collect.save(&client).await?;
35//!
36//! // List all products in a collection
37//! let params = CollectListParams {
38//!     collection_id: Some(841564295),
39//!     ..Default::default()
40//! };
41//! let collects = Collect::all(&client, Some(params)).await?;
42//!
43//! // Remove a product from a collection
44//! Collect::delete(&client, collect_id).await?;
45//! ```
46
47use serde::{Deserialize, Serialize};
48
49use crate::rest::{ResourceOperation, ResourcePath, RestResource};
50use crate::HttpMethod;
51
52/// A connection between a product and a custom collection.
53///
54/// Collects represent the many-to-many relationship between products
55/// and custom collections. Smart collections manage their own products
56/// automatically based on conditions.
57///
58/// # Limited Operations
59///
60/// - **Create**: Yes - add product to collection
61/// - **Find**: Yes - get specific collect
62/// - **List**: Yes - filterable by product_id or collection_id
63/// - **Count**: Yes - filterable by product_id or collection_id
64/// - **Update**: No - delete and recreate instead
65/// - **Delete**: Yes - remove product from collection
66///
67/// # Fields
68///
69/// ## Read-Only Fields
70/// - `id` - The unique identifier
71/// - `created_at` - When the collect was created
72/// - `updated_at` - When the collect was last updated
73///
74/// ## Writable Fields
75/// - `product_id` - The ID of the product
76/// - `collection_id` - The ID of the custom collection
77/// - `position` - The position of the product in the collection
78/// - `sort_value` - The sort value for the product
79#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
80pub struct Collect {
81    /// The unique identifier of the collect.
82    #[serde(skip_serializing)]
83    pub id: Option<u64>,
84
85    /// The ID of the product in this collect.
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub product_id: Option<u64>,
88
89    /// The ID of the custom collection containing the product.
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub collection_id: Option<u64>,
92
93    /// The position of the product in the collection.
94    /// Products are sorted by position in ascending order.
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub position: Option<i32>,
97
98    /// A string used for sorting when the collection is sorted manually.
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub sort_value: Option<String>,
101
102    /// When the collect was created.
103    #[serde(skip_serializing)]
104    pub created_at: Option<String>,
105
106    /// When the collect was last updated.
107    #[serde(skip_serializing)]
108    pub updated_at: Option<String>,
109}
110
111impl RestResource for Collect {
112    type Id = u64;
113    type FindParams = CollectFindParams;
114    type AllParams = CollectListParams;
115    type CountParams = CollectCountParams;
116
117    const NAME: &'static str = "Collect";
118    const PLURAL: &'static str = "collects";
119
120    /// Paths for the Collect resource.
121    ///
122    /// Limited operations: No Update.
123    const PATHS: &'static [ResourcePath] = &[
124        ResourcePath::new(
125            HttpMethod::Get,
126            ResourceOperation::Find,
127            &["id"],
128            "collects/{id}",
129        ),
130        ResourcePath::new(HttpMethod::Get, ResourceOperation::All, &[], "collects"),
131        ResourcePath::new(
132            HttpMethod::Get,
133            ResourceOperation::Count,
134            &[],
135            "collects/count",
136        ),
137        ResourcePath::new(HttpMethod::Post, ResourceOperation::Create, &[], "collects"),
138        ResourcePath::new(
139            HttpMethod::Delete,
140            ResourceOperation::Delete,
141            &["id"],
142            "collects/{id}",
143        ),
144        // Note: No Update path - collects cannot be modified, only created or deleted
145    ];
146
147    fn get_id(&self) -> Option<Self::Id> {
148        self.id
149    }
150}
151
152/// Parameters for finding a single collect.
153#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
154pub struct CollectFindParams {
155    /// Comma-separated list of fields to include in the response.
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub fields: Option<String>,
158}
159
160/// Parameters for listing collects.
161#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
162pub struct CollectListParams {
163    /// Maximum number of results to return.
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub limit: Option<u32>,
166
167    /// Return collects after this ID.
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub since_id: Option<u64>,
170
171    /// Filter by product ID.
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub product_id: Option<u64>,
174
175    /// Filter by collection ID.
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub collection_id: Option<u64>,
178
179    /// Comma-separated list of fields to include in the response.
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub fields: Option<String>,
182}
183
184/// Parameters for counting collects.
185#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
186pub struct CollectCountParams {
187    /// Filter by product ID.
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub product_id: Option<u64>,
190
191    /// Filter by collection ID.
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub collection_id: Option<u64>,
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199    use crate::rest::{get_path, ResourceOperation, RestResource};
200
201    #[test]
202    fn test_collect_serialization() {
203        let collect = Collect {
204            id: Some(455204334),
205            product_id: Some(632910392),
206            collection_id: Some(841564295),
207            position: Some(1),
208            sort_value: Some("0000000001".to_string()),
209            created_at: Some("2024-01-15T10:30:00-05:00".to_string()),
210            updated_at: Some("2024-01-15T10:30:00-05:00".to_string()),
211        };
212
213        let json = serde_json::to_string(&collect).unwrap();
214        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
215
216        // Writable fields should be present
217        assert_eq!(parsed["product_id"], 632910392);
218        assert_eq!(parsed["collection_id"], 841564295);
219        assert_eq!(parsed["position"], 1);
220        assert_eq!(parsed["sort_value"], "0000000001");
221
222        // Read-only fields should be omitted
223        assert!(parsed.get("id").is_none());
224        assert!(parsed.get("created_at").is_none());
225        assert!(parsed.get("updated_at").is_none());
226    }
227
228    #[test]
229    fn test_collect_deserialization() {
230        let json = r#"{
231            "id": 455204334,
232            "product_id": 632910392,
233            "collection_id": 841564295,
234            "position": 1,
235            "sort_value": "0000000001",
236            "created_at": "2024-01-15T10:30:00-05:00",
237            "updated_at": "2024-01-15T10:30:00-05:00"
238        }"#;
239
240        let collect: Collect = serde_json::from_str(json).unwrap();
241
242        assert_eq!(collect.id, Some(455204334));
243        assert_eq!(collect.product_id, Some(632910392));
244        assert_eq!(collect.collection_id, Some(841564295));
245        assert_eq!(collect.position, Some(1));
246        assert_eq!(collect.sort_value, Some("0000000001".to_string()));
247        assert_eq!(
248            collect.created_at,
249            Some("2024-01-15T10:30:00-05:00".to_string())
250        );
251    }
252
253    #[test]
254    fn test_collect_limited_paths_no_update() {
255        // Find
256        let find_path = get_path(Collect::PATHS, ResourceOperation::Find, &["id"]);
257        assert!(find_path.is_some());
258        assert_eq!(find_path.unwrap().template, "collects/{id}");
259
260        // All
261        let all_path = get_path(Collect::PATHS, ResourceOperation::All, &[]);
262        assert!(all_path.is_some());
263        assert_eq!(all_path.unwrap().template, "collects");
264
265        // Count
266        let count_path = get_path(Collect::PATHS, ResourceOperation::Count, &[]);
267        assert!(count_path.is_some());
268        assert_eq!(count_path.unwrap().template, "collects/count");
269
270        // Create
271        let create_path = get_path(Collect::PATHS, ResourceOperation::Create, &[]);
272        assert!(create_path.is_some());
273        assert_eq!(create_path.unwrap().template, "collects");
274
275        // Delete
276        let delete_path = get_path(Collect::PATHS, ResourceOperation::Delete, &["id"]);
277        assert!(delete_path.is_some());
278        assert_eq!(delete_path.unwrap().template, "collects/{id}");
279
280        // No Update (the key differentiator)
281        let update_path = get_path(Collect::PATHS, ResourceOperation::Update, &["id"]);
282        assert!(update_path.is_none());
283    }
284
285    #[test]
286    fn test_collect_constants() {
287        assert_eq!(Collect::NAME, "Collect");
288        assert_eq!(Collect::PLURAL, "collects");
289    }
290
291    #[test]
292    fn test_collect_get_id() {
293        let collect_with_id = Collect {
294            id: Some(455204334),
295            product_id: Some(632910392),
296            collection_id: Some(841564295),
297            ..Default::default()
298        };
299        assert_eq!(collect_with_id.get_id(), Some(455204334));
300
301        let collect_without_id = Collect::default();
302        assert_eq!(collect_without_id.get_id(), None);
303    }
304
305    #[test]
306    fn test_collect_list_params() {
307        let params = CollectListParams {
308            limit: Some(50),
309            since_id: Some(1000),
310            product_id: Some(632910392),
311            collection_id: Some(841564295),
312            fields: Some("id,product_id,collection_id".to_string()),
313        };
314
315        let json = serde_json::to_value(&params).unwrap();
316
317        assert_eq!(json["limit"], 50);
318        assert_eq!(json["since_id"], 1000);
319        assert_eq!(json["product_id"], 632910392);
320        assert_eq!(json["collection_id"], 841564295);
321        assert_eq!(json["fields"], "id,product_id,collection_id");
322    }
323
324    #[test]
325    fn test_collect_count_params() {
326        let params = CollectCountParams {
327            product_id: Some(632910392),
328            collection_id: None,
329        };
330
331        let json = serde_json::to_value(&params).unwrap();
332
333        assert_eq!(json["product_id"], 632910392);
334        assert!(json.get("collection_id").is_none());
335    }
336}