pub(crate) mod crud;
pub(crate) mod describe;
pub(crate) mod envelope;
pub(crate) mod fault;
pub(crate) mod misc;
pub(crate) mod parse;
pub(crate) mod query;
pub(crate) mod typed;
pub(crate) mod types;
pub use fault::SoapFault;
#[cfg(feature = "bench-internals")]
#[doc(hidden)]
pub mod bench_hooks {
pub use super::envelope::escape_text;
pub use super::fault::parse_fault;
pub fn records_to_typed<T: serde::de::DeserializeOwned>(
records: &[super::SObject],
) -> crate::error::Result<Vec<T>> {
super::typed::records_to_typed(records)
}
}
pub use types::{
DeleteResult, DescribeGlobalResult, DescribeGlobalSObject, DescribeSObjectResult,
FieldDescribe, PicklistEntry, QueryResult, SObject, SaveResult, SearchResult, SoapError,
UpsertResult, UserInfo,
};
use crate::error::{HttpError, Result};
use std::sync::Arc;
const CONTENT_TYPE: &str = "text/xml; charset=UTF-8";
const SOAP_ACTION: &str = "\"\"";
const BODY_CAP_BYTES: usize = 100 * 1024 * 1024;
const DEFAULT_CLIENT: &str = "force-rs";
enum Dispatch {
Ok(String),
Fault(SoapFault),
InvalidSession(SoapFault),
}
#[derive(Debug)]
pub struct SoapHandler<A: crate::auth::Authenticator> {
inner: Arc<crate::session::Session<A>>,
client: Option<String>,
}
impl<A: crate::auth::Authenticator> Clone for SoapHandler<A> {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
client: self.client.clone(),
}
}
}
impl<A: crate::auth::Authenticator> SoapHandler<A> {
#[must_use]
pub(crate) fn new(inner: Arc<crate::session::Session<A>>) -> Self {
Self {
inner,
client: Some(DEFAULT_CLIENT.to_string()),
}
}
#[must_use]
pub fn with_client(mut self, client: Option<impl Into<String>>) -> Self {
self.client = client.map(Into::into);
self
}
fn soap_url(&self, instance_url: &str) -> String {
let version = self.inner.config.api_version.trim_start_matches('v');
let base = instance_url.trim_end_matches('/');
let mut url = String::with_capacity(base.len() + version.len() + 16);
url.push_str(base);
url.push_str("/services/Soap/u/");
url.push_str(version);
url
}
pub(crate) async fn send(&self, body: &str) -> Result<String> {
match self.dispatch(body).await? {
Dispatch::Ok(text) => Ok(text),
Dispatch::Fault(fault) => Err(fault.into()),
Dispatch::InvalidSession(_) => {
self.inner.token_manager.force_refresh().await?;
match self.dispatch(body).await? {
Dispatch::Ok(text) => Ok(text),
Dispatch::Fault(fault) | Dispatch::InvalidSession(fault) => Err(fault.into()),
}
}
}
}
async fn dispatch(&self, body: &str) -> Result<Dispatch> {
let token = self.inner.token_manager.get_token_arc().await?;
let url = self.soap_url(token.instance_url());
let envelope = envelope::build_envelope(token.as_str(), self.client.as_deref(), body);
let request = self
.inner
.post(&url)
.header(reqwest::header::CONTENT_TYPE, CONTENT_TYPE)
.header("SOAPAction", SOAP_ACTION)
.body(envelope)
.build()
.map_err(HttpError::from)?;
let response = self.inner.execute_request(request).await?;
let status = response.status();
let text = crate::http::error::read_capped_body(response, BODY_CAP_BYTES).await?;
if let Some(fault) = fault::parse_fault(&text) {
if fault.is_invalid_session() {
return Ok(Dispatch::InvalidSession(fault));
}
return Ok(Dispatch::Fault(fault));
}
if !status.is_success() {
return Err(HttpError::StatusError {
status_code: status.as_u16(),
message: text,
}
.into());
}
Ok(Dispatch::Ok(text))
}
}
#[cfg(test)]
mod tests;