use std::fmt;
#[derive(Debug, thiserror::Error)]
pub enum ForceError {
#[error("authentication failed: {0}")]
Authentication(#[from] AuthenticationError),
#[error("HTTP request failed: {0}")]
Http(#[from] HttpError),
#[error("Salesforce API error: {0}")]
Api(#[from] ApiError),
#[error("configuration error: {0}")]
Config(#[from] ConfigError),
#[error("serialization error: {0}")]
Serialization(#[from] SerializationError),
#[error("invalid Salesforce ID: {0}")]
InvalidId(#[from] crate::types::salesforce_id::SalesforceIdError),
#[error("invalid input: {0}")]
InvalidInput(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("not implemented: {0}")]
NotImplemented(String),
#[cfg(feature = "graphql")]
#[error("GraphQL error: {0}")]
GraphQL(#[from] crate::api::graphql::GraphqlErrorResponse),
#[cfg(feature = "cpq")]
#[error("CPQ error: {0}")]
Cpq(#[from] crate::api::cpq::CpqErrorResponse),
}
#[derive(Debug, thiserror::Error)]
pub enum AuthenticationError {
#[error("OAuth token request failed: {0}")]
TokenRequestFailed(String),
#[error("invalid credentials: {0}")]
InvalidCredentials(String),
#[error("access token has expired")]
TokenExpired,
#[error("failed to refresh access token: {0}")]
TokenRefreshFailed(String),
#[error("invalid token state")]
InvalidToken,
#[cfg(feature = "jwt")]
#[error("JWT token creation failed: {0}")]
JwtCreationFailed(String),
#[cfg(feature = "jwt")]
#[error("invalid JWT configuration: {0}")]
InvalidJwtConfig(String),
}
#[derive(Debug, thiserror::Error)]
pub enum HttpError {
#[error("network request failed: {0}")]
RequestFailed(#[from] reqwest::Error),
#[error("HTTP {status_code}: {message}")]
StatusError {
status_code: u16,
message: String,
},
#[error("rate limit exceeded, retry after {retry_after_seconds} seconds")]
RateLimitExceeded {
retry_after_seconds: u64,
},
#[error("request timeout after {timeout_seconds} seconds")]
Timeout {
timeout_seconds: u64,
},
#[error("invalid URL: {0}")]
InvalidUrl(String),
#[error("response payload exceeded the safety limit of {limit_bytes} bytes")]
PayloadTooLarge {
limit_bytes: usize,
},
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, thiserror::Error)]
#[serde(rename_all = "camelCase")]
pub struct ApiError {
pub message: String,
#[serde(alias = "statusCode", alias = "errorCode")]
pub error_code: String,
#[serde(default)]
pub fields: Vec<String>,
}
impl ApiError {
#[must_use]
pub fn new(message: impl Into<String>, error_code: impl Into<String>) -> Self {
Self {
message: message.into(),
error_code: error_code.into(),
fields: Vec::new(),
}
}
#[must_use]
pub fn with_fields(
message: impl Into<String>,
error_code: impl Into<String>,
fields: Vec<String>,
) -> Self {
Self {
message: message.into(),
error_code: error_code.into(),
fields,
}
}
}
impl fmt::Display for ApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}] {}", self.error_code, self.message)?;
if !self.fields.is_empty() {
write!(f, " (fields: ")?;
for (i, field) in self.fields.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}", field)?;
}
write!(f, ")")?;
}
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("missing required configuration: {0}")]
MissingValue(String),
#[error("invalid configuration value for {field}: {reason}")]
InvalidValue {
field: String,
reason: String,
},
#[error("environment variable error: {0}")]
EnvVar(#[from] std::env::VarError),
}
#[derive(Debug, thiserror::Error)]
pub enum SerializationError {
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[cfg(feature = "bulk")]
#[error("CSV error: {0}")]
Csv(#[from] csv::Error),
#[error("invalid data format: {0}")]
InvalidFormat(String),
}
pub type Result<T> = std::result::Result<T, ForceError>;
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::Must;
#[test]
fn test_authentication_error_display() {
let err = AuthenticationError::TokenRequestFailed("invalid_grant".to_string());
assert_eq!(err.to_string(), "OAuth token request failed: invalid_grant");
}
#[test]
fn test_authentication_error_token_expired() {
let err = AuthenticationError::TokenExpired;
assert_eq!(err.to_string(), "access token has expired");
}
#[test]
fn test_http_error_status() {
let err = HttpError::StatusError {
status_code: 404,
message: "Resource not found".to_string(),
};
assert_eq!(err.to_string(), "HTTP 404: Resource not found");
}
#[test]
fn test_http_error_rate_limit() {
let err = HttpError::RateLimitExceeded {
retry_after_seconds: 60,
};
assert_eq!(
err.to_string(),
"rate limit exceeded, retry after 60 seconds"
);
}
#[test]
fn test_http_error_timeout() {
let err = HttpError::Timeout {
timeout_seconds: 30,
};
assert_eq!(err.to_string(), "request timeout after 30 seconds");
}
#[test]
fn test_api_error_display() {
let err = ApiError {
error_code: "INVALID_FIELD".to_string(),
message: "Field does not exist".to_string(),
fields: vec!["Account.InvalidField".to_string()],
};
assert_eq!(
err.to_string(),
"[INVALID_FIELD] Field does not exist (fields: Account.InvalidField)"
);
}
#[test]
fn test_api_error_no_fields() {
let err = ApiError {
error_code: "UNKNOWN_ERROR".to_string(),
message: "An unknown error occurred".to_string(),
fields: vec![],
};
assert_eq!(err.to_string(), "[UNKNOWN_ERROR] An unknown error occurred");
}
#[test]
fn test_config_error_missing_value() {
let err = ConfigError::MissingValue("client_id".to_string());
assert_eq!(err.to_string(), "missing required configuration: client_id");
}
#[test]
fn test_config_error_invalid_value() {
let err = ConfigError::InvalidValue {
field: "api_version".to_string(),
reason: "must be in format vXX.0".to_string(),
};
assert_eq!(
err.to_string(),
"invalid configuration value for api_version: must be in format vXX.0"
);
}
#[test]
fn test_serialization_error_invalid_format() {
let err = SerializationError::InvalidFormat("expected ISO 8601 date".to_string());
assert_eq!(
err.to_string(),
"invalid data format: expected ISO 8601 date"
);
}
#[test]
fn test_force_error_from_authentication() {
let auth_err = AuthenticationError::TokenExpired;
let force_err: ForceError = auth_err.into();
assert_eq!(
force_err.to_string(),
"authentication failed: access token has expired"
);
}
#[test]
fn test_force_error_from_http() {
let http_err = HttpError::InvalidUrl("not a valid url".to_string());
let force_err: ForceError = http_err.into();
assert_eq!(
force_err.to_string(),
"HTTP request failed: invalid URL: not a valid url"
);
}
#[test]
fn test_force_error_from_api() {
let api_err = ApiError {
error_code: "REQUIRED_FIELD_MISSING".to_string(),
message: "Name is required".to_string(),
fields: vec!["Name".to_string()],
};
let force_err: ForceError = api_err.into();
assert_eq!(
force_err.to_string(),
"Salesforce API error: [REQUIRED_FIELD_MISSING] Name is required (fields: Name)"
);
}
#[test]
fn test_force_error_from_config() {
let config_err = ConfigError::MissingValue("instance_url".to_string());
let force_err: ForceError = config_err.into();
assert_eq!(
force_err.to_string(),
"configuration error: missing required configuration: instance_url"
);
}
#[test]
fn test_force_error_from_serialization() {
let ser_err = SerializationError::InvalidFormat("malformed JSON".to_string());
let force_err: ForceError = ser_err.into();
assert_eq!(
force_err.to_string(),
"serialization error: invalid data format: malformed JSON"
);
}
#[test]
fn test_result_type_ok() {
let result: Result<i32> = Ok(42);
assert!(result.is_ok());
assert_eq!(result.must(), 42);
}
#[test]
fn test_result_type_err() {
let result: Result<i32> = Err(ForceError::Authentication(
AuthenticationError::TokenExpired,
));
let Err(err) = result else {
panic!("Expected an error");
};
assert!(err.to_string().contains(""));
}
#[cfg(feature = "jwt")]
#[test]
fn test_jwt_error_creation_failed() {
let err = AuthenticationError::JwtCreationFailed("invalid key".to_string());
assert_eq!(err.to_string(), "JWT token creation failed: invalid key");
}
#[cfg(feature = "jwt")]
#[test]
fn test_jwt_error_invalid_config() {
let err = AuthenticationError::InvalidJwtConfig("missing private key".to_string());
assert_eq!(
err.to_string(),
"invalid JWT configuration: missing private key"
);
}
#[cfg(feature = "bulk")]
#[test]
fn test_csv_error_conversion() {
let err = SerializationError::InvalidFormat("CSV test".to_string());
assert!(err.to_string().contains("invalid data format"));
}
#[test]
fn test_force_error_from_invalid_id_length() {
let id_err = crate::types::salesforce_id::SalesforceIdError::InvalidLength(10);
let force_err: ForceError = id_err.into();
assert_eq!(
force_err.to_string(),
"invalid Salesforce ID: invalid ID length: 10 (must be 15 or 18 characters)"
);
}
#[test]
fn test_force_error_from_invalid_id_characters() {
let id_err = crate::types::salesforce_id::SalesforceIdError::InvalidCharacters;
let force_err: ForceError = id_err.into();
assert_eq!(
force_err.to_string(),
"invalid Salesforce ID: ID contains invalid characters (must be alphanumeric)"
);
}
#[test]
fn test_force_error_from_invalid_id_checksum() {
let id_err = crate::types::salesforce_id::SalesforceIdError::InvalidChecksum;
let force_err: ForceError = id_err.into();
assert_eq!(
force_err.to_string(),
"invalid Salesforce ID: invalid checksum for 18-character ID"
);
}
#[test]
fn test_error_trait_implementations() {
fn assert_send_sync<T: Send + Sync + 'static>() {}
assert_send_sync::<ForceError>();
assert_send_sync::<AuthenticationError>();
assert_send_sync::<HttpError>();
assert_send_sync::<ApiError>();
assert_send_sync::<ConfigError>();
assert_send_sync::<SerializationError>();
}
}