mod envelope;
mod error;
pub mod handlers;
mod package_manifest;
pub mod result;
pub mod retry;
mod transport;
pub use cirrus_auth as auth;
pub use auth::{AuthError, AuthSession, SharedAuth};
pub use error::{MetadataError, MetadataResult, SoapFault};
pub use handlers::file_based::WaitConfig;
pub use package_manifest::{MetadataType, PackageManifest};
pub use result::{
AsyncRequestState, AsyncResult, CancelDeployResult, CodeCoverageResult, CodeCoverageWarning,
DeleteResult, DeployDetails, DeployMessage, DeployOptions, DeployProblemType, DeployResult,
DeployStatus, DescribeMetadataObject, DescribeMetadataResult, DescribeValueTypeResult,
FileProperties, ListMetadataQuery, ManageableState, MetadataApiError, PicklistEntry,
RetrieveMessage, RetrieveRequest, RetrieveResult, RetrieveStatus, RunTestFailure,
RunTestSuccess, RunTestsResult, SaveResult, TestLevel, UpsertResult, ValueTypeField,
};
pub use retry::RetryPolicy;
pub use transport::SoapOperation;
pub const DEFAULT_API_VERSION: &str = "66.0";
pub(crate) const DEFAULT_USER_AGENT: &str = concat!(
"cirrus-metadata/",
env!("CARGO_PKG_VERSION"),
" (Rust SDK for Salesforce Metadata API)"
);
use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT};
#[derive(Clone)]
pub struct MetadataClient {
pub(crate) http: reqwest::Client,
pub(crate) auth: SharedAuth,
pub(crate) api_version: String,
pub(crate) retry_policy: RetryPolicy,
}
impl std::fmt::Debug for MetadataClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MetadataClient")
.field("api_version", &self.api_version)
.field("instance_url", &self.auth.instance_url())
.field("retry_policy", &self.retry_policy)
.finish_non_exhaustive()
}
}
impl MetadataClient {
pub fn builder() -> MetadataClientBuilder {
MetadataClientBuilder::default()
}
pub fn api_version(&self) -> &str {
&self.api_version
}
pub fn http_client(&self) -> &reqwest::Client {
&self.http
}
pub fn auth(&self) -> &SharedAuth {
&self.auth
}
pub fn retry_policy(&self) -> &RetryPolicy {
&self.retry_policy
}
pub fn endpoint_url(&self) -> String {
format!(
"{}/services/Soap/m/{}",
self.auth.instance_url(),
self.api_version
)
}
pub fn request_builder(&self) -> reqwest::RequestBuilder {
self.http
.post(self.endpoint_url())
.header(reqwest::header::CONTENT_TYPE, "text/xml; charset=UTF-8")
.header("SOAPAction", "\"\"")
}
pub async fn call<O: SoapOperation>(&self, op: &O) -> MetadataResult<O::Response> {
transport::soap_call(self, op).await
}
}
#[derive(Default)]
pub struct MetadataClientBuilder {
auth: Option<SharedAuth>,
api_version: Option<String>,
user_agent: Option<String>,
http_client: Option<reqwest::Client>,
retry_policy: Option<RetryPolicy>,
}
impl MetadataClientBuilder {
pub fn auth(mut self, auth: SharedAuth) -> Self {
self.auth = Some(auth);
self
}
pub fn api_version(mut self, version: impl Into<String>) -> Self {
self.api_version = Some(version.into());
self
}
pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
self.user_agent = Some(ua.into());
self
}
pub fn http_client(mut self, client: reqwest::Client) -> Self {
self.http_client = Some(client);
self
}
pub fn retry_policy(mut self, policy: RetryPolicy) -> Self {
self.retry_policy = Some(policy);
self
}
pub fn build(self) -> MetadataResult<MetadataClient> {
let auth = self.auth.ok_or(MetadataError::MissingField("auth"))?;
let http = if let Some(c) = self.http_client {
c
} else {
let ua = self.user_agent.as_deref().unwrap_or(DEFAULT_USER_AGENT);
let mut headers = HeaderMap::new();
headers.insert(
USER_AGENT,
HeaderValue::from_str(ua)
.map_err(|e| MetadataError::InvalidHeader(e.to_string()))?,
);
reqwest::Client::builder()
.default_headers(headers)
.build()
.map_err(MetadataError::HttpClient)?
};
Ok(MetadataClient {
http,
auth,
api_version: self
.api_version
.unwrap_or_else(|| DEFAULT_API_VERSION.to_string()),
retry_policy: self.retry_policy.unwrap_or_default(),
})
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use std::sync::Arc;
#[test]
fn builder_requires_auth() {
let err = MetadataClient::builder().build().unwrap_err();
assert!(matches!(err, MetadataError::MissingField("auth")));
}
#[test]
fn endpoint_url_uses_bare_version_no_v_prefix() {
let auth = Arc::new(auth::StaticTokenAuth::new(
"tok",
"https://my-org.my.salesforce.com",
));
let md = MetadataClient::builder().auth(auth).build().unwrap();
assert_eq!(
md.endpoint_url(),
"https://my-org.my.salesforce.com/services/Soap/m/66.0"
);
}
#[test]
fn endpoint_url_honors_custom_api_version() {
let auth = Arc::new(auth::StaticTokenAuth::new("tok", "https://x.example.com"));
let md = MetadataClient::builder()
.auth(auth)
.api_version("58.0")
.build()
.unwrap();
assert!(md.endpoint_url().ends_with("/services/Soap/m/58.0"));
}
#[test]
fn debug_redacts_auth_and_client() {
let auth = Arc::new(auth::StaticTokenAuth::new(
"secret-token",
"https://x.example.com",
));
let md = MetadataClient::builder().auth(auth).build().unwrap();
let dbg = format!("{md:?}");
assert!(!dbg.contains("secret-token"));
}
}