qrush 2.1.1

Lightweight Job Queue and Task Scheduler for Rust (Actix/Axum + Redis + Cron)
Documentation
// src/routes/axum_route.rs
//
// Axum adapter for the dashboard. Mirrors `metrics_route` (the Actix adapter):
// each handler extracts request data with Axum extractors, delegates to the
// framework-neutral service functions, and converts the returned
// `DashboardResponse` into an Axum `Response`.

use axum::{
    body::Body,
    extract::{Path, Query},
    http::{header, HeaderValue, StatusCode},
    middleware,
    response::{IntoResponse, Response},
    routing::{get, post},
    Json, Router,
};

use crate::services::basic_auth_service::axum_basic_auth;
use crate::services::http_response::DashboardResponse;
use crate::services::{
    cron_service::{self, CronActionRequest},
    metrics_service::{self, MetricsQuery},
};
use crate::utils::pagination::PaginationQuery;

/// Convert the neutral dashboard response into an Axum response.
impl IntoResponse for DashboardResponse {
    fn into_response(self) -> Response {
        let status =
            StatusCode::from_u16(self.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
        let content_type = HeaderValue::from_str(&self.content_type)
            .unwrap_or_else(|_| HeaderValue::from_static("application/octet-stream"));

        let mut builder = Response::builder()
            .status(status)
            .header(header::CONTENT_TYPE, content_type);
        for (name, value) in &self.headers {
            builder = builder.header(name.as_str(), value.as_str());
        }
        builder
            .body(Body::from(self.body))
            .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response())
    }
}

// --- Handlers -------------------------------------------------------------

async fn render_metrics(Query(query): Query<MetricsQuery>) -> DashboardResponse {
    metrics_service::render_metrics(query).await
}

async fn render_metrics_for_queue(
    Path(queue): Path<String>,
    Query(query): Query<PaginationQuery>,
) -> DashboardResponse {
    metrics_service::render_metrics_for_queue(queue, query).await
}

async fn render_dead_jobs(Query(query): Query<PaginationQuery>) -> DashboardResponse {
    metrics_service::render_dead_jobs(query).await
}

async fn render_delayed_jobs(Query(query): Query<PaginationQuery>) -> DashboardResponse {
    metrics_service::render_delayed_jobs(query).await
}

async fn render_scheduled_jobs(Query(query): Query<PaginationQuery>) -> DashboardResponse {
    metrics_service::render_scheduled_jobs(query).await
}

async fn export_queue_csv(Path(queue): Path<String>) -> DashboardResponse {
    metrics_service::export_queue_csv(queue).await
}

async fn get_metrics_summary() -> DashboardResponse {
    metrics_service::get_metrics_summary().await
}

async fn job_action(Json(payload): Json<serde_json::Value>) -> DashboardResponse {
    metrics_service::job_action(payload).await
}

async fn render_failed_jobs(Query(query): Query<PaginationQuery>) -> DashboardResponse {
    metrics_service::render_failed_jobs(query).await
}

async fn render_retry_jobs(Query(query): Query<PaginationQuery>) -> DashboardResponse {
    metrics_service::render_retry_jobs(query).await
}

async fn render_cron_jobs() -> DashboardResponse {
    cron_service::render_cron_jobs().await
}

async fn cron_action(Json(payload): Json<CronActionRequest>) -> DashboardResponse {
    cron_service::cron_action(payload).await
}

/// Build the QRush dashboard router.
///
/// Nest it under your app's mount point, e.g.:
///
/// ```ignore
/// let app = axum::Router::new().nest("/qrush", qrush::routes::axum_route::qrush_metrics_router());
/// ```
///
/// This yields the same paths as the Actix adapter (`/qrush/metrics/...`).
pub fn qrush_metrics_router() -> Router {
    let metrics = Router::new()
        .route("/health", get(|| async { "healthy" }))
        .route("/", get(render_metrics))
        .route("/queues/{queue}", get(render_metrics_for_queue))
        .route("/extras/dead", get(render_dead_jobs))
        .route("/extras/delayed", get(render_delayed_jobs))
        .route("/extras/scheduled", get(render_scheduled_jobs))
        .route("/queues/{queue}/export", get(export_queue_csv))
        .route("/extras/summary", get(get_metrics_summary))
        .route("/jobs/action", post(job_action))
        .route("/extras/cron", get(render_cron_jobs))
        .route("/cron/action", post(cron_action))
        .route("/extras/failed", get(render_failed_jobs))
        .route("/extras/retry", get(render_retry_jobs))
        .layer(middleware::from_fn(axum_basic_auth));

    Router::new().nest("/metrics", metrics)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn error_status_and_content_type_map() {
        let resp = DashboardResponse::json_status(404, &serde_json::json!({"error": "nope"}))
            .into_response();
        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
        assert_eq!(
            resp.headers().get("content-type").unwrap(),
            "application/json"
        );
    }

    #[test]
    fn csv_disposition_header_maps() {
        let resp = DashboardResponse::csv(b"a,b\n".to_vec(), "queue_x.csv").into_response();
        assert_eq!(resp.status(), StatusCode::OK);
        assert_eq!(resp.headers().get("content-type").unwrap(), "text/csv");
        assert!(resp
            .headers()
            .get("content-disposition")
            .unwrap()
            .to_str()
            .unwrap()
            .contains("queue_x.csv"));
    }
}