1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9use super::schema::JsonSchema2020;
10
11#[derive(Debug, Clone, Serialize, Deserialize, Default)]
16pub struct Webhook {
17 #[serde(skip_serializing_if = "Option::is_none")]
19 pub summary: Option<String>,
20
21 #[serde(skip_serializing_if = "Option::is_none")]
23 pub description: Option<String>,
24
25 #[serde(flatten)]
27 pub operations: HashMap<String, WebhookOperation>,
28}
29
30impl Webhook {
31 pub fn new() -> Self {
33 Self::default()
34 }
35
36 pub fn with_summary(summary: impl Into<String>) -> Self {
38 Self {
39 summary: Some(summary.into()),
40 ..Default::default()
41 }
42 }
43
44 pub fn summary(mut self, summary: impl Into<String>) -> Self {
46 self.summary = Some(summary.into());
47 self
48 }
49
50 pub fn description(mut self, description: impl Into<String>) -> Self {
52 self.description = Some(description.into());
53 self
54 }
55
56 pub fn post(mut self, operation: WebhookOperation) -> Self {
58 self.operations.insert("post".to_string(), operation);
59 self
60 }
61
62 pub fn get(mut self, operation: WebhookOperation) -> Self {
64 self.operations.insert("get".to_string(), operation);
65 self
66 }
67
68 pub fn put(mut self, operation: WebhookOperation) -> Self {
70 self.operations.insert("put".to_string(), operation);
71 self
72 }
73
74 pub fn delete(mut self, operation: WebhookOperation) -> Self {
76 self.operations.insert("delete".to_string(), operation);
77 self
78 }
79
80 pub fn operation(mut self, method: impl Into<String>, op: WebhookOperation) -> Self {
82 self.operations.insert(method.into().to_lowercase(), op);
83 self
84 }
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize, Default)]
89#[serde(rename_all = "camelCase")]
90pub struct WebhookOperation {
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub tags: Option<Vec<String>>,
94
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub summary: Option<String>,
98
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub description: Option<String>,
102
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub external_docs: Option<ExternalDocs>,
106
107 #[serde(skip_serializing_if = "Option::is_none")]
109 pub operation_id: Option<String>,
110
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub request_body: Option<WebhookRequestBody>,
114
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub responses: Option<HashMap<String, WebhookResponse>>,
118
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub security: Option<Vec<HashMap<String, Vec<String>>>>,
122
123 #[serde(skip_serializing_if = "Option::is_none")]
125 pub deprecated: Option<bool>,
126}
127
128impl WebhookOperation {
129 pub fn new() -> Self {
131 Self::default()
132 }
133
134 pub fn summary(mut self, summary: impl Into<String>) -> Self {
136 self.summary = Some(summary.into());
137 self
138 }
139
140 pub fn description(mut self, description: impl Into<String>) -> Self {
142 self.description = Some(description.into());
143 self
144 }
145
146 pub fn operation_id(mut self, id: impl Into<String>) -> Self {
148 self.operation_id = Some(id.into());
149 self
150 }
151
152 pub fn tags(mut self, tags: Vec<String>) -> Self {
154 self.tags = Some(tags);
155 self
156 }
157
158 pub fn request_body(mut self, body: WebhookRequestBody) -> Self {
160 self.request_body = Some(body);
161 self
162 }
163
164 pub fn response(mut self, status: impl Into<String>, response: WebhookResponse) -> Self {
166 let responses = self.responses.get_or_insert_with(HashMap::new);
167 responses.insert(status.into(), response);
168 self
169 }
170
171 pub fn deprecated(mut self) -> Self {
173 self.deprecated = Some(true);
174 self
175 }
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct ExternalDocs {
181 pub url: String,
183
184 #[serde(skip_serializing_if = "Option::is_none")]
186 pub description: Option<String>,
187}
188
189impl ExternalDocs {
190 pub fn new(url: impl Into<String>) -> Self {
192 Self {
193 url: url.into(),
194 description: None,
195 }
196 }
197
198 pub fn with_description(mut self, description: impl Into<String>) -> Self {
200 self.description = Some(description.into());
201 self
202 }
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct WebhookRequestBody {
208 #[serde(skip_serializing_if = "Option::is_none")]
210 pub description: Option<String>,
211
212 #[serde(skip_serializing_if = "Option::is_none")]
214 pub required: Option<bool>,
215
216 pub content: HashMap<String, MediaTypeObject>,
218}
219
220impl WebhookRequestBody {
221 pub fn json(schema: JsonSchema2020) -> Self {
223 let mut content = HashMap::new();
224 content.insert(
225 "application/json".to_string(),
226 MediaTypeObject {
227 schema: Some(schema),
228 example: None,
229 examples: None,
230 },
231 );
232 Self {
233 description: None,
234 required: Some(true),
235 content,
236 }
237 }
238
239 pub fn with_description(mut self, description: impl Into<String>) -> Self {
241 self.description = Some(description.into());
242 self
243 }
244
245 pub fn required(mut self, required: bool) -> Self {
247 self.required = Some(required);
248 self
249 }
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct MediaTypeObject {
255 #[serde(skip_serializing_if = "Option::is_none")]
257 pub schema: Option<JsonSchema2020>,
258
259 #[serde(skip_serializing_if = "Option::is_none")]
261 pub example: Option<serde_json::Value>,
262
263 #[serde(skip_serializing_if = "Option::is_none")]
265 pub examples: Option<HashMap<String, Example>>,
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
270#[serde(rename_all = "camelCase")]
271pub struct Example {
272 #[serde(skip_serializing_if = "Option::is_none")]
274 pub summary: Option<String>,
275
276 #[serde(skip_serializing_if = "Option::is_none")]
278 pub description: Option<String>,
279
280 #[serde(skip_serializing_if = "Option::is_none")]
282 pub value: Option<serde_json::Value>,
283
284 #[serde(skip_serializing_if = "Option::is_none")]
286 pub external_value: Option<String>,
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct WebhookResponse {
292 pub description: String,
294
295 #[serde(skip_serializing_if = "Option::is_none")]
297 pub content: Option<HashMap<String, MediaTypeObject>>,
298
299 #[serde(skip_serializing_if = "Option::is_none")]
301 pub headers: Option<HashMap<String, Header>>,
302}
303
304impl WebhookResponse {
305 pub fn new(description: impl Into<String>) -> Self {
307 Self {
308 description: description.into(),
309 content: None,
310 headers: None,
311 }
312 }
313
314 pub fn with_json(mut self, schema: JsonSchema2020) -> Self {
316 let content = self.content.get_or_insert_with(HashMap::new);
317 content.insert(
318 "application/json".to_string(),
319 MediaTypeObject {
320 schema: Some(schema),
321 example: None,
322 examples: None,
323 },
324 );
325 self
326 }
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
331pub struct Header {
332 #[serde(skip_serializing_if = "Option::is_none")]
334 pub description: Option<String>,
335
336 #[serde(skip_serializing_if = "Option::is_none")]
338 pub required: Option<bool>,
339
340 #[serde(skip_serializing_if = "Option::is_none")]
342 pub schema: Option<JsonSchema2020>,
343
344 #[serde(skip_serializing_if = "Option::is_none")]
346 pub deprecated: Option<bool>,
347}
348
349#[derive(Debug, Clone, Serialize, Deserialize, Default)]
354pub struct Callback {
355 #[serde(flatten)]
357 pub expressions: HashMap<String, Webhook>,
358}
359
360impl Callback {
361 pub fn new() -> Self {
363 Self::default()
364 }
365
366 pub fn expression(mut self, expr: impl Into<String>, webhook: Webhook) -> Self {
371 self.expressions.insert(expr.into(), webhook);
372 self
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379
380 #[test]
381 fn test_webhook_creation() {
382 let webhook = Webhook::with_summary("Order placed notification")
383 .description("Called when a new order is placed")
384 .post(
385 WebhookOperation::new()
386 .summary("Notify about new order")
387 .operation_id("orderPlaced")
388 .request_body(WebhookRequestBody::json(
389 JsonSchema2020::object()
390 .with_property("orderId", JsonSchema2020::string())
391 .with_property("amount", JsonSchema2020::number())
392 .with_required("orderId"),
393 ))
394 .response(
395 "200",
396 WebhookResponse::new("Webhook processed successfully"),
397 ),
398 );
399
400 assert_eq!(
401 webhook.summary,
402 Some("Order placed notification".to_string())
403 );
404 assert!(webhook.operations.contains_key("post"));
405 }
406
407 #[test]
408 fn test_webhook_serialization() {
409 let webhook = Webhook::new().summary("Test webhook").post(
410 WebhookOperation::new()
411 .operation_id("test")
412 .response("200", WebhookResponse::new("OK")),
413 );
414
415 let json = serde_json::to_value(&webhook).unwrap();
416 assert!(json.get("summary").is_some());
417 assert!(json.get("post").is_some());
418 }
419
420 #[test]
421 fn test_callback_creation() {
422 let callback = Callback::new().expression(
423 "{$request.body#/callbackUrl}",
424 Webhook::new().post(
425 WebhookOperation::new()
426 .summary("Callback notification")
427 .response("200", WebhookResponse::new("Callback received")),
428 ),
429 );
430
431 assert!(callback
432 .expressions
433 .contains_key("{$request.body#/callbackUrl}"));
434 }
435}