use std::sync::Arc;
use axum::{
extract::State,
routing::{get, post},
Json, Router,
};
use serde::{Deserialize, Serialize};
use serde_json::json;
use webhooksmith::WebhookEngine;
const DATABASE_URL: &str = "postgres://hooksmith:hooksmith@localhost:5432/hooksmith";
const PARTNER_WEBHOOK_URL: &str = "https://httpbin.org/post";
const SIGNING_SECRET: &str = "your-signing-secret-min-32-chars";
#[derive(Clone)]
struct AppState {
engine: Arc<WebhookEngine>,
partner_endpoint_id: uuid::Uuid,
}
#[derive(Deserialize)]
struct CreateOrderRequest {
product_id: i64,
quantity: u32,
}
#[derive(Serialize)]
struct OrderResponse {
order_id: i64,
status: &'static str,
}
async fn create_order(
State(state): State<AppState>,
Json(req): Json<CreateOrderRequest>,
) -> Json<OrderResponse> {
let order_id = 1001i64;
let mut tx = state.engine.pool().begin().await.unwrap();
state.engine
.send_in_tx(
"order.created",
json!({
"order_id": order_id,
"product_id": req.product_id,
"quantity": req.quantity,
}),
state.partner_endpoint_id,
&mut tx,
)
.await
.unwrap();
tx.commit().await.unwrap();
Json(OrderResponse { order_id, status: "created" })
}
async fn health(State(state): State<AppState>) -> Json<serde_json::Value> {
let stats = state.engine.queue_stats().await.unwrap_or_default();
Json(json!({
"status": "ok",
"webhook_queue": {
"pending": stats.pending,
"delivering": stats.delivering,
"failed": stats.failed,
"dead": stats.dead,
"delivered": stats.delivered,
}
}))
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt().without_time().init();
let engine = Arc::new(
WebhookEngine::builder()
.database_url(DATABASE_URL)
.build()
.await?,
);
engine.migrate().await?;
let endpoint = engine
.register(PARTNER_WEBHOOK_URL, SIGNING_SECRET)
.await?;
println!("Partner endpoint: {}", endpoint.id);
let state = AppState {
engine: engine.clone(),
partner_endpoint_id: endpoint.id,
};
let app = Router::new()
.route("/orders", post(create_order))
.route("/health", get(health))
.with_state(state);
let _shutdown = async {
tokio::signal::ctrl_c().await.ok();
println!("Shutting down...");
};
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
println!("HTTP server on http://0.0.0.0:3000");
println!("Try: curl -X POST http://localhost:3000/orders -H 'Content-Type: application/json' -d '{{\"product_id\":1,\"quantity\":2}}'");
tokio::select! {
result = axum::serve(listener, app) => {
result?;
}
_ = engine.run_graceful(async {
tokio::signal::ctrl_c().await.ok();
}) => {}
}
println!("Shutdown complete.");
Ok(())
}