use std::collections::HashMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::clients::RestClient;
use crate::rest::{ReadOnlyResource, ResourceError, ResourceOperation, ResourcePath, RestResource};
use crate::HttpMethod;
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct Location {
#[serde(skip_serializing)]
pub id: Option<u64>,
#[serde(skip_serializing)]
pub name: Option<String>,
#[serde(skip_serializing)]
pub address1: Option<String>,
#[serde(skip_serializing)]
pub address2: Option<String>,
#[serde(skip_serializing)]
pub city: Option<String>,
#[serde(skip_serializing)]
pub province: Option<String>,
#[serde(skip_serializing)]
pub province_code: Option<String>,
#[serde(skip_serializing)]
pub country: Option<String>,
#[serde(skip_serializing)]
pub country_code: Option<String>,
#[serde(skip_serializing)]
pub localized_country_name: Option<String>,
#[serde(skip_serializing)]
pub localized_province_name: Option<String>,
#[serde(skip_serializing)]
pub zip: Option<String>,
#[serde(skip_serializing)]
pub phone: Option<String>,
#[serde(skip_serializing)]
pub active: Option<bool>,
#[serde(skip_serializing)]
pub legacy: 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 Location {
pub async fn inventory_levels(
&self,
client: &RestClient,
params: Option<LocationInventoryLevelsParams>,
) -> Result<Vec<super::InventoryLevel>, ResourceError> {
let id = self.get_id().ok_or(ResourceError::PathResolutionFailed {
resource: Self::NAME,
operation: "inventory_levels",
})?;
let path = format!("locations/{id}/inventory_levels");
let query = params
.map(|p| {
let value = serde_json::to_value(&p).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::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());
}
_ => {}
}
}
}
Ok::<_, ResourceError>(query)
})
.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,
Self::NAME,
Some(&id.to_string()),
response.request_id(),
));
}
let levels: Vec<super::InventoryLevel> = response
.body
.get("inventory_levels")
.ok_or_else(|| {
ResourceError::Http(crate::clients::HttpError::Response(
crate::clients::HttpResponseError {
code: response.code,
message: "Missing 'inventory_levels' in response".to_string(),
error_reference: response.request_id().map(ToString::to_string),
},
))
})
.and_then(|v| {
serde_json::from_value(v.clone()).map_err(|e| {
ResourceError::Http(crate::clients::HttpError::Response(
crate::clients::HttpResponseError {
code: response.code,
message: format!("Failed to deserialize inventory levels: {e}"),
error_reference: response.request_id().map(ToString::to_string),
},
))
})
})?;
Ok(levels)
}
}
impl RestResource for Location {
type Id = u64;
type FindParams = LocationFindParams;
type AllParams = LocationListParams;
type CountParams = LocationCountParams;
const NAME: &'static str = "Location";
const PLURAL: &'static str = "locations";
const PATHS: &'static [ResourcePath] = &[
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::Find,
&["id"],
"locations/{id}",
),
ResourcePath::new(HttpMethod::Get, ResourceOperation::All, &[], "locations"),
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::Count,
&[],
"locations/count",
),
];
fn get_id(&self) -> Option<Self::Id> {
self.id
}
}
impl ReadOnlyResource for Location {}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct LocationFindParams {
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct LocationListParams {
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct LocationCountParams {
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct LocationInventoryLevelsParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub page_info: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rest::{get_path, ResourceOperation};
#[test]
fn test_location_implements_read_only_resource() {
fn assert_read_only<T: ReadOnlyResource>() {}
assert_read_only::<Location>();
}
#[test]
fn test_location_has_only_get_paths() {
assert!(get_path(Location::PATHS, ResourceOperation::Find, &["id"]).is_some());
assert!(get_path(Location::PATHS, ResourceOperation::All, &[]).is_some());
assert!(get_path(Location::PATHS, ResourceOperation::Count, &[]).is_some());
assert!(get_path(Location::PATHS, ResourceOperation::Create, &[]).is_none());
assert!(get_path(Location::PATHS, ResourceOperation::Update, &["id"]).is_none());
assert!(get_path(Location::PATHS, ResourceOperation::Delete, &["id"]).is_none());
}
#[test]
fn test_location_deserialization() {
let json = r#"{
"id": 655441491,
"name": "Main Warehouse",
"address1": "123 Main St",
"address2": "Suite 100",
"city": "New York",
"province": "New York",
"province_code": "NY",
"country": "United States",
"country_code": "US",
"localized_country_name": "United States",
"localized_province_name": "New York",
"zip": "10001",
"phone": "555-555-5555",
"active": true,
"legacy": false,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-06-20T15:45:00Z",
"admin_graphql_api_id": "gid://shopify/Location/655441491"
}"#;
let location: Location = serde_json::from_str(json).unwrap();
assert_eq!(location.id, Some(655441491));
assert_eq!(location.name, Some("Main Warehouse".to_string()));
assert_eq!(location.address1, Some("123 Main St".to_string()));
assert_eq!(location.address2, Some("Suite 100".to_string()));
assert_eq!(location.city, Some("New York".to_string()));
assert_eq!(location.province, Some("New York".to_string()));
assert_eq!(location.province_code, Some("NY".to_string()));
assert_eq!(location.country, Some("United States".to_string()));
assert_eq!(location.country_code, Some("US".to_string()));
assert_eq!(
location.localized_country_name,
Some("United States".to_string())
);
assert_eq!(
location.localized_province_name,
Some("New York".to_string())
);
assert_eq!(location.zip, Some("10001".to_string()));
assert_eq!(location.phone, Some("555-555-5555".to_string()));
assert_eq!(location.active, Some(true));
assert_eq!(location.legacy, Some(false));
assert!(location.created_at.is_some());
assert!(location.updated_at.is_some());
assert_eq!(
location.admin_graphql_api_id,
Some("gid://shopify/Location/655441491".to_string())
);
}
#[test]
fn test_location_serialization_is_empty() {
let location = Location {
id: Some(655441491),
name: Some("Main Warehouse".to_string()),
city: Some("New York".to_string()),
active: Some(true),
..Default::default()
};
let json = serde_json::to_string(&location).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, serde_json::json!({}));
}
#[test]
fn test_location_get_id_returns_correct_value() {
let location_with_id = Location {
id: Some(655441491),
name: Some("Warehouse".to_string()),
..Default::default()
};
assert_eq!(location_with_id.get_id(), Some(655441491));
let location_without_id = Location {
id: None,
name: Some("New Location".to_string()),
..Default::default()
};
assert_eq!(location_without_id.get_id(), None);
}
#[test]
fn test_location_path_constants() {
let find_path = get_path(Location::PATHS, ResourceOperation::Find, &["id"]);
assert!(find_path.is_some());
assert_eq!(find_path.unwrap().template, "locations/{id}");
assert_eq!(find_path.unwrap().http_method, HttpMethod::Get);
let all_path = get_path(Location::PATHS, ResourceOperation::All, &[]);
assert!(all_path.is_some());
assert_eq!(all_path.unwrap().template, "locations");
let count_path = get_path(Location::PATHS, ResourceOperation::Count, &[]);
assert!(count_path.is_some());
assert_eq!(count_path.unwrap().template, "locations/count");
}
#[test]
fn test_location_constants() {
assert_eq!(Location::NAME, "Location");
assert_eq!(Location::PLURAL, "locations");
}
}