#![deny(missing_docs)]
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Network error: {0}")]
Network(String),
#[error("HTTP body parse error: {0}")]
BodyParse(String),
#[error("HTTP error {status}: {message}")]
Http {
status: u16,
message: String,
},
#[error("Notion request parameter error: {0}")]
RequestParameter(String),
#[error("Serialization/Deserialization error: {0}")]
SerdeJson(#[from] serde_json::Error),
#[error("Serialization/Deserialization error: {0}")]
SerdeUrlEncodedSerialize(#[from] serde_urlencoded::ser::Error),
}
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ApiErrorCode {
InvalidJson,
InvalidRequestUrl,
InvalidRequest,
Unauthorized,
RestrictedResource,
ValidationError,
ObjectNotFound,
ConflictError,
RateLimited,
InternalServerError,
ServiceUnavailable,
GatewayTimeout,
#[serde(untagged)]
Unknown(String),
}
impl std::fmt::Display for ApiErrorCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ApiErrorCode::InvalidJson => write!(f, "invalid_json"),
ApiErrorCode::InvalidRequestUrl => write!(f, "invalid_request_url"),
ApiErrorCode::InvalidRequest => write!(f, "invalid_request"),
ApiErrorCode::Unauthorized => write!(f, "unauthorized"),
ApiErrorCode::RestrictedResource => write!(f, "restricted_resource"),
ApiErrorCode::ValidationError => write!(f, "validation_error"),
ApiErrorCode::ObjectNotFound => write!(f, "object_not_found"),
ApiErrorCode::ConflictError => write!(f, "conflict_error"),
ApiErrorCode::RateLimited => write!(f, "rate_limited"),
ApiErrorCode::InternalServerError => write!(f, "internal_server_error"),
ApiErrorCode::ServiceUnavailable => write!(f, "service_unavailable"),
ApiErrorCode::GatewayTimeout => write!(f, "gateway_timeout"),
ApiErrorCode::Unknown(code) => write!(f, "{}", code),
}
}
}
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct ErrorResponse {
pub object: String,
pub status: u16,
pub code: ApiErrorCode,
pub message: String,
pub request_id: Option<String>,
pub developer_survey: Option<String>,
}
impl Error {
pub(crate) async fn try_from_response_async(response: reqwest::Response) -> Self {
let status = response.status().as_u16();
let error_body = match response.text().await{
Err(_) =>{
return crate::error::Error::Http {
status,
message: "An error occurred, but failed to retrieve the error details from the response body.".to_string(),
}},
Ok(body) => body
};
let error_json = serde_json::from_str::<crate::error::ErrorResponse>(&error_body).ok();
let error_message = match error_json {
Some(e) => e.message,
None => format!("{:?}", error_body),
};
crate::error::Error::Http {
status,
message: error_message,
}
}
}
#[cfg(test)]
mod unit_tests {
use super::*;
#[test]
fn deserialize_api_error_code_gateway_timeout() {
let json = r#""gateway_timeout""#;
let code: ApiErrorCode = serde_json::from_str(json).unwrap();
assert_eq!(code, ApiErrorCode::GatewayTimeout);
}
#[test]
fn deserialize_api_error_code_known_codes() {
let test_cases = vec![
(r#""invalid_json""#, ApiErrorCode::InvalidJson),
(r#""invalid_request_url""#, ApiErrorCode::InvalidRequestUrl),
(r#""invalid_request""#, ApiErrorCode::InvalidRequest),
(r#""unauthorized""#, ApiErrorCode::Unauthorized),
(r#""restricted_resource""#, ApiErrorCode::RestrictedResource),
(r#""validation_error""#, ApiErrorCode::ValidationError),
(r#""object_not_found""#, ApiErrorCode::ObjectNotFound),
(r#""conflict_error""#, ApiErrorCode::ConflictError),
(r#""rate_limited""#, ApiErrorCode::RateLimited),
(
r#""internal_server_error""#,
ApiErrorCode::InternalServerError,
),
(
r#""service_unavailable""#,
ApiErrorCode::ServiceUnavailable,
),
(r#""gateway_timeout""#, ApiErrorCode::GatewayTimeout),
];
for (json, expected) in test_cases {
let code: ApiErrorCode = serde_json::from_str(json).unwrap();
assert_eq!(code, expected);
}
}
#[test]
fn deserialize_api_error_code_unknown() {
let json = r#""some_future_error_code""#;
let code: ApiErrorCode = serde_json::from_str(json).unwrap();
assert_eq!(
code,
ApiErrorCode::Unknown("some_future_error_code".to_string())
);
}
#[test]
fn serialize_api_error_code() {
let json = serde_json::to_string(&ApiErrorCode::GatewayTimeout).unwrap();
assert_eq!(json, r#""gateway_timeout""#);
}
#[test]
fn deserialize_error_response_with_gateway_timeout() {
let json = r#"
{
"object": "error",
"status": 504,
"code": "gateway_timeout",
"message": "The request timed out.",
"request_id": "abc123"
}
"#;
let error: ErrorResponse = serde_json::from_str(json).unwrap();
assert_eq!(error.status, 504);
assert_eq!(error.code, ApiErrorCode::GatewayTimeout);
assert_eq!(error.message, "The request timed out.");
}
#[test]
fn api_error_code_display() {
assert_eq!(ApiErrorCode::GatewayTimeout.to_string(), "gateway_timeout");
assert_eq!(
ApiErrorCode::InternalServerError.to_string(),
"internal_server_error"
);
assert_eq!(
ApiErrorCode::Unknown("custom".to_string()).to_string(),
"custom"
);
}
}