fraiseql_webhooks/
config.rs1use std::collections::HashMap;
4
5use serde::Deserialize;
6
7use crate::WebhookError;
8
9#[derive(Debug, Clone, Deserialize)]
11pub struct WebhookConfig {
12 pub provider: Option<String>,
14
15 pub path: Option<String>,
17
18 pub secret_env: String,
20
21 pub signature_scheme: Option<String>,
23
24 pub signature_header: Option<String>,
26
27 pub timestamp_header: Option<String>,
29
30 #[serde(default = "default_timestamp_tolerance")]
32 pub timestamp_tolerance: u64,
33
34 #[serde(default = "default_idempotent")]
36 pub idempotent: bool,
37
38 #[serde(default)]
40 pub events: HashMap<String, WebhookEventConfig>,
41}
42
43impl WebhookConfig {
44 pub fn validate_secret_env(&self) -> Result<(), WebhookError> {
58 let name = &self.secret_env;
59 if name.is_empty() {
60 return Err(WebhookError::Configuration("secret_env cannot be empty".to_string()));
61 }
62 let mut chars = name.chars();
63 let first = chars.next().expect("non-empty; checked above");
64 if !first.is_ascii_alphabetic() && first != '_' {
65 return Err(WebhookError::Configuration(format!(
66 "secret_env '{name}' must start with a letter or underscore"
67 )));
68 }
69 for ch in chars {
70 if !ch.is_ascii_alphanumeric() && ch != '_' {
71 return Err(WebhookError::Configuration(format!(
72 "secret_env '{name}' contains invalid character '{ch}' (only [A-Za-z0-9_] allowed)"
73 )));
74 }
75 }
76 Ok(())
77 }
78}
79
80fn default_timestamp_tolerance() -> u64 {
81 300
82}
83
84fn default_idempotent() -> bool {
85 true
86}
87
88#[derive(Debug, Clone, Deserialize)]
90pub struct WebhookEventConfig {
91 pub function: String,
93
94 #[serde(default)]
96 pub mapping: HashMap<String, String>,
97
98 pub condition: Option<String>,
100}
101
102#[allow(clippy::unwrap_used)] #[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn test_default_values() {
109 let json = r#"{
110 "secret_env": "WEBHOOK_SECRET",
111 "events": {}
112 }"#;
113
114 let config: WebhookConfig = serde_json::from_str(json).unwrap();
115 assert_eq!(config.timestamp_tolerance, 300);
116 assert!(config.idempotent);
117 }
118
119 #[test]
120 fn test_custom_values() {
121 let json = r#"{
122 "provider": "stripe",
123 "secret_env": "STRIPE_SECRET",
124 "timestamp_tolerance": 600,
125 "idempotent": false,
126 "events": {
127 "payment_intent.succeeded": {
128 "function": "handle_payment",
129 "mapping": {
130 "payment_id": "data.object.id"
131 }
132 }
133 }
134 }"#;
135
136 let config: WebhookConfig = serde_json::from_str(json).unwrap();
137 assert_eq!(config.provider, Some("stripe".to_string()));
138 assert_eq!(config.timestamp_tolerance, 600);
139 assert!(!config.idempotent);
140 assert_eq!(config.events.len(), 1);
141 }
142}