use cirrus_auth::AuthError;
use thiserror::Error;
pub type MetadataResult<T> = Result<T, MetadataError>;
#[derive(Debug, Clone)]
pub struct SoapFault {
pub faultcode: String,
pub faultstring: String,
}
impl SoapFault {
pub fn code(&self) -> &str {
self.faultcode
.rsplit_once(':')
.map(|(_, local)| local)
.unwrap_or(&self.faultcode)
}
pub(crate) fn is_invalid_session(&self) -> bool {
self.code() == "INVALID_SESSION_ID"
}
}
#[derive(Debug, Error)]
pub enum MetadataError {
#[error("missing required builder field: {0}")]
MissingField(&'static str),
#[error("failed to construct HTTP client: {0}")]
HttpClient(#[source] reqwest::Error),
#[error("HTTP request failed: {0}")]
Http(#[from] reqwest::Error),
#[error("Metadata API SOAP fault (status {status}) [{}]: {}", .fault.code(), .fault.faultstring)]
Soap {
status: u16,
fault: SoapFault,
},
#[error("HTTP {status} from Metadata API (non-SOAP body): {raw}")]
Http4xx5xx {
status: u16,
raw: String,
},
#[error(transparent)]
Auth(#[from] AuthError),
#[error("XML error: {0}")]
Xml(String),
#[error("invalid header value: {0}")]
InvalidHeader(String),
#[error("invalid response: {0}")]
InvalidResponse(String),
#[error("invalid argument: {0}")]
InvalidArgument(String),
#[error("polling timed out: {0}")]
PollTimeout(String),
}
impl From<quick_xml::Error> for MetadataError {
fn from(e: quick_xml::Error) -> Self {
MetadataError::Xml(e.to_string())
}
}
impl From<quick_xml::DeError> for MetadataError {
fn from(e: quick_xml::DeError) -> Self {
MetadataError::Xml(e.to_string())
}
}
impl From<std::io::Error> for MetadataError {
fn from(e: std::io::Error) -> Self {
MetadataError::Xml(e.to_string())
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn fault_code_strips_namespace_prefix() {
let f = SoapFault {
faultcode: "sf:INVALID_SESSION_ID".into(),
faultstring: "session expired".into(),
};
assert_eq!(f.code(), "INVALID_SESSION_ID");
assert!(f.is_invalid_session());
}
#[test]
fn fault_code_passes_through_when_unqualified() {
let f = SoapFault {
faultcode: "INVALID_TYPE".into(),
faultstring: "no such type".into(),
};
assert_eq!(f.code(), "INVALID_TYPE");
assert!(!f.is_invalid_session());
}
#[test]
fn soap_error_display_includes_code_and_message() {
let err = MetadataError::Soap {
status: 500,
fault: SoapFault {
faultcode: "sf:INVALID_TYPE".into(),
faultstring: "no such metadata type".into(),
},
};
let msg = err.to_string();
assert!(msg.contains("500"));
assert!(msg.contains("INVALID_TYPE"));
assert!(msg.contains("no such metadata type"));
}
}