use super::endpoint::UrlBase;
use crate::auth::AuthError;
use std::error::Error;
use thiserror::Error;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum BodyError {
#[error("failed to URL encode form parameters: {0}")]
UrlEncoded(#[from] serde_urlencoded::ser::Error),
#[error("failed to JSON encode form parameters: {0}")]
JsonEncoded(#[from] serde_json::Error),
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ApiError<E>
where
E: Error + Send + Sync + 'static,
{
#[error("client error: {0}")]
Client(E),
#[error("failed to authenticate: {0}")]
Auth(#[from] AuthError),
#[error("failed to parse url: {0}")]
UrlParse(#[from] url::ParseError),
#[error("failed to create form data: {0}")]
Body(#[from] BodyError),
#[error("could not parse JSON response: {0}")]
Json(#[from] serde_json::Error),
#[error("moved permanently to: {}", location.as_ref().map_or("<UNKNOWN>", AsRef::as_ref))]
MovedPermanently {
location: Option<String>,
},
#[error("spotify internal server error {status}")]
SpotifyService {
status: http::StatusCode,
data: Vec<u8>,
},
#[error("could not parse {typename} data: {source}")]
DataType {
source: serde_json::Error,
typename: &'static str,
},
#[error("unsupported URL base: {0:?}")]
UnsupportedUrlBase(UrlBase),
#[error("spotify server error ({status}): {msg}")]
SpotifyWithStatus {
status: http::StatusCode,
msg: String,
},
#[error("spotify server error ({status}): {obj:?}")]
SpotifyObjectWithStatus {
status: http::StatusCode,
obj: serde_json::Value,
},
#[error("spotify server error ({status}): {obj:?}")]
SpotifyUnrecognizedWithStatus {
status: http::StatusCode,
obj: serde_json::Value,
},
}
impl<E> ApiError<E>
where
E: Error + Send + Sync + 'static,
{
pub fn client(source: E) -> Self {
Self::Client(source)
}
pub fn map_client<F, W>(self, f: F) -> ApiError<W>
where
F: FnOnce(E) -> W,
W: Error + Send + Sync + 'static,
{
match self {
Self::Client(source) => ApiError::client(f(source)),
Self::UrlParse(source) => ApiError::UrlParse(source),
Self::Auth(source) => ApiError::Auth(source),
Self::Body(source) => ApiError::Body(source),
Self::Json(source) => ApiError::Json(source),
Self::MovedPermanently { location } => ApiError::MovedPermanently { location },
Self::SpotifyWithStatus { status, msg } => ApiError::SpotifyWithStatus { status, msg },
Self::SpotifyService { status, data } => ApiError::SpotifyService { status, data },
Self::SpotifyObjectWithStatus { status, obj } => {
ApiError::SpotifyObjectWithStatus { status, obj }
}
Self::SpotifyUnrecognizedWithStatus { status, obj } => {
ApiError::SpotifyUnrecognizedWithStatus { status, obj }
}
Self::DataType { source, typename } => ApiError::DataType { source, typename },
Self::UnsupportedUrlBase(url_base) => ApiError::UnsupportedUrlBase(url_base),
}
}
pub(crate) fn moved_permanently(raw_location: Option<&http::HeaderValue>) -> Self {
let location = raw_location.map(|v| String::from_utf8_lossy(v.as_bytes()).into());
Self::MovedPermanently { location }
}
pub(crate) fn server_error(status: http::StatusCode, body: &bytes::Bytes) -> Self {
Self::SpotifyService {
status,
data: body.into_iter().copied().collect(),
}
}
pub(crate) fn from_spotify_with_status(
status: http::StatusCode,
value: serde_json::Value,
) -> Self {
let error_value = value
.pointer("/message")
.or_else(|| value.pointer("/error"));
if let Some(error_value) = error_value {
if let Some(msg) = error_value.as_str() {
Self::SpotifyWithStatus {
status,
msg: msg.into(),
}
} else {
Self::SpotifyObjectWithStatus {
status,
obj: error_value.clone(),
}
}
} else {
Self::SpotifyUnrecognizedWithStatus { status, obj: value }
}
}
pub(crate) fn data_type<T>(source: serde_json::Error) -> Self {
Self::DataType {
source,
typename: std::any::type_name::<T>(),
}
}
}