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;
9use crate::function::{FunctionInfo, FunctionKind};
10
11use super::context::WebhookContext;
12use super::signature::{IdempotencyConfig, SignatureConfig};
13
14pub trait ForgeWebhook: crate::__sealed::Sealed + Send + Sync + 'static {
16 type Payload: serde::de::DeserializeOwned + Send + Sync + 'static;
17
18 fn info() -> WebhookInfo;
19
20 fn execute(
21 ctx: &WebhookContext,
22 payload: Self::Payload,
23 ) -> Pin<Box<dyn Future<Output = Result<WebhookResult>> + Send + '_>>;
24}
25
26#[derive(Debug, Clone)]
28pub struct WebhookInfo {
29 pub name: &'static str,
30 pub description: Option<&'static str>,
31 pub path: &'static str,
32 pub signature: Option<SignatureConfig>,
33 pub allow_unsigned: bool,
34 pub idempotency: Option<IdempotencyConfig>,
35 pub timeout: Duration,
36 pub http_timeout: Option<Duration>,
37}
38
39impl Default for WebhookInfo {
40 fn default() -> Self {
41 Self {
42 name: "",
43 description: None,
44 path: "",
45 signature: None,
46 allow_unsigned: false,
47 idempotency: None,
48 timeout: Duration::from_secs(30),
49 http_timeout: None,
50 }
51 }
52}
53
54impl From<&WebhookInfo> for FunctionInfo {
55 fn from(webhook: &WebhookInfo) -> Self {
56 Self {
57 name: webhook.name,
58 description: webhook.description,
59 kind: FunctionKind::Webhook,
60 required_role: None,
61 is_public: true,
62 cache_ttl: None,
63 timeout: Some(webhook.timeout),
64 http_timeout: webhook.http_timeout,
65 rate_limit_requests: None,
66 rate_limit_per_secs: None,
67 rate_limit_key: None,
68 log_level: None,
69 table_dependencies: &[],
70 selected_columns: &[],
71 changed_columns: &[],
72 transactional: false,
73 consistent: false,
74 max_upload_size_bytes: None,
75 requires_tenant_scope: false,
76 }
77 }
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
81#[serde(tag = "status")]
82#[non_exhaustive]
83pub enum WebhookResult {
84 #[serde(rename = "ok")]
85 Ok,
86 #[serde(rename = "accepted")]
87 Accepted,
88 #[serde(rename = "custom")]
89 Custom { status_code: u16, body: Value },
90}
91
92impl WebhookResult {
93 pub fn status_code(&self) -> u16 {
94 match self {
95 Self::Ok => 200,
96 Self::Accepted => 202,
97 Self::Custom { status_code, .. } => *status_code,
98 }
99 }
100
101 pub fn body(&self) -> Value {
102 match self {
103 Self::Ok => serde_json::json!({"status": "ok"}),
104 Self::Accepted => serde_json::json!({"status": "accepted"}),
105 Self::Custom { body, .. } => body.clone(),
106 }
107 }
108}
109
110#[cfg(test)]
111#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn test_default_webhook_info() {
117 let info = WebhookInfo::default();
118 assert!(info.signature.is_none());
119 assert!(!info.allow_unsigned);
120 assert!(info.idempotency.is_none());
121 assert_eq!(info.timeout, Duration::from_secs(30));
122 assert_eq!(info.http_timeout, None);
123 }
124
125 #[test]
126 fn test_webhook_result_status_codes() {
127 assert_eq!(WebhookResult::Ok.status_code(), 200);
128 assert_eq!(WebhookResult::Accepted.status_code(), 202);
129 assert_eq!(
130 WebhookResult::Custom {
131 status_code: 400,
132 body: serde_json::json!({"error": "bad request"})
133 }
134 .status_code(),
135 400
136 );
137 }
138
139 #[test]
140 fn test_webhook_result_body() {
141 assert_eq!(
142 WebhookResult::Ok.body(),
143 serde_json::json!({"status": "ok"})
144 );
145 assert_eq!(
146 WebhookResult::Accepted.body(),
147 serde_json::json!({"status": "accepted"})
148 );
149 }
150}