forge_core/webhook/
context.rs1use std::collections::HashMap;
2use std::sync::Arc;
3
4use uuid::Uuid;
5
6use crate::env::{EnvAccess, EnvProvider, RealEnvProvider};
7use crate::function::JobDispatch;
8
9pub struct WebhookContext {
11 pub webhook_name: String,
13 pub request_id: String,
15 pub idempotency_key: Option<String>,
17 headers: HashMap<String, String>,
19 db_pool: sqlx::PgPool,
21 http_client: reqwest::Client,
23 job_dispatch: Option<Arc<dyn JobDispatch>>,
25 env_provider: Arc<dyn EnvProvider>,
27}
28
29impl WebhookContext {
30 pub fn new(
32 webhook_name: String,
33 request_id: String,
34 headers: HashMap<String, String>,
35 db_pool: sqlx::PgPool,
36 http_client: reqwest::Client,
37 ) -> Self {
38 Self {
39 webhook_name,
40 request_id,
41 idempotency_key: None,
42 headers,
43 db_pool,
44 http_client,
45 job_dispatch: None,
46 env_provider: Arc::new(RealEnvProvider::new()),
47 }
48 }
49
50 pub fn with_idempotency_key(mut self, key: Option<String>) -> Self {
52 self.idempotency_key = key;
53 self
54 }
55
56 pub fn with_job_dispatch(mut self, dispatcher: Arc<dyn JobDispatch>) -> Self {
58 self.job_dispatch = Some(dispatcher);
59 self
60 }
61
62 pub fn with_env_provider(mut self, provider: Arc<dyn EnvProvider>) -> Self {
64 self.env_provider = provider;
65 self
66 }
67
68 pub fn db(&self) -> &sqlx::PgPool {
70 &self.db_pool
71 }
72
73 pub fn db_conn(&self) -> crate::function::DbConn<'_> {
75 crate::function::DbConn::Pool(&self.db_pool)
76 }
77
78 pub async fn conn(&self) -> sqlx::Result<crate::function::ForgeConn<'static>> {
80 Ok(crate::function::ForgeConn::Pool(
81 self.db_pool.acquire().await?,
82 ))
83 }
84
85 pub fn http(&self) -> &reqwest::Client {
87 &self.http_client
88 }
89
90 pub fn header(&self, name: &str) -> Option<&str> {
94 self.headers.get(&name.to_lowercase()).map(|s| s.as_str())
95 }
96
97 pub fn headers(&self) -> &HashMap<String, String> {
99 &self.headers
100 }
101
102 pub async fn dispatch_job<T: serde::Serialize>(
116 &self,
117 job_type: &str,
118 args: T,
119 ) -> crate::error::Result<Uuid> {
120 let dispatcher = self.job_dispatch.as_ref().ok_or_else(|| {
121 crate::error::ForgeError::Internal("Job dispatch not available".into())
122 })?;
123 let args_json = serde_json::to_value(args)?;
124 dispatcher.dispatch_by_name(job_type, args_json, None).await
125 }
126
127 pub async fn cancel_job(
129 &self,
130 job_id: Uuid,
131 reason: Option<String>,
132 ) -> crate::error::Result<bool> {
133 let dispatcher = self.job_dispatch.as_ref().ok_or_else(|| {
134 crate::error::ForgeError::Internal("Job dispatch not available".into())
135 })?;
136 dispatcher.cancel(job_id, reason).await
137 }
138}
139
140impl EnvAccess for WebhookContext {
141 fn env_provider(&self) -> &dyn EnvProvider {
142 self.env_provider.as_ref()
143 }
144}
145
146#[cfg(test)]
147#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
148mod tests {
149 use super::*;
150
151 #[tokio::test]
152 async fn test_webhook_context_creation() {
153 let pool = sqlx::postgres::PgPoolOptions::new()
154 .max_connections(1)
155 .connect_lazy("postgres://localhost/nonexistent")
156 .expect("Failed to create mock pool");
157
158 let mut headers = HashMap::new();
159 headers.insert("x-github-event".to_string(), "push".to_string());
160 headers.insert("x-github-delivery".to_string(), "abc-123".to_string());
161
162 let ctx = WebhookContext::new(
163 "github_webhook".to_string(),
164 "req-123".to_string(),
165 headers,
166 pool,
167 reqwest::Client::new(),
168 )
169 .with_idempotency_key(Some("abc-123".to_string()));
170
171 assert_eq!(ctx.webhook_name, "github_webhook");
172 assert_eq!(ctx.request_id, "req-123");
173 assert_eq!(ctx.idempotency_key, Some("abc-123".to_string()));
174 assert_eq!(ctx.header("X-GitHub-Event"), Some("push"));
175 assert_eq!(ctx.header("x-github-event"), Some("push")); assert!(ctx.header("nonexistent").is_none());
177 }
178}