eclaw 0.0.12

Personal AI assistant powered by Elph
use axum::{
    Json,
    body::Body,
    extract::rejection::{JsonRejection, PathRejection, QueryRejection},
    http::{Request, StatusCode},
    response::{IntoResponse, Response},
};
use serde::Serialize;

#[derive(Debug, Serialize)]
pub struct ErrorDetail {
    pub status: u16,
    pub message: String,
}

#[derive(Debug, Serialize)]
pub struct ErrorResponse {
    pub error: ErrorDetail,
}

impl ErrorResponse {
    pub fn new(status: StatusCode, message: impl Into<String>) -> Self {
        Self {
            error: ErrorDetail {
                status: status.as_u16(),
                message: message.into(),
            },
        }
    }

    pub fn into_response(self, status: StatusCode) -> Response {
        (status, [("content-type", "application/json")], Json(self)).into_response()
    }
}

#[derive(Debug)]
#[allow(dead_code)]
pub enum AppError {
    BadRequest(String),
    NotFound(String),
    #[allow(dead_code)]
    Internal(String),
}

impl AppError {
    fn status(&self) -> StatusCode {
        match self {
            Self::BadRequest(_) => StatusCode::BAD_REQUEST,
            Self::NotFound(_) => StatusCode::NOT_FOUND,
            Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
        }
    }

    fn message(&self) -> &str {
        match self {
            Self::BadRequest(message) | Self::NotFound(message) | Self::Internal(message) => message,
        }
    }
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        ErrorResponse::new(self.status(), self.message()).into_response(self.status())
    }
}

pub async fn not_found(req: Request<Body>) -> Response {
    ErrorResponse::new(StatusCode::NOT_FOUND, format!("route `{}` not found", req.uri().path()))
        .into_response(StatusCode::NOT_FOUND)
}

pub async fn method_not_allowed(req: Request<Body>) -> Response {
    ErrorResponse::new(
        StatusCode::METHOD_NOT_ALLOWED,
        format!("method `{}` not allowed for `{}`", req.method(), req.uri().path()),
    )
    .into_response(StatusCode::METHOD_NOT_ALLOWED)
}

#[allow(dead_code)]
pub async fn json_rejection(err: JsonRejection) -> Response {
    let (status, message) = match err {
        JsonRejection::JsonDataError(error) => (StatusCode::UNPROCESSABLE_ENTITY, error.to_string()),
        JsonRejection::JsonSyntaxError(error) => (StatusCode::BAD_REQUEST, error.to_string()),
        JsonRejection::MissingJsonContentType(_) => (
            StatusCode::UNSUPPORTED_MEDIA_TYPE,
            "expected application/json content-type".to_string(),
        ),
        _ => (StatusCode::BAD_REQUEST, "invalid json request".to_string()),
    };

    ErrorResponse::new(status, message).into_response(status)
}

#[allow(dead_code)]
pub async fn path_rejection(err: PathRejection) -> Response {
    ErrorResponse::new(StatusCode::BAD_REQUEST, err.to_string()).into_response(StatusCode::BAD_REQUEST)
}

#[allow(dead_code)]
pub async fn query_rejection(err: QueryRejection) -> Response {
    ErrorResponse::new(StatusCode::BAD_REQUEST, err.to_string()).into_response(StatusCode::BAD_REQUEST)
}

pub fn panic_response() -> Response {
    ErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR, "internal server error")
        .into_response(StatusCode::INTERNAL_SERVER_ERROR)
}