use std::collections::HashMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::clients::RestClient;
use crate::rest::{ResourceError, ResourceOperation, ResourcePath, RestResource};
use crate::HttpMethod;
use super::common::{CollectionImage, SmartCollectionRule};
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct SmartCollection {
#[serde(skip_serializing)]
pub id: Option<u64>,
#[serde(skip_serializing)]
pub handle: Option<String>,
#[serde(skip_serializing)]
pub created_at: Option<DateTime<Utc>>,
#[serde(skip_serializing)]
pub updated_at: Option<DateTime<Utc>>,
#[serde(skip_serializing)]
pub admin_graphql_api_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub body_html: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub published_at: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub published_scope: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sort_order: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub template_suffix: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image: Option<CollectionImage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rules: Option<Vec<SmartCollectionRule>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub disjunctive: Option<bool>,
#[serde(skip_serializing)]
pub published: Option<bool>,
}
impl RestResource for SmartCollection {
type Id = u64;
type FindParams = SmartCollectionFindParams;
type AllParams = SmartCollectionListParams;
type CountParams = SmartCollectionCountParams;
const NAME: &'static str = "SmartCollection";
const PLURAL: &'static str = "smart_collections";
const PATHS: &'static [ResourcePath] = &[
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::Find,
&["id"],
"smart_collections/{id}",
),
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::All,
&[],
"smart_collections",
),
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::Count,
&[],
"smart_collections/count",
),
ResourcePath::new(
HttpMethod::Post,
ResourceOperation::Create,
&[],
"smart_collections",
),
ResourcePath::new(
HttpMethod::Put,
ResourceOperation::Update,
&["id"],
"smart_collections/{id}",
),
ResourcePath::new(
HttpMethod::Delete,
ResourceOperation::Delete,
&["id"],
"smart_collections/{id}",
),
];
fn get_id(&self) -> Option<Self::Id> {
self.id
}
}
impl SmartCollection {
pub async fn order(
&self,
client: &RestClient,
product_ids: Vec<u64>,
) -> Result<(), ResourceError> {
let id = self.get_id().ok_or(ResourceError::PathResolutionFailed {
resource: Self::NAME,
operation: "order",
})?;
let mut query: HashMap<String, String> = HashMap::new();
let products_param: String = product_ids
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(",");
query.insert("products[]".to_string(), products_param);
let path = format!("smart_collections/{id}/order");
let body = serde_json::json!({});
let response = client.put(&path, body, Some(query)).await?;
if !response.is_ok() {
return Err(ResourceError::from_http_response(
response.code,
&response.body,
Self::NAME,
Some(&id.to_string()),
response.request_id(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct SmartCollectionFindParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct SmartCollectionListParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub ids: Option<Vec<u64>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub since_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub handle: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub product_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at_min: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at_max: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub published_status: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub page_info: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct SmartCollectionCountParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub product_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at_min: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at_max: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub published_status: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rest::{get_path, ResourceOperation};
#[test]
fn test_smart_collection_struct_serialization() {
let collection = SmartCollection {
id: Some(1063001322),
title: Some("Nike Products".to_string()),
body_html: Some("<p>All Nike products</p>".to_string()),
handle: Some("nike-products".to_string()),
published_at: None,
published_scope: Some("web".to_string()),
sort_order: Some("best-selling".to_string()),
template_suffix: None,
image: None,
rules: Some(vec![SmartCollectionRule {
column: "vendor".to_string(),
relation: "equals".to_string(),
condition: "Nike".to_string(),
}]),
disjunctive: Some(false),
created_at: None,
updated_at: None,
admin_graphql_api_id: Some("gid://shopify/Collection/1063001322".to_string()),
published: Some(true),
};
let json = serde_json::to_string(&collection).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["title"], "Nike Products");
assert_eq!(parsed["body_html"], "<p>All Nike products</p>");
assert_eq!(parsed["published_scope"], "web");
assert_eq!(parsed["sort_order"], "best-selling");
assert_eq!(parsed["disjunctive"], false);
assert!(parsed.get("rules").is_some());
let rules = &parsed["rules"];
assert_eq!(rules[0]["column"], "vendor");
assert_eq!(rules[0]["relation"], "equals");
assert_eq!(rules[0]["condition"], "Nike");
assert!(parsed.get("id").is_none());
assert!(parsed.get("handle").is_none());
assert!(parsed.get("created_at").is_none());
assert!(parsed.get("updated_at").is_none());
assert!(parsed.get("admin_graphql_api_id").is_none());
assert!(parsed.get("published").is_none());
}
#[test]
fn test_smart_collection_deserialization_with_rules() {
let json = r#"{
"id": 1063001322,
"handle": "nike-sale",
"title": "Nike Sale",
"updated_at": "2024-01-02T09:28:43-05:00",
"body_html": "<p>Nike products on sale</p>",
"published_at": "2024-01-01T19:00:00-05:00",
"sort_order": "price-asc",
"template_suffix": null,
"published_scope": "global",
"disjunctive": true,
"rules": [
{
"column": "vendor",
"relation": "equals",
"condition": "Nike"
},
{
"column": "tag",
"relation": "equals",
"condition": "sale"
}
],
"admin_graphql_api_id": "gid://shopify/Collection/1063001322"
}"#;
let collection: SmartCollection = serde_json::from_str(json).unwrap();
assert_eq!(collection.id, Some(1063001322));
assert_eq!(collection.handle.as_deref(), Some("nike-sale"));
assert_eq!(collection.title.as_deref(), Some("Nike Sale"));
assert_eq!(
collection.body_html.as_deref(),
Some("<p>Nike products on sale</p>")
);
assert_eq!(collection.sort_order.as_deref(), Some("price-asc"));
assert_eq!(collection.published_scope.as_deref(), Some("global"));
assert_eq!(collection.disjunctive, Some(true));
assert!(collection.published_at.is_some());
assert!(collection.updated_at.is_some());
let rules = collection.rules.unwrap();
assert_eq!(rules.len(), 2);
assert_eq!(rules[0].column, "vendor");
assert_eq!(rules[0].relation, "equals");
assert_eq!(rules[0].condition, "Nike");
assert_eq!(rules[1].column, "tag");
assert_eq!(rules[1].relation, "equals");
assert_eq!(rules[1].condition, "sale");
}
#[test]
fn test_smart_collection_rule_struct() {
let rule = SmartCollectionRule {
column: "variant_price".to_string(),
relation: "greater_than".to_string(),
condition: "50".to_string(),
};
let json = serde_json::to_string(&rule).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["column"], "variant_price");
assert_eq!(parsed["relation"], "greater_than");
assert_eq!(parsed["condition"], "50");
let json_str = r#"{"column":"title","relation":"contains","condition":"summer"}"#;
let rule: SmartCollectionRule = serde_json::from_str(json_str).unwrap();
assert_eq!(rule.column, "title");
assert_eq!(rule.relation, "contains");
assert_eq!(rule.condition, "summer");
}
#[test]
fn test_smart_collection_path_constants_are_correct() {
let find_path = get_path(SmartCollection::PATHS, ResourceOperation::Find, &["id"]);
assert!(find_path.is_some());
assert_eq!(find_path.unwrap().template, "smart_collections/{id}");
assert_eq!(find_path.unwrap().http_method, HttpMethod::Get);
let all_path = get_path(SmartCollection::PATHS, ResourceOperation::All, &[]);
assert!(all_path.is_some());
assert_eq!(all_path.unwrap().template, "smart_collections");
let count_path = get_path(SmartCollection::PATHS, ResourceOperation::Count, &[]);
assert!(count_path.is_some());
assert_eq!(count_path.unwrap().template, "smart_collections/count");
let create_path = get_path(SmartCollection::PATHS, ResourceOperation::Create, &[]);
assert!(create_path.is_some());
assert_eq!(create_path.unwrap().http_method, HttpMethod::Post);
let update_path = get_path(SmartCollection::PATHS, ResourceOperation::Update, &["id"]);
assert!(update_path.is_some());
assert_eq!(update_path.unwrap().http_method, HttpMethod::Put);
let delete_path = get_path(SmartCollection::PATHS, ResourceOperation::Delete, &["id"]);
assert!(delete_path.is_some());
assert_eq!(delete_path.unwrap().http_method, HttpMethod::Delete);
assert_eq!(SmartCollection::NAME, "SmartCollection");
assert_eq!(SmartCollection::PLURAL, "smart_collections");
}
#[test]
fn test_smart_collection_get_id_returns_correct_value() {
let collection_with_id = SmartCollection {
id: Some(1063001322),
title: Some("Test Collection".to_string()),
..Default::default()
};
assert_eq!(collection_with_id.get_id(), Some(1063001322));
let collection_without_id = SmartCollection {
id: None,
title: Some("New Collection".to_string()),
..Default::default()
};
assert_eq!(collection_without_id.get_id(), None);
}
#[test]
fn test_smart_collection_list_params_serialization() {
let params = SmartCollectionListParams {
ids: Some(vec![123, 456, 789]),
limit: Some(50),
since_id: Some(100),
title: Some("Summer".to_string()),
handle: Some("summer-sale".to_string()),
product_id: Some(999),
published_status: Some("published".to_string()),
..Default::default()
};
let json = serde_json::to_value(¶ms).unwrap();
assert_eq!(json["ids"], serde_json::json!([123, 456, 789]));
assert_eq!(json["limit"], 50);
assert_eq!(json["since_id"], 100);
assert_eq!(json["title"], "Summer");
assert_eq!(json["handle"], "summer-sale");
assert_eq!(json["product_id"], 999);
assert_eq!(json["published_status"], "published");
let empty_params = SmartCollectionListParams::default();
let empty_json = serde_json::to_value(&empty_params).unwrap();
assert_eq!(empty_json, serde_json::json!({}));
}
#[test]
fn test_sort_order_and_order_method_signature() {
let collection = SmartCollection {
id: Some(123),
title: Some("Manual Sort Collection".to_string()),
sort_order: Some("manual".to_string()),
..Default::default()
};
assert_eq!(collection.sort_order.as_deref(), Some("manual"));
assert!(collection.get_id().is_some());
fn _assert_order_signature<F, Fut>(f: F)
where
F: Fn(&SmartCollection, &RestClient, Vec<u64>) -> Fut,
Fut: std::future::Future<Output = Result<(), ResourceError>>,
{
let _ = f;
}
}
#[test]
fn test_disjunctive_field_logic() {
let or_collection = SmartCollection {
title: Some("OR Logic Collection".to_string()),
disjunctive: Some(true),
rules: Some(vec![
SmartCollectionRule {
column: "tag".to_string(),
relation: "equals".to_string(),
condition: "summer".to_string(),
},
SmartCollectionRule {
column: "tag".to_string(),
relation: "equals".to_string(),
condition: "winter".to_string(),
},
]),
..Default::default()
};
let json = serde_json::to_string(&or_collection).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["disjunctive"], true);
let and_collection = SmartCollection {
title: Some("AND Logic Collection".to_string()),
disjunctive: Some(false),
rules: Some(vec![
SmartCollectionRule {
column: "vendor".to_string(),
relation: "equals".to_string(),
condition: "Nike".to_string(),
},
SmartCollectionRule {
column: "variant_price".to_string(),
relation: "greater_than".to_string(),
condition: "100".to_string(),
},
]),
..Default::default()
};
let json = serde_json::to_string(&and_collection).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["disjunctive"], false);
}
#[test]
fn test_smart_collection_count_params_serialization() {
let params = SmartCollectionCountParams {
title: Some("Summer".to_string()),
product_id: Some(12345),
published_status: Some("published".to_string()),
..Default::default()
};
let json = serde_json::to_value(¶ms).unwrap();
assert_eq!(json["title"], "Summer");
assert_eq!(json["product_id"], 12345);
assert_eq!(json["published_status"], "published");
let empty_params = SmartCollectionCountParams::default();
let empty_json = serde_json::to_value(&empty_params).unwrap();
assert_eq!(empty_json, serde_json::json!({}));
}
#[test]
fn test_smart_collection_with_image() {
let collection = SmartCollection {
title: Some("Image Test".to_string()),
image: Some(CollectionImage {
src: Some("https://example.com/collection.jpg".to_string()),
alt: Some("Collection banner".to_string()),
width: Some(1200),
height: Some(400),
created_at: None,
}),
..Default::default()
};
let json = serde_json::to_string(&collection).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let image = &parsed["image"];
assert_eq!(image["src"], "https://example.com/collection.jpg");
assert_eq!(image["alt"], "Collection banner");
assert_eq!(image["width"], 1200);
assert_eq!(image["height"], 400);
}
}