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 pub http_timeout: Option<Duration>,
51}
52
53impl Default for WebhookInfo {
54 fn default() -> Self {
55 Self {
56 name: "",
57 path: "",
58 signature: None,
59 allow_unsigned: false,
60 idempotency: None,
61 timeout: Duration::from_secs(30),
62 http_timeout: None,
63 }
64 }
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69#[serde(tag = "status")]
70pub enum WebhookResult {
71 #[serde(rename = "ok")]
73 Ok,
74 #[serde(rename = "accepted")]
76 Accepted,
77 #[serde(rename = "custom")]
79 Custom {
80 status_code: u16,
82 body: Value,
84 },
85}
86
87impl WebhookResult {
88 pub fn status_code(&self) -> u16 {
90 match self {
91 Self::Ok => 200,
92 Self::Accepted => 202,
93 Self::Custom { status_code, .. } => *status_code,
94 }
95 }
96
97 pub fn body(&self) -> Value {
99 match self {
100 Self::Ok => serde_json::json!({"status": "ok"}),
101 Self::Accepted => serde_json::json!({"status": "accepted"}),
102 Self::Custom { body, .. } => body.clone(),
103 }
104 }
105}
106
107#[cfg(test)]
108#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn test_default_webhook_info() {
114 let info = WebhookInfo::default();
115 assert!(info.signature.is_none());
116 assert!(!info.allow_unsigned);
117 assert!(info.idempotency.is_none());
118 assert_eq!(info.timeout, Duration::from_secs(30));
119 assert_eq!(info.http_timeout, None);
120 }
121
122 #[test]
123 fn test_webhook_result_status_codes() {
124 assert_eq!(WebhookResult::Ok.status_code(), 200);
125 assert_eq!(WebhookResult::Accepted.status_code(), 202);
126 assert_eq!(
127 WebhookResult::Custom {
128 status_code: 400,
129 body: serde_json::json!({"error": "bad request"})
130 }
131 .status_code(),
132 400
133 );
134 }
135
136 #[test]
137 fn test_webhook_result_body() {
138 assert_eq!(
139 WebhookResult::Ok.body(),
140 serde_json::json!({"status": "ok"})
141 );
142 assert_eq!(
143 WebhookResult::Accepted.body(),
144 serde_json::json!({"status": "accepted"})
145 );
146 }
147}