use crate::client;
use crate::http::HttpClientCache;
use std::sync::Arc;
use std::time::Duration;
use crate::{DEFAULT_API_VERSION, DEFAULT_CONNECT_TIMEOUT_SECS, DEFAULT_REQUEST_TIMEOUT_SECS};
use super::{CreateManagedEventSubscriptionRequest, CreateManagedEventSubscriptionResponse};
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum Error {
#[error("Authentication error: {source}")]
Auth {
#[source]
source: client::Error,
},
#[error("Communication error: {source}")]
Communication {
#[source]
source: reqwest::Error,
},
#[error("Serialization error: {source}")]
Serialization {
#[source]
source: serde_json::Error,
},
#[error("Tooling API error: {message} (status: {status})")]
ApiError { status: u16, message: String },
#[error("Instance URL not available: call connect() on the auth client first")]
MissingInstanceUrl,
#[error("Failed to build Tooling API client")]
Build,
}
#[derive(Clone, Debug)]
pub struct Client {
auth_client: Arc<client::Client>,
api_version: String,
connect_timeout: Duration,
request_timeout: Duration,
http_cache: Arc<HttpClientCache>,
}
pub struct ClientBuilder {
auth_client: client::Client,
api_version: Option<String>,
connect_timeout: Option<Duration>,
request_timeout: Option<Duration>,
}
impl ClientBuilder {
pub fn new(auth_client: client::Client) -> Self {
Self {
auth_client,
api_version: None,
connect_timeout: None,
request_timeout: None,
}
}
pub fn api_version(mut self, version: impl Into<String>) -> Self {
self.api_version = Some(version.into());
self
}
pub fn connect_timeout(mut self, timeout: Duration) -> Self {
self.connect_timeout = Some(timeout);
self
}
pub fn request_timeout(mut self, timeout: Duration) -> Self {
self.request_timeout = Some(timeout);
self
}
pub fn build(self) -> Result<Client, Error> {
Ok(Client {
auth_client: Arc::new(self.auth_client),
api_version: self
.api_version
.unwrap_or_else(|| DEFAULT_API_VERSION.to_string()),
connect_timeout: self
.connect_timeout
.unwrap_or_else(|| Duration::from_secs(DEFAULT_CONNECT_TIMEOUT_SECS)),
request_timeout: self
.request_timeout
.unwrap_or_else(|| Duration::from_secs(DEFAULT_REQUEST_TIMEOUT_SECS)),
http_cache: Arc::new(HttpClientCache::new()),
})
}
}
impl Client {
pub fn auth_client(&self) -> &Arc<client::Client> {
&self.auth_client
}
pub fn api_version(&self) -> &str {
&self.api_version
}
pub fn connect_timeout(&self) -> Duration {
self.connect_timeout
}
pub fn request_timeout(&self) -> Duration {
self.request_timeout
}
fn base_url(&self) -> Result<String, Error> {
let instance_url = self
.auth_client
.instance_url
.as_ref()
.ok_or(Error::MissingInstanceUrl)?;
Ok(format!(
"{}/services/data/v{}/tooling",
instance_url, self.api_version
))
}
async fn build_http_client(&self) -> Result<reqwest::Client, Error> {
self.http_cache
.get(
self.auth_client.as_ref(),
self.connect_timeout(),
self.request_timeout(),
)
.await
.map_err(|e| match e {
crate::http::Error::Auth { source } => Error::Auth { source },
crate::http::Error::InvalidHeader | crate::http::Error::Lock => Error::Auth {
source: client::Error::LockError,
},
crate::http::Error::Build { source } => Error::Communication { source },
})
}
pub async fn create_managed_event_subscription(
&self,
request: CreateManagedEventSubscriptionRequest,
) -> Result<CreateManagedEventSubscriptionResponse, Error> {
let http_client = self.build_http_client().await?;
let base_url = self.base_url()?;
let url = format!("{base_url}/sobjects/ManagedEventSubscription");
let response = http_client
.post(&url)
.json(&request)
.send()
.await
.map_err(|source| Error::Communication { source })?;
let status = response.status();
if status.is_success() {
response
.json::<CreateManagedEventSubscriptionResponse>()
.await
.map_err(|source| Error::Communication { source })
} else {
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
Err(Error::ApiError {
status: status.as_u16(),
message: error_text,
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_source_chain_preserved() {
let auth_error = client::Error::LockError;
let error = Error::Auth { source: auth_error };
assert!(std::error::Error::source(&error).is_some());
}
#[tokio::test]
async fn test_reqwest_error_conversion() {
let reqwest_error = reqwest::Client::new()
.get("http://invalid-url-that-does-not-exist.test")
.send()
.await
.unwrap_err();
let error = Error::Communication {
source: reqwest_error,
};
assert!(matches!(error, Error::Communication { .. }));
assert!(std::error::Error::source(&error).is_some());
}
#[test]
fn test_serde_error_conversion() {
let bad_json = "{ invalid json }";
let serde_error: serde_json::Error =
serde_json::from_str::<serde_json::Value>(bad_json).unwrap_err();
let error = Error::Serialization {
source: serde_error,
};
assert!(matches!(error, Error::Serialization { .. }));
assert!(std::error::Error::source(&error).is_some());
}
}