use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::rest::{ResourceOperation, ResourcePath, RestResource};
use crate::HttpMethod;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum WeightUnit {
#[default]
Kg,
G,
Lb,
Oz,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct Variant {
#[serde(skip_serializing)]
pub id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub product_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub price: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub compare_at_price: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sku: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub barcode: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub position: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub grams: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub weight: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub weight_unit: Option<WeightUnit>,
#[serde(skip_serializing)]
pub inventory_item_id: Option<u64>,
#[serde(skip_serializing)]
pub inventory_quantity: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inventory_management: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inventory_policy: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fulfillment_service: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub option1: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub option2: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub option3: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub taxable: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tax_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub requires_shipping: Option<bool>,
#[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>,
}
impl RestResource for Variant {
type Id = u64;
type FindParams = VariantFindParams;
type AllParams = VariantListParams;
type CountParams = VariantCountParams;
const NAME: &'static str = "Variant";
const PLURAL: &'static str = "variants";
const PATHS: &'static [ResourcePath] = &[
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::Find,
&["product_id", "id"],
"products/{product_id}/variants/{id}",
),
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::All,
&["product_id"],
"products/{product_id}/variants",
),
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::Count,
&["product_id"],
"products/{product_id}/variants/count",
),
ResourcePath::new(
HttpMethod::Post,
ResourceOperation::Create,
&["product_id"],
"products/{product_id}/variants",
),
ResourcePath::new(
HttpMethod::Put,
ResourceOperation::Update,
&["product_id", "id"],
"products/{product_id}/variants/{id}",
),
ResourcePath::new(
HttpMethod::Delete,
ResourceOperation::Delete,
&["product_id", "id"],
"products/{product_id}/variants/{id}",
),
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::Find,
&["id"],
"variants/{id}",
),
ResourcePath::new(
HttpMethod::Put,
ResourceOperation::Update,
&["id"],
"variants/{id}",
),
];
fn get_id(&self) -> Option<Self::Id> {
self.id
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct VariantFindParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct VariantListParams {
#[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 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 VariantCountParams {
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rest::{get_path, ResourceOperation};
#[test]
fn test_variant_struct_serialization() {
let variant = Variant {
id: Some(12345), product_id: Some(67890), title: Some("Large / Blue".to_string()),
price: Some("29.99".to_string()),
compare_at_price: Some("39.99".to_string()),
sku: Some("PROD-LG-BL".to_string()),
barcode: Some("1234567890123".to_string()),
position: Some(2),
grams: Some(500),
weight: Some(0.5),
weight_unit: Some(WeightUnit::Kg),
inventory_item_id: Some(111222), inventory_quantity: Some(100), inventory_management: Some("shopify".to_string()),
inventory_policy: Some("deny".to_string()),
fulfillment_service: Some("manual".to_string()),
option1: Some("Large".to_string()),
option2: Some("Blue".to_string()),
option3: None,
image_id: Some(999888),
taxable: Some(true),
tax_code: None,
requires_shipping: Some(true),
created_at: Some(
DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
.unwrap()
.with_timezone(&Utc),
), updated_at: Some(
DateTime::parse_from_rfc3339("2024-06-20T15:45:00Z")
.unwrap()
.with_timezone(&Utc),
), admin_graphql_api_id: Some("gid://shopify/ProductVariant/12345".to_string()), };
let json = serde_json::to_string(&variant).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["product_id"], 67890);
assert_eq!(parsed["title"], "Large / Blue");
assert_eq!(parsed["price"], "29.99");
assert_eq!(parsed["compare_at_price"], "39.99");
assert_eq!(parsed["sku"], "PROD-LG-BL");
assert_eq!(parsed["barcode"], "1234567890123");
assert_eq!(parsed["position"], 2);
assert_eq!(parsed["grams"], 500);
assert_eq!(parsed["weight"], 0.5);
assert_eq!(parsed["weight_unit"], "kg");
assert_eq!(parsed["inventory_management"], "shopify");
assert_eq!(parsed["inventory_policy"], "deny");
assert_eq!(parsed["fulfillment_service"], "manual");
assert_eq!(parsed["option1"], "Large");
assert_eq!(parsed["option2"], "Blue");
assert_eq!(parsed["image_id"], 999888);
assert_eq!(parsed["taxable"], true);
assert_eq!(parsed["requires_shipping"], true);
assert!(parsed.get("id").is_none());
assert!(parsed.get("inventory_item_id").is_none());
assert!(parsed.get("inventory_quantity").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("option3").is_none());
assert!(parsed.get("tax_code").is_none());
}
#[test]
fn test_variant_deserialization_from_api_response() {
let json = r#"{
"id": 39072856,
"product_id": 788032119674292922,
"title": "Large / Blue",
"price": "29.99",
"compare_at_price": "39.99",
"sku": "PROD-LG-BL",
"barcode": "1234567890123",
"position": 2,
"grams": 500,
"weight": 0.5,
"weight_unit": "kg",
"inventory_item_id": 111222333,
"inventory_quantity": 100,
"inventory_management": "shopify",
"inventory_policy": "deny",
"fulfillment_service": "manual",
"option1": "Large",
"option2": "Blue",
"option3": null,
"image_id": 999888777,
"taxable": true,
"tax_code": null,
"requires_shipping": true,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-06-20T15:45:00Z",
"admin_graphql_api_id": "gid://shopify/ProductVariant/39072856"
}"#;
let variant: Variant = serde_json::from_str(json).unwrap();
assert_eq!(variant.id, Some(39072856));
assert_eq!(variant.product_id, Some(788032119674292922));
assert_eq!(variant.title, Some("Large / Blue".to_string()));
assert_eq!(variant.price, Some("29.99".to_string()));
assert_eq!(variant.compare_at_price, Some("39.99".to_string()));
assert_eq!(variant.sku, Some("PROD-LG-BL".to_string()));
assert_eq!(variant.barcode, Some("1234567890123".to_string()));
assert_eq!(variant.position, Some(2));
assert_eq!(variant.grams, Some(500));
assert_eq!(variant.weight, Some(0.5));
assert_eq!(variant.weight_unit, Some(WeightUnit::Kg));
assert_eq!(variant.inventory_item_id, Some(111222333));
assert_eq!(variant.inventory_quantity, Some(100));
assert_eq!(variant.inventory_management, Some("shopify".to_string()));
assert_eq!(variant.inventory_policy, Some("deny".to_string()));
assert_eq!(variant.fulfillment_service, Some("manual".to_string()));
assert_eq!(variant.option1, Some("Large".to_string()));
assert_eq!(variant.option2, Some("Blue".to_string()));
assert_eq!(variant.option3, None);
assert_eq!(variant.image_id, Some(999888777));
assert_eq!(variant.taxable, Some(true));
assert_eq!(variant.tax_code, None);
assert_eq!(variant.requires_shipping, Some(true));
assert!(variant.created_at.is_some());
assert!(variant.updated_at.is_some());
assert_eq!(
variant.admin_graphql_api_id,
Some("gid://shopify/ProductVariant/39072856".to_string())
);
}
#[test]
fn test_dual_path_patterns() {
let nested_find_path = get_path(
Variant::PATHS,
ResourceOperation::Find,
&["product_id", "id"],
);
assert!(nested_find_path.is_some());
assert_eq!(
nested_find_path.unwrap().template,
"products/{product_id}/variants/{id}"
);
let standalone_find_path = get_path(Variant::PATHS, ResourceOperation::Find, &["id"]);
assert!(standalone_find_path.is_some());
assert_eq!(standalone_find_path.unwrap().template, "variants/{id}");
let nested_all_path = get_path(Variant::PATHS, ResourceOperation::All, &["product_id"]);
assert!(nested_all_path.is_some());
assert_eq!(
nested_all_path.unwrap().template,
"products/{product_id}/variants"
);
let standalone_all_path = get_path(Variant::PATHS, ResourceOperation::All, &[]);
assert!(standalone_all_path.is_none());
let nested_update_path = get_path(
Variant::PATHS,
ResourceOperation::Update,
&["product_id", "id"],
);
assert!(nested_update_path.is_some());
assert_eq!(
nested_update_path.unwrap().template,
"products/{product_id}/variants/{id}"
);
let standalone_update_path = get_path(Variant::PATHS, ResourceOperation::Update, &["id"]);
assert!(standalone_update_path.is_some());
assert_eq!(standalone_update_path.unwrap().template, "variants/{id}");
let create_path = get_path(Variant::PATHS, ResourceOperation::Create, &["product_id"]);
assert!(create_path.is_some());
assert_eq!(
create_path.unwrap().template,
"products/{product_id}/variants"
);
let delete_path = get_path(
Variant::PATHS,
ResourceOperation::Delete,
&["product_id", "id"],
);
assert!(delete_path.is_some());
assert_eq!(
delete_path.unwrap().template,
"products/{product_id}/variants/{id}"
);
let count_path = get_path(Variant::PATHS, ResourceOperation::Count, &["product_id"]);
assert!(count_path.is_some());
assert_eq!(
count_path.unwrap().template,
"products/{product_id}/variants/count"
);
assert_eq!(Variant::NAME, "Variant");
assert_eq!(Variant::PLURAL, "variants");
}
#[test]
fn test_weight_unit_enum_serialization() {
assert_eq!(serde_json::to_string(&WeightUnit::Kg).unwrap(), "\"kg\"");
assert_eq!(serde_json::to_string(&WeightUnit::G).unwrap(), "\"g\"");
assert_eq!(serde_json::to_string(&WeightUnit::Lb).unwrap(), "\"lb\"");
assert_eq!(serde_json::to_string(&WeightUnit::Oz).unwrap(), "\"oz\"");
let kg: WeightUnit = serde_json::from_str("\"kg\"").unwrap();
let g: WeightUnit = serde_json::from_str("\"g\"").unwrap();
let lb: WeightUnit = serde_json::from_str("\"lb\"").unwrap();
let oz: WeightUnit = serde_json::from_str("\"oz\"").unwrap();
assert_eq!(kg, WeightUnit::Kg);
assert_eq!(g, WeightUnit::G);
assert_eq!(lb, WeightUnit::Lb);
assert_eq!(oz, WeightUnit::Oz);
assert_eq!(WeightUnit::default(), WeightUnit::Kg);
}
#[test]
fn test_variant_list_params_serialization() {
let params = VariantListParams {
limit: Some(50),
since_id: Some(12345),
fields: Some("id,title,price,sku".to_string()),
page_info: Some("eyJsYXN0X2lkIjoxMjM0NTY3ODkwfQ".to_string()),
};
let json = serde_json::to_value(¶ms).unwrap();
assert_eq!(json["limit"], 50);
assert_eq!(json["since_id"], 12345);
assert_eq!(json["fields"], "id,title,price,sku");
assert_eq!(json["page_info"], "eyJsYXN0X2lkIjoxMjM0NTY3ODkwfQ");
let empty_params = VariantListParams::default();
let empty_json = serde_json::to_value(&empty_params).unwrap();
assert_eq!(empty_json, serde_json::json!({}));
}
#[test]
fn test_variant_get_id_returns_correct_value() {
let variant_with_id = Variant {
id: Some(123456789),
product_id: Some(987654321),
title: Some("Test Variant".to_string()),
..Default::default()
};
assert_eq!(variant_with_id.get_id(), Some(123456789));
let variant_without_id = Variant {
id: None,
product_id: Some(987654321),
title: Some("New Variant".to_string()),
..Default::default()
};
assert_eq!(variant_without_id.get_id(), None);
}
}