use std::any;
use std::error::Error;
use thiserror::Error;
use crate::api::PaginationError;
use crate::types::ServiceType;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum BodyError {
#[error("failed to URL encode form parameters: {}", source)]
UrlEncoded {
#[from]
source: serde_urlencoded::ser::Error,
},
#[error("failed to serialize request body: {}", source)]
Serialize {
#[from]
source: serde_json::Error,
},
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ApiError<E>
where
E: Error + Send + Sync + 'static,
{
#[error("client error: {}", source)]
Client {
source: E,
},
#[error("failed to parse url: {}", source)]
UrlParse {
#[from]
source: url::ParseError,
},
#[error("failed to create form data: {}", source)]
Body {
#[from]
source: BodyError,
},
#[error("could not parse JSON response: {}", source)]
Json {
#[from]
source: serde_json::Error,
},
#[error("resource not found")]
ResourceNotFound,
#[error("cannot uniqly find resource by identifier")]
IdNotUnique,
#[error("openstack session error: {}", msg)]
Session {
msg: String,
},
#[error("openstack server error: {}", msg)]
OpenStack {
status: http::StatusCode,
msg: String,
},
#[error("openstack internal server error {}", status)]
OpenStackService {
status: http::StatusCode,
data: String,
},
#[error("openstack server error: {:?}", obj)]
OpenStackUnrecognized {
status: http::StatusCode,
obj: serde_json::Value,
},
#[error("could not parse {} data from JSON: {}", typename, source)]
DataType {
source: serde_json::Error,
typename: &'static str,
},
#[error("failed to handle for pagination: {}", source)]
Pagination {
#[from]
source: PaginationError,
},
}
impl<E> ApiError<E>
where
E: Error + Send + Sync + 'static,
{
pub fn client(source: E) -> Self {
ApiError::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::Body { source } => ApiError::Body { source },
Self::Json { source } => ApiError::Json { source },
Self::OpenStack { status, msg } => ApiError::OpenStack { status, msg },
Self::Session { msg } => ApiError::Session { msg },
Self::OpenStackService { status, data } => ApiError::OpenStackService { status, data },
Self::ResourceNotFound => ApiError::ResourceNotFound,
Self::IdNotUnique => ApiError::IdNotUnique,
Self::OpenStackUnrecognized { status, obj } => {
ApiError::OpenStackUnrecognized { status, obj }
}
Self::DataType { source, typename } => ApiError::DataType { source, typename },
Self::Pagination { source } => ApiError::Pagination { source },
}
}
pub(crate) fn server_error(status: http::StatusCode, body: &bytes::Bytes) -> Self {
if http::StatusCode::NOT_FOUND.as_u16() == status {
return ApiError::ResourceNotFound;
};
Self::OpenStackService {
status,
data: String::from_utf8_lossy(body).into(),
}
}
pub(crate) fn from_openstack(status: http::StatusCode, value: serde_json::Value) -> Self {
if http::StatusCode::NOT_FOUND.as_u16() == status {
return ApiError::ResourceNotFound;
};
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() {
ApiError::OpenStack {
status,
msg: msg.into(),
}
} else {
ApiError::OpenStackUnrecognized {
status,
obj: error_value.clone(),
}
}
} else {
ApiError::OpenStackUnrecognized { status, obj: value }
}
}
pub(crate) fn data_type<T>(source: serde_json::Error) -> Self {
ApiError::DataType {
source,
typename: any::type_name::<T>(),
}
}
pub(crate) fn endpoint(service_type: &ServiceType) -> Self {
ApiError::Session {
msg: format!("No Endpoint for service `{}`", service_type),
}
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use thiserror::Error;
use crate::api::ApiError;
#[derive(Debug, Error)]
#[error("my error")]
enum MyError {}
#[test]
fn openstack_error_error() {
let obj = json!({
"error": "error contents",
});
let err: ApiError<MyError> =
ApiError::from_openstack(http::StatusCode::CONFLICT, obj.clone());
if let ApiError::OpenStack { status, msg } = err {
assert_eq!(msg, "error contents");
assert_eq!(status, http::StatusCode::CONFLICT);
} else {
panic!("unexpected error: {}", err);
}
}
#[test]
fn openstack_error_message_string() {
let obj = json!({
"message": "error contents",
});
let err: ApiError<MyError> =
ApiError::from_openstack(http::StatusCode::CONFLICT, obj.clone());
if let ApiError::OpenStack { status, msg } = err {
assert_eq!(msg, "error contents");
assert_eq!(status, http::StatusCode::CONFLICT);
} else {
panic!("unexpected error: {}", err);
}
}
#[test]
fn openstack_error_message_object() {
let err_obj = json!({
"blah": "foo",
});
let obj = json!({
"message": err_obj,
});
let err: ApiError<MyError> =
ApiError::from_openstack(http::StatusCode::CONFLICT, obj.clone());
if let ApiError::OpenStackUnrecognized { status, obj } = err {
assert_eq!(obj, err_obj);
assert_eq!(status, http::StatusCode::CONFLICT);
} else {
panic!("unexpected error: {}", err);
}
}
#[test]
fn openstack_error_message_unrecognized() {
let err_obj = json!({
"some_weird_key": "an even weirder value",
});
let err: ApiError<MyError> =
ApiError::from_openstack(http::StatusCode::CONFLICT, err_obj.clone());
if let ApiError::OpenStackUnrecognized { status, obj } = err {
assert_eq!(obj, err_obj);
assert_eq!(status, http::StatusCode::CONFLICT);
} else {
panic!("unexpected error: {}", err);
}
}
#[test]
fn openstack_error_not_found() {
let err_obj = json!({
"some_weird_key": "an even weirder value",
});
let err: ApiError<MyError> =
ApiError::from_openstack(http::StatusCode::NOT_FOUND, err_obj.clone());
if !matches!(err, ApiError::ResourceNotFound) {
panic!("unexpected error: {}", err);
}
}
}