Radish alpha
r
rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5
Radicle web interface
Radicle
Git
radicle-explorer radicle-httpd src axum_extra.rs
use axum::extract::path::ErrorKind;
use axum::extract::rejection::{PathRejection, QueryRejection};
use axum::extract::FromRequestParts;
use axum::http::request::Parts;
use axum::http::{header, StatusCode};
use axum::response::IntoResponse;
use axum::Json;

use serde::de::DeserializeOwned;
use serde::Serialize;

pub struct Path<T>(pub T);

impl<S, T> FromRequestParts<S> for Path<T>
where
    T: DeserializeOwned + Send,
    S: Send + Sync,
{
    type Rejection = (StatusCode, axum::Json<Error>);

    async fn from_request_parts(req: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
        match axum::extract::Path::<T>::from_request_parts(req, state).await {
            Ok(value) => Ok(Self(value.0)),
            Err(rejection) => {
                let status = StatusCode::BAD_REQUEST;
                let body = match rejection {
                    PathRejection::FailedToDeserializePathParams(inner) => {
                        let kind = inner.into_kind();
                        match &kind {
                            ErrorKind::Message(msg) => Json(Error {
                                success: false,
                                error: msg.to_string(),
                            }),
                            _ => Json(Error {
                                success: false,
                                error: kind.to_string(),
                            }),
                        }
                    }
                    _ => Json(Error {
                        success: false,
                        error: format!("{rejection}"),
                    }),
                };

                Err((status, body))
            }
        }
    }
}

#[derive(Default)]
pub struct Query<T>(pub T);

impl<S, T> FromRequestParts<S> for Query<T>
where
    T: DeserializeOwned + Send,
    S: Send + Sync,
{
    type Rejection = (StatusCode, axum::Json<Error>);

    async fn from_request_parts(req: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
        match axum::extract::Query::<T>::from_request_parts(req, state).await {
            Ok(value) => Ok(Self(value.0)),
            Err(rejection) => {
                let status = StatusCode::BAD_REQUEST;
                let body = match rejection {
                    QueryRejection::FailedToDeserializeQueryString(inner) => Json(Error {
                        success: false,
                        error: inner.to_string(),
                    }),
                    _ => Json(Error {
                        success: false,
                        error: format!("{rejection}"),
                    }),
                };

                Err((status, body))
            }
        }
    }
}

#[derive(Serialize)]
pub struct Error {
    success: bool,
    error: String,
}

/// Add a Cache-Control header that marks the response as immutable and
/// instructs clients to cache the response for 7 days.
pub fn immutable_response(data: impl serde::Serialize) -> impl IntoResponse {
    (
        [(header::CACHE_CONTROL, "public, max-age=604800, immutable")],
        Json(data),
    )
}

/// Add a Cache-Control header that marks the response as must-revalidate and
/// instructs clients to cache the response for `max_age_seconds` .
pub fn cached_response(data: impl serde::Serialize, max_age_in_seconds: u64) -> impl IntoResponse {
    (
        [(
            header::CACHE_CONTROL,
            format!("public, max-age={max_age_in_seconds}, must-revalidate"),
        )],
        Json(data),
    )
}