forge_core/webhook/
traits.rs1use std::future::Future;
2use std::pin::Pin;
3use std::time::Duration;
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8use crate::error::Result;
9
10use super::context::WebhookContext;
11use super::signature::{IdempotencyConfig, SignatureConfig};
12
13pub trait ForgeWebhook: Send + Sync + 'static {
18 fn info() -> WebhookInfo;
20
21 fn execute(
27 ctx: &WebhookContext,
28 payload: Value,
29 ) -> Pin<Box<dyn Future<Output = Result<WebhookResult>> + Send + '_>>;
30}
31
32#[derive(Debug, Clone)]
34pub struct WebhookInfo {
35 pub name: &'static str,
37 pub path: &'static str,
39 pub signature: Option<SignatureConfig>,
41 pub allow_unsigned: bool,
45 pub idempotency: Option<IdempotencyConfig>,
47 pub timeout: Duration,
49}
50
51impl Default for WebhookInfo {
52 fn default() -> Self {
53 Self {
54 name: "",
55 path: "",
56 signature: None,
57 allow_unsigned: false,
58 idempotency: None,
59 timeout: Duration::from_secs(30),
60 }
61 }
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66#[serde(tag = "status")]
67pub enum WebhookResult {
68 #[serde(rename = "ok")]
70 Ok,
71 #[serde(rename = "accepted")]
73 Accepted,
74 #[serde(rename = "custom")]
76 Custom {
77 status_code: u16,
79 body: Value,
81 },
82}
83
84impl WebhookResult {
85 pub fn status_code(&self) -> u16 {
87 match self {
88 Self::Ok => 200,
89 Self::Accepted => 202,
90 Self::Custom { status_code, .. } => *status_code,
91 }
92 }
93
94 pub fn body(&self) -> Value {
96 match self {
97 Self::Ok => serde_json::json!({"status": "ok"}),
98 Self::Accepted => serde_json::json!({"status": "accepted"}),
99 Self::Custom { body, .. } => body.clone(),
100 }
101 }
102}
103
104#[cfg(test)]
105#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn test_default_webhook_info() {
111 let info = WebhookInfo::default();
112 assert!(info.signature.is_none());
113 assert!(!info.allow_unsigned);
114 assert!(info.idempotency.is_none());
115 assert_eq!(info.timeout, Duration::from_secs(30));
116 }
117
118 #[test]
119 fn test_webhook_result_status_codes() {
120 assert_eq!(WebhookResult::Ok.status_code(), 200);
121 assert_eq!(WebhookResult::Accepted.status_code(), 202);
122 assert_eq!(
123 WebhookResult::Custom {
124 status_code: 400,
125 body: serde_json::json!({"error": "bad request"})
126 }
127 .status_code(),
128 400
129 );
130 }
131
132 #[test]
133 fn test_webhook_result_body() {
134 assert_eq!(
135 WebhookResult::Ok.body(),
136 serde_json::json!({"status": "ok"})
137 );
138 assert_eq!(
139 WebhookResult::Accepted.body(),
140 serde_json::json!({"status": "accepted"})
141 );
142 }
143}