//! Failures, in the shapes Radarr produces them. //! //! Jellyseerr surfaces the response body verbatim in its own logs and settings //! UI, so a validation failure that comes back in Radarr's shape is readable //! there without anyone knowing this is not Radarr. use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use axum::Json; use serde::Serialize; /// Everything the shim can answer with instead of a result. #[derive(Debug)] pub enum CompatError { /// The request was well-formed but names something that does not exist /// here — usually a root folder path Jellyseerr was configured with by /// hand. `property` is Radarr's field name, `PascalCase` as it sends it. Validation { /// Radarr's name for the offending field, e.g. `RootFolderPath`. property: &'static str, /// What is wrong with it, in a sentence. message: String, }, /// No such movie. NotFound, /// TMDB is unconfigured or would not answer. Upstream(String), /// The database would not answer. Database(String), } /// One entry of Radarr's validation-failure body. #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] struct ValidationFailure { property_name: &'static str, error_message: String, severity: &'static str, } /// Radarr's shape for everything that is not a validation failure. #[derive(Debug, Serialize)] struct Message { message: String, } impl IntoResponse for CompatError { fn into_response(self) -> Response { match self { // A list, not an object: Radarr's model binder reports every // failing field at once and Jellyseerr's client reads index 0. Self::Validation { property, message } => ( StatusCode::BAD_REQUEST, Json(vec![ValidationFailure { property_name: property, error_message: message, severity: "error", }]), ) .into_response(), Self::NotFound => ( StatusCode::NOT_FOUND, Json(Message { message: "NotFound".into(), }), ) .into_response(), Self::Upstream(detail) => { tracing::warn!(%detail, "compat shim upstream failure"); (StatusCode::BAD_GATEWAY, Json(Message { message: detail })).into_response() } Self::Database(detail) => { tracing::error!(%detail, "compat shim database error"); ( StatusCode::INTERNAL_SERVER_ERROR, Json(Message { message: "database error".into(), }), ) .into_response() } } } } impl From for CompatError { fn from(error: sqlx::Error) -> Self { if matches!(error, sqlx::Error::RowNotFound) { Self::NotFound } else { Self::Database(error.to_string()) } } } impl From for CompatError { fn from(error: arr_meta::Error) -> Self { Self::Upstream(error.to_string()) } }