use std::collections::HashMap;
use serde::Serialize;
use crate::clients::RestClient;
use crate::rest::{ResourceError, ResourceResponse};
use super::custom_collection::CustomCollection;
use super::product::{Product, ProductListParams};
use super::smart_collection::SmartCollection;
#[allow(async_fn_in_trait)]
pub trait Collection: Send + Sync {
fn get_collection_id(&self) -> Option<u64>;
async fn products(
&self,
client: &RestClient,
params: Option<ProductListParams>,
) -> Result<ResourceResponse<Vec<Product>>, ResourceError>;
async fn product_count(&self, client: &RestClient) -> Result<u64, ResourceError>;
}
impl Collection for CustomCollection {
fn get_collection_id(&self) -> Option<u64> {
self.id
}
async fn products(
&self,
client: &RestClient,
params: Option<ProductListParams>,
) -> Result<ResourceResponse<Vec<Product>>, ResourceError> {
let id = self
.get_collection_id()
.ok_or(ResourceError::PathResolutionFailed {
resource: "CustomCollection",
operation: "products",
})?;
fetch_collection_products(client, id, params).await
}
async fn product_count(&self, client: &RestClient) -> Result<u64, ResourceError> {
let id = self
.get_collection_id()
.ok_or(ResourceError::PathResolutionFailed {
resource: "CustomCollection",
operation: "product_count",
})?;
fetch_collection_product_count(client, id).await
}
}
impl Collection for SmartCollection {
fn get_collection_id(&self) -> Option<u64> {
self.id
}
async fn products(
&self,
client: &RestClient,
params: Option<ProductListParams>,
) -> Result<ResourceResponse<Vec<Product>>, ResourceError> {
let id = self
.get_collection_id()
.ok_or(ResourceError::PathResolutionFailed {
resource: "SmartCollection",
operation: "products",
})?;
fetch_collection_products(client, id, params).await
}
async fn product_count(&self, client: &RestClient) -> Result<u64, ResourceError> {
let id = self
.get_collection_id()
.ok_or(ResourceError::PathResolutionFailed {
resource: "SmartCollection",
operation: "product_count",
})?;
fetch_collection_product_count(client, id).await
}
}
async fn fetch_collection_products(
client: &RestClient,
collection_id: u64,
params: Option<ProductListParams>,
) -> Result<ResourceResponse<Vec<Product>>, ResourceError> {
let path = format!("collections/{collection_id}/products");
let query = params
.map(|p| serialize_to_query(&p))
.transpose()?
.filter(|q| !q.is_empty());
let response = client.get(&path, query).await?;
if !response.is_ok() {
return Err(ResourceError::from_http_response(
response.code,
&response.body,
"Collection",
Some(&collection_id.to_string()),
response.request_id(),
));
}
ResourceResponse::from_http_response(response, "products")
}
async fn fetch_collection_product_count(
client: &RestClient,
collection_id: u64,
) -> Result<u64, ResourceError> {
let path = format!("collections/{collection_id}/products/count");
let response = client.get(&path, None).await?;
if !response.is_ok() {
return Err(ResourceError::from_http_response(
response.code,
&response.body,
"Collection",
Some(&collection_id.to_string()),
response.request_id(),
));
}
let count = response
.body
.get("count")
.and_then(serde_json::Value::as_u64)
.ok_or_else(|| {
ResourceError::Http(crate::clients::HttpError::Response(
crate::clients::HttpResponseError {
code: response.code,
message: "Missing 'count' in response".to_string(),
error_reference: response.request_id().map(ToString::to_string),
},
))
})?;
Ok(count)
}
fn serialize_to_query<T: Serialize>(params: &T) -> Result<HashMap<String, String>, ResourceError> {
let value = serde_json::to_value(params).map_err(|e| {
ResourceError::Http(crate::clients::HttpError::Response(
crate::clients::HttpResponseError {
code: 400,
message: format!("Failed to serialize params: {e}"),
error_reference: None,
},
))
})?;
let mut query = HashMap::new();
if let serde_json::Value::Object(map) = value {
for (key, val) in map {
match val {
serde_json::Value::Null => {}
serde_json::Value::String(s) => {
query.insert(key, s);
}
serde_json::Value::Number(n) => {
query.insert(key, n.to_string());
}
serde_json::Value::Bool(b) => {
query.insert(key, b.to_string());
}
serde_json::Value::Array(arr) => {
let values: Vec<String> = arr
.iter()
.filter_map(|v| match v {
serde_json::Value::String(s) => Some(s.clone()),
serde_json::Value::Number(n) => Some(n.to_string()),
_ => None,
})
.collect();
if !values.is_empty() {
query.insert(key, values.join(","));
}
}
serde_json::Value::Object(_) => {
query.insert(key, val.to_string());
}
}
}
}
Ok(query)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_collection_trait_get_collection_id_custom() {
let collection = CustomCollection {
id: Some(841564295),
title: Some("Test Collection".to_string()),
..Default::default()
};
assert_eq!(collection.get_collection_id(), Some(841564295));
let new_collection = CustomCollection {
id: None,
title: Some("New Collection".to_string()),
..Default::default()
};
assert_eq!(new_collection.get_collection_id(), None);
}
#[test]
fn test_collection_trait_get_collection_id_smart() {
let collection = SmartCollection {
id: Some(1063001322),
title: Some("Test Collection".to_string()),
..Default::default()
};
assert_eq!(collection.get_collection_id(), Some(1063001322));
let new_collection = SmartCollection {
id: None,
title: Some("New Collection".to_string()),
..Default::default()
};
assert_eq!(new_collection.get_collection_id(), None);
}
#[test]
fn test_collection_trait_products_method_signature_custom() {
fn _assert_products_signature<F, Fut>(f: F)
where
F: Fn(&CustomCollection, &RestClient, Option<ProductListParams>) -> Fut,
Fut:
std::future::Future<Output = Result<ResourceResponse<Vec<Product>>, ResourceError>>,
{
let _ = f;
}
let collection = CustomCollection {
id: None,
..Default::default()
};
assert!(collection.get_collection_id().is_none());
}
#[test]
fn test_collection_trait_products_method_signature_smart() {
fn _assert_products_signature<F, Fut>(f: F)
where
F: Fn(&SmartCollection, &RestClient, Option<ProductListParams>) -> Fut,
Fut:
std::future::Future<Output = Result<ResourceResponse<Vec<Product>>, ResourceError>>,
{
let _ = f;
}
let collection = SmartCollection {
id: None,
..Default::default()
};
assert!(collection.get_collection_id().is_none());
}
#[test]
fn test_collection_trait_product_count_method_signature_custom() {
fn _assert_product_count_signature<F, Fut>(f: F)
where
F: Fn(&CustomCollection, &RestClient) -> Fut,
Fut: std::future::Future<Output = Result<u64, ResourceError>>,
{
let _ = f;
}
}
#[test]
fn test_collection_trait_product_count_method_signature_smart() {
fn _assert_product_count_signature<F, Fut>(f: F)
where
F: Fn(&SmartCollection, &RestClient) -> Fut,
Fut: std::future::Future<Output = Result<u64, ResourceError>>,
{
let _ = f;
}
}
#[test]
fn test_collection_trait_polymorphism() {
fn _takes_collection<C: Collection>(_collection: &C) {}
let custom = CustomCollection {
id: Some(123),
..Default::default()
};
_takes_collection(&custom);
let smart = SmartCollection {
id: Some(456),
..Default::default()
};
_takes_collection(&smart);
}
#[test]
fn test_serialize_to_query_helper() {
let params = ProductListParams {
limit: Some(50),
title: Some("Test".to_string()),
..Default::default()
};
let query = serialize_to_query(¶ms).unwrap();
assert_eq!(query.get("limit"), Some(&"50".to_string()));
assert_eq!(query.get("title"), Some(&"Test".to_string()));
}
#[test]
fn test_collection_trait_bounds() {
fn assert_trait_bounds<T: Collection + Send + Sync>() {}
assert_trait_bounds::<CustomCollection>();
assert_trait_bounds::<SmartCollection>();
}
}