use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use uuid::Uuid;
use crate::env::{EnvAccess, EnvProvider, RealEnvProvider};
use crate::function::JobDispatch;
use crate::http::CircuitBreakerClient;
pub struct WebhookContext {
pub webhook_name: String,
pub request_id: String,
pub idempotency_key: Option<String>,
headers: HashMap<String, String>,
db_pool: sqlx::PgPool,
http_client: CircuitBreakerClient,
http_timeout: Option<Duration>,
job_dispatch: Option<Arc<dyn JobDispatch>>,
env_provider: Arc<dyn EnvProvider>,
}
impl WebhookContext {
pub fn new(
webhook_name: String,
request_id: String,
headers: HashMap<String, String>,
db_pool: sqlx::PgPool,
http_client: CircuitBreakerClient,
) -> Self {
Self {
webhook_name,
request_id,
idempotency_key: None,
headers,
db_pool,
http_client,
http_timeout: None,
job_dispatch: None,
env_provider: Arc::new(RealEnvProvider::new()),
}
}
pub fn with_idempotency_key(mut self, key: Option<String>) -> Self {
self.idempotency_key = key;
self
}
pub fn with_job_dispatch(mut self, dispatcher: Arc<dyn JobDispatch>) -> Self {
self.job_dispatch = Some(dispatcher);
self
}
pub fn with_env_provider(mut self, provider: Arc<dyn EnvProvider>) -> Self {
self.env_provider = provider;
self
}
pub fn db(&self) -> crate::function::ForgeDb {
crate::function::ForgeDb::from_pool(&self.db_pool)
}
pub fn db_conn(&self) -> crate::function::DbConn<'_> {
crate::function::DbConn::Pool(self.db_pool.clone())
}
pub async fn conn(&self) -> sqlx::Result<crate::function::ForgeConn<'static>> {
Ok(crate::function::ForgeConn::Pool(
self.db_pool.acquire().await?,
))
}
pub fn http(&self) -> crate::http::HttpClient {
self.http_client.with_timeout(self.http_timeout)
}
pub fn raw_http(&self) -> &reqwest::Client {
self.http_client.inner()
}
pub fn set_http_timeout(&mut self, timeout: Option<Duration>) {
self.http_timeout = timeout;
}
pub fn header(&self, name: &str) -> Option<&str> {
self.headers.get(&name.to_lowercase()).map(|s| s.as_str())
}
pub fn headers(&self) -> &HashMap<String, String> {
&self.headers
}
pub async fn dispatch_job<T: serde::Serialize>(
&self,
job_type: &str,
args: T,
) -> crate::error::Result<Uuid> {
let dispatcher = self.job_dispatch.as_ref().ok_or_else(|| {
crate::error::ForgeError::Internal("Job dispatch not available".into())
})?;
let args_json = serde_json::to_value(args)?;
dispatcher.dispatch_by_name(job_type, args_json, None).await
}
pub async fn cancel_job(
&self,
job_id: Uuid,
reason: Option<String>,
) -> crate::error::Result<bool> {
let dispatcher = self.job_dispatch.as_ref().ok_or_else(|| {
crate::error::ForgeError::Internal("Job dispatch not available".into())
})?;
dispatcher.cancel(job_id, reason).await
}
}
impl EnvAccess for WebhookContext {
fn env_provider(&self) -> &dyn EnvProvider {
self.env_provider.as_ref()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
mod tests {
use super::*;
#[tokio::test]
async fn test_webhook_context_creation() {
let pool = sqlx::postgres::PgPoolOptions::new()
.max_connections(1)
.connect_lazy("postgres://localhost/nonexistent")
.expect("Failed to create mock pool");
let mut headers = HashMap::new();
headers.insert("x-github-event".to_string(), "push".to_string());
headers.insert("x-github-delivery".to_string(), "abc-123".to_string());
let ctx = WebhookContext::new(
"github_webhook".to_string(),
"req-123".to_string(),
headers,
pool,
CircuitBreakerClient::with_defaults(reqwest::Client::new()),
)
.with_idempotency_key(Some("abc-123".to_string()));
assert_eq!(ctx.webhook_name, "github_webhook");
assert_eq!(ctx.request_id, "req-123");
assert_eq!(ctx.idempotency_key, Some("abc-123".to_string()));
assert_eq!(ctx.header("X-GitHub-Event"), Some("push"));
assert_eq!(ctx.header("x-github-event"), Some("push")); assert!(ctx.header("nonexistent").is_none());
}
}