1use chrono::{DateTime, Utc};
41use serde::{Deserialize, Serialize};
42
43use crate::rest::{ResourceOperation, ResourcePath, RestResource};
44use crate::HttpMethod;
45
46use super::common::{WebhookFormat, WebhookTopic};
47
48#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
70pub struct Webhook {
71 #[serde(skip_serializing)]
74 pub id: Option<u64>,
75
76 #[serde(skip_serializing_if = "Option::is_none")]
79 pub address: Option<String>,
80
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub topic: Option<WebhookTopic>,
84
85 #[serde(skip_serializing_if = "Option::is_none")]
88 pub format: Option<WebhookFormat>,
89
90 #[serde(skip_serializing_if = "Option::is_none")]
93 pub api_version: Option<String>,
94
95 #[serde(skip_serializing_if = "Option::is_none")]
98 pub fields: Option<Vec<String>>,
99
100 #[serde(skip_serializing_if = "Option::is_none")]
102 pub metafield_namespaces: Option<Vec<String>>,
103
104 #[serde(skip_serializing)]
107 pub created_at: Option<DateTime<Utc>>,
108
109 #[serde(skip_serializing)]
112 pub updated_at: Option<DateTime<Utc>>,
113
114 #[serde(skip_serializing)]
117 pub admin_graphql_api_id: Option<String>,
118}
119
120impl RestResource for Webhook {
121 type Id = u64;
122 type FindParams = WebhookFindParams;
123 type AllParams = WebhookListParams;
124 type CountParams = WebhookCountParams;
125
126 const NAME: &'static str = "Webhook";
127 const PLURAL: &'static str = "webhooks";
128
129 const PATHS: &'static [ResourcePath] = &[
130 ResourcePath::new(
131 HttpMethod::Get,
132 ResourceOperation::Find,
133 &["id"],
134 "webhooks/{id}",
135 ),
136 ResourcePath::new(HttpMethod::Get, ResourceOperation::All, &[], "webhooks"),
137 ResourcePath::new(
138 HttpMethod::Get,
139 ResourceOperation::Count,
140 &[],
141 "webhooks/count",
142 ),
143 ResourcePath::new(HttpMethod::Post, ResourceOperation::Create, &[], "webhooks"),
144 ResourcePath::new(
145 HttpMethod::Put,
146 ResourceOperation::Update,
147 &["id"],
148 "webhooks/{id}",
149 ),
150 ResourcePath::new(
151 HttpMethod::Delete,
152 ResourceOperation::Delete,
153 &["id"],
154 "webhooks/{id}",
155 ),
156 ];
157
158 fn get_id(&self) -> Option<Self::Id> {
159 self.id
160 }
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
165pub struct WebhookFindParams {
166 #[serde(skip_serializing_if = "Option::is_none")]
168 pub fields: Option<String>,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
173pub struct WebhookListParams {
174 #[serde(skip_serializing_if = "Option::is_none")]
176 pub address: Option<String>,
177
178 #[serde(skip_serializing_if = "Option::is_none")]
181 pub topic: Option<String>,
182
183 #[serde(skip_serializing_if = "Option::is_none")]
185 pub created_at_min: Option<DateTime<Utc>>,
186
187 #[serde(skip_serializing_if = "Option::is_none")]
189 pub created_at_max: Option<DateTime<Utc>>,
190
191 #[serde(skip_serializing_if = "Option::is_none")]
193 pub updated_at_min: Option<DateTime<Utc>>,
194
195 #[serde(skip_serializing_if = "Option::is_none")]
197 pub updated_at_max: Option<DateTime<Utc>>,
198
199 #[serde(skip_serializing_if = "Option::is_none")]
201 pub limit: Option<u32>,
202
203 #[serde(skip_serializing_if = "Option::is_none")]
205 pub since_id: Option<u64>,
206
207 #[serde(skip_serializing_if = "Option::is_none")]
209 pub page_info: Option<String>,
210
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub fields: Option<String>,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
218pub struct WebhookCountParams {
219 #[serde(skip_serializing_if = "Option::is_none")]
221 pub address: Option<String>,
222
223 #[serde(skip_serializing_if = "Option::is_none")]
226 pub topic: Option<String>,
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232 use crate::rest::{get_path, ResourceOperation};
233
234 #[test]
235 fn test_webhook_struct_serialization() {
236 let webhook = Webhook {
237 id: Some(12345),
238 address: Some("https://example.com/webhooks".to_string()),
239 topic: Some(WebhookTopic::OrdersCreate),
240 format: Some(WebhookFormat::Json),
241 api_version: Some("2025-10".to_string()),
242 fields: Some(vec!["id".to_string(), "email".to_string()]),
243 metafield_namespaces: Some(vec!["custom".to_string()]),
244 created_at: Some(
245 DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
246 .unwrap()
247 .with_timezone(&Utc),
248 ),
249 updated_at: Some(
250 DateTime::parse_from_rfc3339("2024-06-20T15:45:00Z")
251 .unwrap()
252 .with_timezone(&Utc),
253 ),
254 admin_graphql_api_id: Some("gid://shopify/WebhookSubscription/12345".to_string()),
255 };
256
257 let json = serde_json::to_string(&webhook).unwrap();
258 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
259
260 assert_eq!(parsed["address"], "https://example.com/webhooks");
262 assert_eq!(parsed["topic"], "orders/create");
263 assert_eq!(parsed["format"], "json");
264 assert_eq!(parsed["api_version"], "2025-10");
265 assert_eq!(parsed["fields"], serde_json::json!(["id", "email"]));
266 assert_eq!(
267 parsed["metafield_namespaces"],
268 serde_json::json!(["custom"])
269 );
270
271 assert!(parsed.get("id").is_none());
273 assert!(parsed.get("created_at").is_none());
274 assert!(parsed.get("updated_at").is_none());
275 assert!(parsed.get("admin_graphql_api_id").is_none());
276 }
277
278 #[test]
279 fn test_webhook_deserialization_from_api_response() {
280 let json = r#"{
281 "id": 4759306,
282 "address": "https://example.com/webhooks/orders",
283 "topic": "orders/create",
284 "created_at": "2024-01-15T10:30:00Z",
285 "updated_at": "2024-06-20T15:45:00Z",
286 "format": "json",
287 "fields": ["id", "email", "total_price"],
288 "metafield_namespaces": ["custom", "global"],
289 "api_version": "2025-10",
290 "admin_graphql_api_id": "gid://shopify/WebhookSubscription/4759306"
291 }"#;
292
293 let webhook: Webhook = serde_json::from_str(json).unwrap();
294
295 assert_eq!(webhook.id, Some(4759306));
296 assert_eq!(
297 webhook.address,
298 Some("https://example.com/webhooks/orders".to_string())
299 );
300 assert_eq!(webhook.topic, Some(WebhookTopic::OrdersCreate));
301 assert_eq!(webhook.format, Some(WebhookFormat::Json));
302 assert_eq!(webhook.api_version, Some("2025-10".to_string()));
303 assert_eq!(
304 webhook.fields,
305 Some(vec![
306 "id".to_string(),
307 "email".to_string(),
308 "total_price".to_string()
309 ])
310 );
311 assert_eq!(
312 webhook.metafield_namespaces,
313 Some(vec!["custom".to_string(), "global".to_string()])
314 );
315 assert!(webhook.created_at.is_some());
316 assert!(webhook.updated_at.is_some());
317 assert_eq!(
318 webhook.admin_graphql_api_id,
319 Some("gid://shopify/WebhookSubscription/4759306".to_string())
320 );
321 }
322
323 #[test]
324 fn test_webhook_topic_enum_in_struct() {
325 let topics = vec![
327 (WebhookTopic::OrdersCreate, "orders/create"),
328 (WebhookTopic::OrdersUpdated, "orders/updated"),
329 (WebhookTopic::OrdersPaid, "orders/paid"),
330 (WebhookTopic::ProductsCreate, "products/create"),
331 (WebhookTopic::ProductsUpdate, "products/update"),
332 (WebhookTopic::CustomersCreate, "customers/create"),
333 (WebhookTopic::AppUninstalled, "app/uninstalled"),
334 ];
335
336 for (topic, expected_str) in topics {
337 let webhook = Webhook {
338 topic: Some(topic),
339 address: Some("https://example.com".to_string()),
340 ..Default::default()
341 };
342
343 let json = serde_json::to_value(&webhook).unwrap();
344 assert_eq!(json["topic"], expected_str);
345 }
346 }
347
348 #[test]
349 fn test_webhook_format_enum_handling() {
350 let webhook_json = Webhook {
352 format: Some(WebhookFormat::Json),
353 address: Some("https://example.com".to_string()),
354 ..Default::default()
355 };
356
357 let json = serde_json::to_value(&webhook_json).unwrap();
358 assert_eq!(json["format"], "json");
359
360 let webhook_xml = Webhook {
362 format: Some(WebhookFormat::Xml),
363 address: Some("https://example.com".to_string()),
364 ..Default::default()
365 };
366
367 let json = serde_json::to_value(&webhook_xml).unwrap();
368 assert_eq!(json["format"], "xml");
369
370 assert_eq!(WebhookFormat::default(), WebhookFormat::Json);
372 }
373
374 #[test]
375 fn test_webhook_list_params_with_topic_filter() {
376 let params = WebhookListParams {
377 topic: Some("orders/create".to_string()),
378 address: Some("https://example.com".to_string()),
379 limit: Some(50),
380 since_id: Some(100),
381 ..Default::default()
382 };
383
384 let json = serde_json::to_value(¶ms).unwrap();
385
386 assert_eq!(json["topic"], "orders/create");
387 assert_eq!(json["address"], "https://example.com");
388 assert_eq!(json["limit"], 50);
389 assert_eq!(json["since_id"], 100);
390
391 assert!(json.get("created_at_min").is_none());
393 assert!(json.get("page_info").is_none());
394 }
395
396 #[test]
397 fn test_webhook_count_params() {
398 let params = WebhookCountParams {
399 topic: Some("orders/create".to_string()),
400 address: Some("https://example.com".to_string()),
401 };
402
403 let json = serde_json::to_value(¶ms).unwrap();
404
405 assert_eq!(json["topic"], "orders/create");
406 assert_eq!(json["address"], "https://example.com");
407
408 let empty_params = WebhookCountParams::default();
410 let empty_json = serde_json::to_value(&empty_params).unwrap();
411 assert_eq!(empty_json, serde_json::json!({}));
412 }
413
414 #[test]
415 fn test_webhook_path_constants_are_correct() {
416 let find_path = get_path(Webhook::PATHS, ResourceOperation::Find, &["id"]);
418 assert!(find_path.is_some());
419 assert_eq!(find_path.unwrap().template, "webhooks/{id}");
420 assert_eq!(find_path.unwrap().http_method, HttpMethod::Get);
421
422 let all_path = get_path(Webhook::PATHS, ResourceOperation::All, &[]);
424 assert!(all_path.is_some());
425 assert_eq!(all_path.unwrap().template, "webhooks");
426 assert_eq!(all_path.unwrap().http_method, HttpMethod::Get);
427
428 let count_path = get_path(Webhook::PATHS, ResourceOperation::Count, &[]);
430 assert!(count_path.is_some());
431 assert_eq!(count_path.unwrap().template, "webhooks/count");
432 assert_eq!(count_path.unwrap().http_method, HttpMethod::Get);
433
434 let create_path = get_path(Webhook::PATHS, ResourceOperation::Create, &[]);
436 assert!(create_path.is_some());
437 assert_eq!(create_path.unwrap().template, "webhooks");
438 assert_eq!(create_path.unwrap().http_method, HttpMethod::Post);
439
440 let update_path = get_path(Webhook::PATHS, ResourceOperation::Update, &["id"]);
442 assert!(update_path.is_some());
443 assert_eq!(update_path.unwrap().template, "webhooks/{id}");
444 assert_eq!(update_path.unwrap().http_method, HttpMethod::Put);
445
446 let delete_path = get_path(Webhook::PATHS, ResourceOperation::Delete, &["id"]);
448 assert!(delete_path.is_some());
449 assert_eq!(delete_path.unwrap().template, "webhooks/{id}");
450 assert_eq!(delete_path.unwrap().http_method, HttpMethod::Delete);
451
452 assert_eq!(Webhook::NAME, "Webhook");
454 assert_eq!(Webhook::PLURAL, "webhooks");
455 }
456
457 #[test]
458 fn test_webhook_get_id_returns_correct_value() {
459 let webhook_with_id = Webhook {
461 id: Some(123456789),
462 address: Some("https://example.com".to_string()),
463 ..Default::default()
464 };
465 assert_eq!(webhook_with_id.get_id(), Some(123456789));
466
467 let webhook_without_id = Webhook {
469 id: None,
470 address: Some("https://example.com".to_string()),
471 ..Default::default()
472 };
473 assert_eq!(webhook_without_id.get_id(), None);
474 }
475}