use crate::error::Result;
use async_trait::async_trait;
use serde::de::DeserializeOwned;
pub trait WebhookEvent: DeserializeOwned + Send + Sync {
fn event_id(&self) -> &str;
fn event_type(&self) -> &str;
}
#[async_trait]
pub trait WebhookHandler<E: WebhookEvent>: Send + Sync {
async fn handle(&self, event: &E) -> Result<()>;
async fn validate(&self, _event: &E) -> Result<()> {
Ok(())
}
async fn on_error(&self, event: &E, error: &crate::error::TidewayError) {
tracing::error!(
event_id = event.event_id(),
event_type = event.event_type(),
error = %error,
"Webhook processing failed"
);
}
}
pub struct WebhookRouter {
}
impl WebhookRouter {
pub fn new() -> Self {
Self {}
}
pub async fn process<E, H>(
&self,
event: &E,
handler: &H,
idempotency_store: &dyn crate::webhooks::IdempotencyStore,
) -> Result<()>
where
E: WebhookEvent,
H: WebhookHandler<E>,
{
if idempotency_store.is_processed(event.event_id()).await? {
tracing::debug!(
event_id = event.event_id(),
"Skipping already processed event"
);
return Ok(());
}
handler.validate(event).await?;
match handler.handle(event).await {
Ok(()) => {
idempotency_store
.mark_processed(event.event_id().to_string())
.await?;
tracing::info!(
event_id = event.event_id(),
event_type = event.event_type(),
"Webhook processed successfully"
);
Ok(())
}
Err(e) => {
handler.on_error(event, &e).await;
Err(e)
}
}
}
}
impl Default for WebhookRouter {
fn default() -> Self {
Self::new()
}
}