1use 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#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
116pub struct SmartCollection {
117 #[serde(skip_serializing)]
120 pub id: Option<u64>,
121
122 #[serde(skip_serializing)]
125 pub handle: Option<String>,
126
127 #[serde(skip_serializing)]
129 pub created_at: Option<DateTime<Utc>>,
130
131 #[serde(skip_serializing)]
133 pub updated_at: Option<DateTime<Utc>>,
134
135 #[serde(skip_serializing)]
137 pub admin_graphql_api_id: Option<String>,
138
139 #[serde(skip_serializing_if = "Option::is_none")]
142 pub title: Option<String>,
143
144 #[serde(skip_serializing_if = "Option::is_none")]
146 pub body_html: Option<String>,
147
148 #[serde(skip_serializing_if = "Option::is_none")]
151 pub published_at: Option<DateTime<Utc>>,
152
153 #[serde(skip_serializing_if = "Option::is_none")]
156 pub published_scope: Option<String>,
157
158 #[serde(skip_serializing_if = "Option::is_none")]
170 pub sort_order: Option<String>,
171
172 #[serde(skip_serializing_if = "Option::is_none")]
174 pub template_suffix: Option<String>,
175
176 #[serde(skip_serializing_if = "Option::is_none")]
178 pub image: Option<CollectionImage>,
179
180 #[serde(skip_serializing_if = "Option::is_none")]
184 pub rules: Option<Vec<SmartCollectionRule>>,
185
186 #[serde(skip_serializing_if = "Option::is_none")]
191 pub disjunctive: Option<bool>,
192
193 #[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 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 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#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
322pub struct SmartCollectionFindParams {
323 #[serde(skip_serializing_if = "Option::is_none")]
325 pub fields: Option<String>,
326}
327
328#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
330pub struct SmartCollectionListParams {
331 #[serde(skip_serializing_if = "Option::is_none")]
333 pub ids: Option<Vec<u64>>,
334
335 #[serde(skip_serializing_if = "Option::is_none")]
337 pub limit: Option<u32>,
338
339 #[serde(skip_serializing_if = "Option::is_none")]
341 pub since_id: Option<u64>,
342
343 #[serde(skip_serializing_if = "Option::is_none")]
345 pub title: Option<String>,
346
347 #[serde(skip_serializing_if = "Option::is_none")]
349 pub handle: Option<String>,
350
351 #[serde(skip_serializing_if = "Option::is_none")]
353 pub product_id: Option<u64>,
354
355 #[serde(skip_serializing_if = "Option::is_none")]
357 pub updated_at_min: Option<DateTime<Utc>>,
358
359 #[serde(skip_serializing_if = "Option::is_none")]
361 pub updated_at_max: Option<DateTime<Utc>>,
362
363 #[serde(skip_serializing_if = "Option::is_none")]
366 pub published_status: Option<String>,
367
368 #[serde(skip_serializing_if = "Option::is_none")]
370 pub fields: Option<String>,
371
372 #[serde(skip_serializing_if = "Option::is_none")]
374 pub page_info: Option<String>,
375}
376
377#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
379pub struct SmartCollectionCountParams {
380 #[serde(skip_serializing_if = "Option::is_none")]
382 pub title: Option<String>,
383
384 #[serde(skip_serializing_if = "Option::is_none")]
386 pub product_id: Option<u64>,
387
388 #[serde(skip_serializing_if = "Option::is_none")]
390 pub updated_at_min: Option<DateTime<Utc>>,
391
392 #[serde(skip_serializing_if = "Option::is_none")]
394 pub updated_at_max: Option<DateTime<Utc>>,
395
396 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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(¶ms).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 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 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 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 }
647
648 #[test]
649 fn test_disjunctive_field_logic() {
650 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 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(¶ms).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}