use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::rest::{ResourceOperation, ResourcePath, RestResource};
use crate::HttpMethod;
use super::common::{WebhookFormat, WebhookTopic};
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct Webhook {
#[serde(skip_serializing)]
pub id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub address: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub topic: Option<WebhookTopic>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<WebhookFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub api_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metafield_namespaces: Option<Vec<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>,
}
impl RestResource for Webhook {
type Id = u64;
type FindParams = WebhookFindParams;
type AllParams = WebhookListParams;
type CountParams = WebhookCountParams;
const NAME: &'static str = "Webhook";
const PLURAL: &'static str = "webhooks";
const PATHS: &'static [ResourcePath] = &[
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::Find,
&["id"],
"webhooks/{id}",
),
ResourcePath::new(HttpMethod::Get, ResourceOperation::All, &[], "webhooks"),
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::Count,
&[],
"webhooks/count",
),
ResourcePath::new(HttpMethod::Post, ResourceOperation::Create, &[], "webhooks"),
ResourcePath::new(
HttpMethod::Put,
ResourceOperation::Update,
&["id"],
"webhooks/{id}",
),
ResourcePath::new(
HttpMethod::Delete,
ResourceOperation::Delete,
&["id"],
"webhooks/{id}",
),
];
fn get_id(&self) -> Option<Self::Id> {
self.id
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct WebhookFindParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct WebhookListParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub address: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub topic: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at_min: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at_max: Option<DateTime<Utc>>,
#[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 limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub since_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub page_info: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct WebhookCountParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub address: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub topic: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rest::{get_path, ResourceOperation};
#[test]
fn test_webhook_struct_serialization() {
let webhook = Webhook {
id: Some(12345),
address: Some("https://example.com/webhooks".to_string()),
topic: Some(WebhookTopic::OrdersCreate),
format: Some(WebhookFormat::Json),
api_version: Some("2025-10".to_string()),
fields: Some(vec!["id".to_string(), "email".to_string()]),
metafield_namespaces: Some(vec!["custom".to_string()]),
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/WebhookSubscription/12345".to_string()),
};
let json = serde_json::to_string(&webhook).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["address"], "https://example.com/webhooks");
assert_eq!(parsed["topic"], "orders/create");
assert_eq!(parsed["format"], "json");
assert_eq!(parsed["api_version"], "2025-10");
assert_eq!(parsed["fields"], serde_json::json!(["id", "email"]));
assert_eq!(
parsed["metafield_namespaces"],
serde_json::json!(["custom"])
);
assert!(parsed.get("id").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());
}
#[test]
fn test_webhook_deserialization_from_api_response() {
let json = r#"{
"id": 4759306,
"address": "https://example.com/webhooks/orders",
"topic": "orders/create",
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-06-20T15:45:00Z",
"format": "json",
"fields": ["id", "email", "total_price"],
"metafield_namespaces": ["custom", "global"],
"api_version": "2025-10",
"admin_graphql_api_id": "gid://shopify/WebhookSubscription/4759306"
}"#;
let webhook: Webhook = serde_json::from_str(json).unwrap();
assert_eq!(webhook.id, Some(4759306));
assert_eq!(
webhook.address,
Some("https://example.com/webhooks/orders".to_string())
);
assert_eq!(webhook.topic, Some(WebhookTopic::OrdersCreate));
assert_eq!(webhook.format, Some(WebhookFormat::Json));
assert_eq!(webhook.api_version, Some("2025-10".to_string()));
assert_eq!(
webhook.fields,
Some(vec![
"id".to_string(),
"email".to_string(),
"total_price".to_string()
])
);
assert_eq!(
webhook.metafield_namespaces,
Some(vec!["custom".to_string(), "global".to_string()])
);
assert!(webhook.created_at.is_some());
assert!(webhook.updated_at.is_some());
assert_eq!(
webhook.admin_graphql_api_id,
Some("gid://shopify/WebhookSubscription/4759306".to_string())
);
}
#[test]
fn test_webhook_topic_enum_in_struct() {
let topics = vec![
(WebhookTopic::OrdersCreate, "orders/create"),
(WebhookTopic::OrdersUpdated, "orders/updated"),
(WebhookTopic::OrdersPaid, "orders/paid"),
(WebhookTopic::ProductsCreate, "products/create"),
(WebhookTopic::ProductsUpdate, "products/update"),
(WebhookTopic::CustomersCreate, "customers/create"),
(WebhookTopic::AppUninstalled, "app/uninstalled"),
];
for (topic, expected_str) in topics {
let webhook = Webhook {
topic: Some(topic),
address: Some("https://example.com".to_string()),
..Default::default()
};
let json = serde_json::to_value(&webhook).unwrap();
assert_eq!(json["topic"], expected_str);
}
}
#[test]
fn test_webhook_format_enum_handling() {
let webhook_json = Webhook {
format: Some(WebhookFormat::Json),
address: Some("https://example.com".to_string()),
..Default::default()
};
let json = serde_json::to_value(&webhook_json).unwrap();
assert_eq!(json["format"], "json");
let webhook_xml = Webhook {
format: Some(WebhookFormat::Xml),
address: Some("https://example.com".to_string()),
..Default::default()
};
let json = serde_json::to_value(&webhook_xml).unwrap();
assert_eq!(json["format"], "xml");
assert_eq!(WebhookFormat::default(), WebhookFormat::Json);
}
#[test]
fn test_webhook_list_params_with_topic_filter() {
let params = WebhookListParams {
topic: Some("orders/create".to_string()),
address: Some("https://example.com".to_string()),
limit: Some(50),
since_id: Some(100),
..Default::default()
};
let json = serde_json::to_value(¶ms).unwrap();
assert_eq!(json["topic"], "orders/create");
assert_eq!(json["address"], "https://example.com");
assert_eq!(json["limit"], 50);
assert_eq!(json["since_id"], 100);
assert!(json.get("created_at_min").is_none());
assert!(json.get("page_info").is_none());
}
#[test]
fn test_webhook_count_params() {
let params = WebhookCountParams {
topic: Some("orders/create".to_string()),
address: Some("https://example.com".to_string()),
};
let json = serde_json::to_value(¶ms).unwrap();
assert_eq!(json["topic"], "orders/create");
assert_eq!(json["address"], "https://example.com");
let empty_params = WebhookCountParams::default();
let empty_json = serde_json::to_value(&empty_params).unwrap();
assert_eq!(empty_json, serde_json::json!({}));
}
#[test]
fn test_webhook_path_constants_are_correct() {
let find_path = get_path(Webhook::PATHS, ResourceOperation::Find, &["id"]);
assert!(find_path.is_some());
assert_eq!(find_path.unwrap().template, "webhooks/{id}");
assert_eq!(find_path.unwrap().http_method, HttpMethod::Get);
let all_path = get_path(Webhook::PATHS, ResourceOperation::All, &[]);
assert!(all_path.is_some());
assert_eq!(all_path.unwrap().template, "webhooks");
assert_eq!(all_path.unwrap().http_method, HttpMethod::Get);
let count_path = get_path(Webhook::PATHS, ResourceOperation::Count, &[]);
assert!(count_path.is_some());
assert_eq!(count_path.unwrap().template, "webhooks/count");
assert_eq!(count_path.unwrap().http_method, HttpMethod::Get);
let create_path = get_path(Webhook::PATHS, ResourceOperation::Create, &[]);
assert!(create_path.is_some());
assert_eq!(create_path.unwrap().template, "webhooks");
assert_eq!(create_path.unwrap().http_method, HttpMethod::Post);
let update_path = get_path(Webhook::PATHS, ResourceOperation::Update, &["id"]);
assert!(update_path.is_some());
assert_eq!(update_path.unwrap().template, "webhooks/{id}");
assert_eq!(update_path.unwrap().http_method, HttpMethod::Put);
let delete_path = get_path(Webhook::PATHS, ResourceOperation::Delete, &["id"]);
assert!(delete_path.is_some());
assert_eq!(delete_path.unwrap().template, "webhooks/{id}");
assert_eq!(delete_path.unwrap().http_method, HttpMethod::Delete);
assert_eq!(Webhook::NAME, "Webhook");
assert_eq!(Webhook::PLURAL, "webhooks");
}
#[test]
fn test_webhook_get_id_returns_correct_value() {
let webhook_with_id = Webhook {
id: Some(123456789),
address: Some("https://example.com".to_string()),
..Default::default()
};
assert_eq!(webhook_with_id.get_id(), Some(123456789));
let webhook_without_id = Webhook {
id: None,
address: Some("https://example.com".to_string()),
..Default::default()
};
assert_eq!(webhook_without_id.get_id(), None);
}
}