Skip to main content

shopify_sdk/rest/resources/v2025_10/
smart_collection.rs

1//! `SmartCollection` resource implementation.
2//!
3//! This module provides the [`SmartCollection`] resource for managing rule-based
4//! product collections in Shopify.
5//!
6//! # Overview
7//!
8//! Smart collections automatically include products based on rules. For example,
9//! a smart collection can include all products with a specific tag, from a certain
10//! vendor, or within a price range.
11//!
12//! # Example
13//!
14//! ```rust,ignore
15//! use shopify_sdk::rest::resources::v2025_10::{SmartCollection, SmartCollectionListParams};
16//! use shopify_sdk::rest::resources::v2025_10::common::SmartCollectionRule;
17//! use shopify_sdk::rest::RestResource;
18//!
19//! // Find a single smart collection
20//! let collection = SmartCollection::find(&client, 123, None).await?;
21//! println!("Collection: {}", collection.title.as_deref().unwrap_or(""));
22//!
23//! // Create a smart collection with rules
24//! let collection = SmartCollection {
25//!     title: Some("Summer Products".to_string()),
26//!     rules: Some(vec![
27//!         SmartCollectionRule {
28//!             column: "tag".to_string(),
29//!             relation: "equals".to_string(),
30//!             condition: "summer".to_string(),
31//!         },
32//!     ]),
33//!     disjunctive: Some(false),  // All rules must match
34//!     ..Default::default()
35//! };
36//! let saved = collection.save(&client).await?;
37//!
38//! // Manually reorder products (when sort_order is "manual")
39//! collection.order(&client, vec![product_id_1, product_id_2, product_id_3]).await?;
40//! ```
41
42use std::collections::HashMap;
43
44use chrono::{DateTime, Utc};
45use serde::{Deserialize, Serialize};
46
47use crate::clients::RestClient;
48use crate::rest::{ResourceError, ResourceOperation, ResourcePath, RestResource};
49use crate::HttpMethod;
50
51use super::common::{CollectionImage, SmartCollectionRule};
52
53/// A rule-based collection of products.
54///
55/// Smart collections automatically include products that match specified rules.
56/// Products are dynamically added or removed based on the collection's rules.
57///
58/// # Rules
59///
60/// Each rule has three components:
61/// - `column` - The product property to check (e.g., "tag", "vendor", "title")
62/// - `relation` - How to compare (e.g., "equals", "contains", "`greater_than`")
63/// - `condition` - The value to compare against
64///
65/// # Disjunctive Mode
66///
67/// When `disjunctive` is `true`, products matching ANY rule are included (OR logic).
68/// When `disjunctive` is `false`, products must match ALL rules (AND logic).
69///
70/// # Fields
71///
72/// ## Read-Only Fields
73/// - `id` - The unique identifier
74/// - `handle` - The URL-friendly name (auto-generated from title)
75/// - `created_at` - When the collection was created
76/// - `updated_at` - When the collection was last updated
77/// - `admin_graphql_api_id` - The GraphQL API ID
78///
79/// ## Writable Fields
80/// - `title` - The name of the collection
81/// - `body_html` - The description in HTML format
82/// - `published_at` - When the collection was/will be published
83/// - `published_scope` - Where the collection is published
84/// - `sort_order` - How products are sorted
85/// - `template_suffix` - The template suffix
86/// - `image` - The collection's featured image
87/// - `rules` - The rules that determine which products are included
88/// - `disjunctive` - Whether rules use OR (true) or AND (false) logic
89///
90/// # Example
91///
92/// ```rust,ignore
93/// use shopify_sdk::rest::resources::v2025_10::SmartCollection;
94/// use shopify_sdk::rest::resources::v2025_10::common::SmartCollectionRule;
95///
96/// // Collection with multiple rules (AND logic)
97/// let collection = SmartCollection {
98///     title: Some("Premium Nike Products".to_string()),
99///     disjunctive: Some(false),  // All rules must match
100///     rules: Some(vec![
101///         SmartCollectionRule {
102///             column: "vendor".to_string(),
103///             relation: "equals".to_string(),
104///             condition: "Nike".to_string(),
105///         },
106///         SmartCollectionRule {
107///             column: "variant_price".to_string(),
108///             relation: "greater_than".to_string(),
109///             condition: "100".to_string(),
110///         },
111///     ]),
112///     ..Default::default()
113/// };
114/// ```
115#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
116pub struct SmartCollection {
117    // --- Read-only fields ---
118    /// The unique identifier of the smart collection.
119    #[serde(skip_serializing)]
120    pub id: Option<u64>,
121
122    /// The URL-friendly name of the collection.
123    /// Automatically generated from the title if not specified.
124    #[serde(skip_serializing)]
125    pub handle: Option<String>,
126
127    /// When the collection was created.
128    #[serde(skip_serializing)]
129    pub created_at: Option<DateTime<Utc>>,
130
131    /// When the collection was last updated.
132    #[serde(skip_serializing)]
133    pub updated_at: Option<DateTime<Utc>>,
134
135    /// The admin GraphQL API ID for this collection.
136    #[serde(skip_serializing)]
137    pub admin_graphql_api_id: Option<String>,
138
139    // --- Writable fields ---
140    /// The name of the collection.
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub title: Option<String>,
143
144    /// The description of the collection in HTML format.
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub body_html: Option<String>,
147
148    /// When the collection was or will be published.
149    /// Set to null to unpublish.
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub published_at: Option<DateTime<Utc>>,
152
153    /// Where the collection is published.
154    /// Valid values: "web", "global".
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub published_scope: Option<String>,
157
158    /// The order in which products appear in the collection.
159    ///
160    /// Valid values:
161    /// - `alpha-asc` - Alphabetically, A-Z
162    /// - `alpha-desc` - Alphabetically, Z-A
163    /// - `best-selling` - By best-selling products
164    /// - `created` - By date created, newest first
165    /// - `created-desc` - By date created, oldest first
166    /// - `manual` - Manual ordering (allows use of `order()` method)
167    /// - `price-asc` - By price, lowest to highest
168    /// - `price-desc` - By price, highest to lowest
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub sort_order: Option<String>,
171
172    /// The suffix of the Liquid template used for the collection page.
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub template_suffix: Option<String>,
175
176    /// The collection's featured image.
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub image: Option<CollectionImage>,
179
180    /// The rules that determine which products are included.
181    ///
182    /// Each rule specifies a column, relation, and condition.
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub rules: Option<Vec<SmartCollectionRule>>,
185
186    /// Whether products must match any rule (true) or all rules (false).
187    ///
188    /// - `true` - Products matching ANY rule are included (OR logic)
189    /// - `false` - Products must match ALL rules (AND logic)
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub disjunctive: Option<bool>,
192
193    /// Whether the collection is published.
194    /// Convenience field returned by the API.
195    #[serde(skip_serializing)]
196    pub published: Option<bool>,
197}
198
199impl RestResource for SmartCollection {
200    type Id = u64;
201    type FindParams = SmartCollectionFindParams;
202    type AllParams = SmartCollectionListParams;
203    type CountParams = SmartCollectionCountParams;
204
205    const NAME: &'static str = "SmartCollection";
206    const PLURAL: &'static str = "smart_collections";
207
208    const PATHS: &'static [ResourcePath] = &[
209        ResourcePath::new(
210            HttpMethod::Get,
211            ResourceOperation::Find,
212            &["id"],
213            "smart_collections/{id}",
214        ),
215        ResourcePath::new(
216            HttpMethod::Get,
217            ResourceOperation::All,
218            &[],
219            "smart_collections",
220        ),
221        ResourcePath::new(
222            HttpMethod::Get,
223            ResourceOperation::Count,
224            &[],
225            "smart_collections/count",
226        ),
227        ResourcePath::new(
228            HttpMethod::Post,
229            ResourceOperation::Create,
230            &[],
231            "smart_collections",
232        ),
233        ResourcePath::new(
234            HttpMethod::Put,
235            ResourceOperation::Update,
236            &["id"],
237            "smart_collections/{id}",
238        ),
239        ResourcePath::new(
240            HttpMethod::Delete,
241            ResourceOperation::Delete,
242            &["id"],
243            "smart_collections/{id}",
244        ),
245    ];
246
247    fn get_id(&self) -> Option<Self::Id> {
248        self.id
249    }
250}
251
252impl SmartCollection {
253    /// Manually reorders products in the smart collection.
254    ///
255    /// This method is only applicable when `sort_order` is set to "manual".
256    /// The products are reordered according to their position in the `product_ids` array.
257    ///
258    /// # Arguments
259    ///
260    /// * `client` - The REST client to use for the request
261    /// * `product_ids` - The product IDs in the desired order
262    ///
263    /// # Errors
264    ///
265    /// Returns [`ResourceError::PathResolutionFailed`] if the collection has no ID.
266    /// Returns [`ResourceError::Http`] if the API request fails.
267    ///
268    /// # Example
269    ///
270    /// ```rust,ignore
271    /// use shopify_sdk::rest::resources::v2025_10::SmartCollection;
272    ///
273    /// let collection = SmartCollection::find(&client, 123, None).await?.into_inner();
274    ///
275    /// // Reorder products (collection must have sort_order = "manual")
276    /// collection.order(&client, vec![
277    ///     111222333,  // First product
278    ///     444555666,  // Second product
279    ///     777888999,  // Third product
280    /// ]).await?;
281    /// ```
282    pub async fn order(
283        &self,
284        client: &RestClient,
285        product_ids: Vec<u64>,
286    ) -> Result<(), ResourceError> {
287        let id = self.get_id().ok_or(ResourceError::PathResolutionFailed {
288            resource: Self::NAME,
289            operation: "order",
290        })?;
291
292        // Build the query parameters with products[] array
293        let mut query: HashMap<String, String> = HashMap::new();
294        let products_param: String = product_ids
295            .iter()
296            .map(ToString::to_string)
297            .collect::<Vec<_>>()
298            .join(",");
299        query.insert("products[]".to_string(), products_param);
300
301        let path = format!("smart_collections/{id}/order");
302        let body = serde_json::json!({});
303
304        let response = client.put(&path, body, Some(query)).await?;
305
306        if !response.is_ok() {
307            return Err(ResourceError::from_http_response(
308                response.code,
309                &response.body,
310                Self::NAME,
311                Some(&id.to_string()),
312                response.request_id(),
313            ));
314        }
315
316        Ok(())
317    }
318}
319
320/// Parameters for finding a single smart collection.
321#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
322pub struct SmartCollectionFindParams {
323    /// Comma-separated list of fields to include in the response.
324    #[serde(skip_serializing_if = "Option::is_none")]
325    pub fields: Option<String>,
326}
327
328/// Parameters for listing smart collections.
329#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
330pub struct SmartCollectionListParams {
331    /// Return only collections with the given IDs.
332    #[serde(skip_serializing_if = "Option::is_none")]
333    pub ids: Option<Vec<u64>>,
334
335    /// Maximum number of results to return (default: 50, max: 250).
336    #[serde(skip_serializing_if = "Option::is_none")]
337    pub limit: Option<u32>,
338
339    /// Return collections after this ID.
340    #[serde(skip_serializing_if = "Option::is_none")]
341    pub since_id: Option<u64>,
342
343    /// Filter by collection title.
344    #[serde(skip_serializing_if = "Option::is_none")]
345    pub title: Option<String>,
346
347    /// Filter by collection handle.
348    #[serde(skip_serializing_if = "Option::is_none")]
349    pub handle: Option<String>,
350
351    /// Filter to collections containing this product.
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub product_id: Option<u64>,
354
355    /// Show collections updated after this date.
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub updated_at_min: Option<DateTime<Utc>>,
358
359    /// Show collections updated before this date.
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub updated_at_max: Option<DateTime<Utc>>,
362
363    /// Filter by published status.
364    /// Valid values: "published", "unpublished", "any".
365    #[serde(skip_serializing_if = "Option::is_none")]
366    pub published_status: Option<String>,
367
368    /// Comma-separated list of fields to include in the response.
369    #[serde(skip_serializing_if = "Option::is_none")]
370    pub fields: Option<String>,
371
372    /// Cursor for pagination.
373    #[serde(skip_serializing_if = "Option::is_none")]
374    pub page_info: Option<String>,
375}
376
377/// Parameters for counting smart collections.
378#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
379pub struct SmartCollectionCountParams {
380    /// Filter by collection title.
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub title: Option<String>,
383
384    /// Filter to collections containing this product.
385    #[serde(skip_serializing_if = "Option::is_none")]
386    pub product_id: Option<u64>,
387
388    /// Show collections updated after this date.
389    #[serde(skip_serializing_if = "Option::is_none")]
390    pub updated_at_min: Option<DateTime<Utc>>,
391
392    /// Show collections updated before this date.
393    #[serde(skip_serializing_if = "Option::is_none")]
394    pub updated_at_max: Option<DateTime<Utc>>,
395
396    /// Filter by published status.
397    /// Valid values: "published", "unpublished", "any".
398    #[serde(skip_serializing_if = "Option::is_none")]
399    pub published_status: Option<String>,
400}
401
402#[cfg(test)]
403mod tests {
404    use super::*;
405    use crate::rest::{get_path, ResourceOperation};
406
407    #[test]
408    fn test_smart_collection_struct_serialization() {
409        let collection = SmartCollection {
410            id: Some(1063001322),
411            title: Some("Nike Products".to_string()),
412            body_html: Some("<p>All Nike products</p>".to_string()),
413            handle: Some("nike-products".to_string()),
414            published_at: None,
415            published_scope: Some("web".to_string()),
416            sort_order: Some("best-selling".to_string()),
417            template_suffix: None,
418            image: None,
419            rules: Some(vec![SmartCollectionRule {
420                column: "vendor".to_string(),
421                relation: "equals".to_string(),
422                condition: "Nike".to_string(),
423            }]),
424            disjunctive: Some(false),
425            created_at: None,
426            updated_at: None,
427            admin_graphql_api_id: Some("gid://shopify/Collection/1063001322".to_string()),
428            published: Some(true),
429        };
430
431        let json = serde_json::to_string(&collection).unwrap();
432        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
433
434        // Writable fields should be present
435        assert_eq!(parsed["title"], "Nike Products");
436        assert_eq!(parsed["body_html"], "<p>All Nike products</p>");
437        assert_eq!(parsed["published_scope"], "web");
438        assert_eq!(parsed["sort_order"], "best-selling");
439        assert_eq!(parsed["disjunctive"], false);
440        assert!(parsed.get("rules").is_some());
441
442        // Check rules array
443        let rules = &parsed["rules"];
444        assert_eq!(rules[0]["column"], "vendor");
445        assert_eq!(rules[0]["relation"], "equals");
446        assert_eq!(rules[0]["condition"], "Nike");
447
448        // Read-only fields should be omitted
449        assert!(parsed.get("id").is_none());
450        assert!(parsed.get("handle").is_none());
451        assert!(parsed.get("created_at").is_none());
452        assert!(parsed.get("updated_at").is_none());
453        assert!(parsed.get("admin_graphql_api_id").is_none());
454        assert!(parsed.get("published").is_none());
455    }
456
457    #[test]
458    fn test_smart_collection_deserialization_with_rules() {
459        let json = r#"{
460            "id": 1063001322,
461            "handle": "nike-sale",
462            "title": "Nike Sale",
463            "updated_at": "2024-01-02T09:28:43-05:00",
464            "body_html": "<p>Nike products on sale</p>",
465            "published_at": "2024-01-01T19:00:00-05:00",
466            "sort_order": "price-asc",
467            "template_suffix": null,
468            "published_scope": "global",
469            "disjunctive": true,
470            "rules": [
471                {
472                    "column": "vendor",
473                    "relation": "equals",
474                    "condition": "Nike"
475                },
476                {
477                    "column": "tag",
478                    "relation": "equals",
479                    "condition": "sale"
480                }
481            ],
482            "admin_graphql_api_id": "gid://shopify/Collection/1063001322"
483        }"#;
484
485        let collection: SmartCollection = serde_json::from_str(json).unwrap();
486
487        assert_eq!(collection.id, Some(1063001322));
488        assert_eq!(collection.handle.as_deref(), Some("nike-sale"));
489        assert_eq!(collection.title.as_deref(), Some("Nike Sale"));
490        assert_eq!(
491            collection.body_html.as_deref(),
492            Some("<p>Nike products on sale</p>")
493        );
494        assert_eq!(collection.sort_order.as_deref(), Some("price-asc"));
495        assert_eq!(collection.published_scope.as_deref(), Some("global"));
496        assert_eq!(collection.disjunctive, Some(true));
497        assert!(collection.published_at.is_some());
498        assert!(collection.updated_at.is_some());
499
500        // Check rules
501        let rules = collection.rules.unwrap();
502        assert_eq!(rules.len(), 2);
503
504        assert_eq!(rules[0].column, "vendor");
505        assert_eq!(rules[0].relation, "equals");
506        assert_eq!(rules[0].condition, "Nike");
507
508        assert_eq!(rules[1].column, "tag");
509        assert_eq!(rules[1].relation, "equals");
510        assert_eq!(rules[1].condition, "sale");
511    }
512
513    #[test]
514    fn test_smart_collection_rule_struct() {
515        // Test serialization
516        let rule = SmartCollectionRule {
517            column: "variant_price".to_string(),
518            relation: "greater_than".to_string(),
519            condition: "50".to_string(),
520        };
521
522        let json = serde_json::to_string(&rule).unwrap();
523        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
524
525        assert_eq!(parsed["column"], "variant_price");
526        assert_eq!(parsed["relation"], "greater_than");
527        assert_eq!(parsed["condition"], "50");
528
529        // Test deserialization
530        let json_str = r#"{"column":"title","relation":"contains","condition":"summer"}"#;
531        let rule: SmartCollectionRule = serde_json::from_str(json_str).unwrap();
532
533        assert_eq!(rule.column, "title");
534        assert_eq!(rule.relation, "contains");
535        assert_eq!(rule.condition, "summer");
536    }
537
538    #[test]
539    fn test_smart_collection_path_constants_are_correct() {
540        // Test Find path
541        let find_path = get_path(SmartCollection::PATHS, ResourceOperation::Find, &["id"]);
542        assert!(find_path.is_some());
543        assert_eq!(find_path.unwrap().template, "smart_collections/{id}");
544        assert_eq!(find_path.unwrap().http_method, HttpMethod::Get);
545
546        // Test All path
547        let all_path = get_path(SmartCollection::PATHS, ResourceOperation::All, &[]);
548        assert!(all_path.is_some());
549        assert_eq!(all_path.unwrap().template, "smart_collections");
550
551        // Test Count path
552        let count_path = get_path(SmartCollection::PATHS, ResourceOperation::Count, &[]);
553        assert!(count_path.is_some());
554        assert_eq!(count_path.unwrap().template, "smart_collections/count");
555
556        // Test Create path
557        let create_path = get_path(SmartCollection::PATHS, ResourceOperation::Create, &[]);
558        assert!(create_path.is_some());
559        assert_eq!(create_path.unwrap().http_method, HttpMethod::Post);
560
561        // Test Update path
562        let update_path = get_path(SmartCollection::PATHS, ResourceOperation::Update, &["id"]);
563        assert!(update_path.is_some());
564        assert_eq!(update_path.unwrap().http_method, HttpMethod::Put);
565
566        // Test Delete path
567        let delete_path = get_path(SmartCollection::PATHS, ResourceOperation::Delete, &["id"]);
568        assert!(delete_path.is_some());
569        assert_eq!(delete_path.unwrap().http_method, HttpMethod::Delete);
570
571        // Verify constants
572        assert_eq!(SmartCollection::NAME, "SmartCollection");
573        assert_eq!(SmartCollection::PLURAL, "smart_collections");
574    }
575
576    #[test]
577    fn test_smart_collection_get_id_returns_correct_value() {
578        let collection_with_id = SmartCollection {
579            id: Some(1063001322),
580            title: Some("Test Collection".to_string()),
581            ..Default::default()
582        };
583        assert_eq!(collection_with_id.get_id(), Some(1063001322));
584
585        let collection_without_id = SmartCollection {
586            id: None,
587            title: Some("New Collection".to_string()),
588            ..Default::default()
589        };
590        assert_eq!(collection_without_id.get_id(), None);
591    }
592
593    #[test]
594    fn test_smart_collection_list_params_serialization() {
595        let params = SmartCollectionListParams {
596            ids: Some(vec![123, 456, 789]),
597            limit: Some(50),
598            since_id: Some(100),
599            title: Some("Summer".to_string()),
600            handle: Some("summer-sale".to_string()),
601            product_id: Some(999),
602            published_status: Some("published".to_string()),
603            ..Default::default()
604        };
605
606        let json = serde_json::to_value(&params).unwrap();
607
608        assert_eq!(json["ids"], serde_json::json!([123, 456, 789]));
609        assert_eq!(json["limit"], 50);
610        assert_eq!(json["since_id"], 100);
611        assert_eq!(json["title"], "Summer");
612        assert_eq!(json["handle"], "summer-sale");
613        assert_eq!(json["product_id"], 999);
614        assert_eq!(json["published_status"], "published");
615
616        // Test empty params
617        let empty_params = SmartCollectionListParams::default();
618        let empty_json = serde_json::to_value(&empty_params).unwrap();
619        assert_eq!(empty_json, serde_json::json!({}));
620    }
621
622    #[test]
623    fn test_sort_order_and_order_method_signature() {
624        // Test sort_order field with "manual" value
625        let collection = SmartCollection {
626            id: Some(123),
627            title: Some("Manual Sort Collection".to_string()),
628            sort_order: Some("manual".to_string()),
629            ..Default::default()
630        };
631
632        assert_eq!(collection.sort_order.as_deref(), Some("manual"));
633        assert!(collection.get_id().is_some());
634
635        // Verify the order method signature exists by referencing it
636        fn _assert_order_signature<F, Fut>(f: F)
637        where
638            F: Fn(&SmartCollection, &RestClient, Vec<u64>) -> Fut,
639            Fut: std::future::Future<Output = Result<(), ResourceError>>,
640        {
641            let _ = f;
642        }
643
644        // This test verifies the method exists and has the correct signature.
645        // The actual HTTP call would require a mock client.
646    }
647
648    #[test]
649    fn test_disjunctive_field_logic() {
650        // Test disjunctive = true (OR logic)
651        let or_collection = SmartCollection {
652            title: Some("OR Logic Collection".to_string()),
653            disjunctive: Some(true),
654            rules: Some(vec![
655                SmartCollectionRule {
656                    column: "tag".to_string(),
657                    relation: "equals".to_string(),
658                    condition: "summer".to_string(),
659                },
660                SmartCollectionRule {
661                    column: "tag".to_string(),
662                    relation: "equals".to_string(),
663                    condition: "winter".to_string(),
664                },
665            ]),
666            ..Default::default()
667        };
668
669        let json = serde_json::to_string(&or_collection).unwrap();
670        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
671        assert_eq!(parsed["disjunctive"], true);
672
673        // Test disjunctive = false (AND logic)
674        let and_collection = SmartCollection {
675            title: Some("AND Logic Collection".to_string()),
676            disjunctive: Some(false),
677            rules: Some(vec![
678                SmartCollectionRule {
679                    column: "vendor".to_string(),
680                    relation: "equals".to_string(),
681                    condition: "Nike".to_string(),
682                },
683                SmartCollectionRule {
684                    column: "variant_price".to_string(),
685                    relation: "greater_than".to_string(),
686                    condition: "100".to_string(),
687                },
688            ]),
689            ..Default::default()
690        };
691
692        let json = serde_json::to_string(&and_collection).unwrap();
693        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
694        assert_eq!(parsed["disjunctive"], false);
695    }
696
697    #[test]
698    fn test_smart_collection_count_params_serialization() {
699        let params = SmartCollectionCountParams {
700            title: Some("Summer".to_string()),
701            product_id: Some(12345),
702            published_status: Some("published".to_string()),
703            ..Default::default()
704        };
705
706        let json = serde_json::to_value(&params).unwrap();
707
708        assert_eq!(json["title"], "Summer");
709        assert_eq!(json["product_id"], 12345);
710        assert_eq!(json["published_status"], "published");
711
712        let empty_params = SmartCollectionCountParams::default();
713        let empty_json = serde_json::to_value(&empty_params).unwrap();
714        assert_eq!(empty_json, serde_json::json!({}));
715    }
716
717    #[test]
718    fn test_smart_collection_with_image() {
719        let collection = SmartCollection {
720            title: Some("Image Test".to_string()),
721            image: Some(CollectionImage {
722                src: Some("https://example.com/collection.jpg".to_string()),
723                alt: Some("Collection banner".to_string()),
724                width: Some(1200),
725                height: Some(400),
726                created_at: None,
727            }),
728            ..Default::default()
729        };
730
731        let json = serde_json::to_string(&collection).unwrap();
732        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
733
734        let image = &parsed["image"];
735        assert_eq!(image["src"], "https://example.com/collection.jpg");
736        assert_eq!(image["alt"], "Collection banner");
737        assert_eq!(image["width"], 1200);
738        assert_eq!(image["height"], 400);
739    }
740}