use std::future::ready;
use std::sync::{Arc, OnceLock};
use std::time::Duration;
use agentcat_api::apis::configuration::Configuration;
use agentcat_api::apis::events_api::{PublishEventParams, publish_event};
use agentcat_api::models::PublishEventRequest;
use super::Publisher;
use crate::sink::BoxFuture;
const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
const ATTEMPT_TIMEOUT: Duration = Duration::from_secs(10);
const USER_AGENT: &str = concat!("agentcat-rust/", env!("CARGO_PKG_VERSION"));
const NO_CLIENT: &str = "the HTTP client could not be built";
type BuildClient = fn(Duration) -> Result<reqwest::Client, String>;
pub(crate) struct ApiPublisher {
base_url: String,
attempt_timeout: Duration,
build: BuildClient,
configuration: OnceLock<Option<Arc<Configuration>>>,
}
impl ApiPublisher {
pub(crate) fn new(base_url: String, log: &mut Vec<String>) -> Self {
Self::with_attempt_timeout(base_url, ATTEMPT_TIMEOUT, log)
}
pub(crate) fn with_attempt_timeout(
base_url: String,
attempt_timeout: Duration,
log: &mut Vec<String>,
) -> Self {
log.extend(tls_warning(&base_url));
Self {
base_url,
attempt_timeout,
build: build_client,
configuration: OnceLock::new(),
}
}
#[cfg(test)]
pub(super) fn with_build(base_url: String, build: BuildClient) -> Self {
Self {
build,
..Self::new(base_url, &mut Vec::new())
}
}
fn configuration(&self, log: &mut Vec<String>) -> Option<&Arc<Configuration>> {
self.configuration
.get_or_init(|| match (self.build)(self.attempt_timeout) {
Ok(client) => Some(Arc::new(Configuration {
base_path: self.base_url.clone(),
user_agent: Some(USER_AGENT.to_owned()),
client,
basic_auth: None,
oauth_access_token: None,
bearer_access_token: None,
api_key: None,
})),
Err(error) => {
log.push(format!(
"Failed to build the HTTP client, events will not be sent: {error}"
));
None
}
})
.as_ref()
}
}
fn tls_warning(base_url: &str) -> Option<String> {
let https = base_url
.trim_start()
.to_ascii_lowercase()
.starts_with("https:");
(https && cfg!(not(any(feature = "rustls", feature = "native-tls")))).then(|| {
format!(
"Warning: agentcat-sdk was built without a TLS feature (rustls or native-tls), so \
events cannot be sent to {base_url}"
)
})
}
fn build_client(attempt_timeout: Duration) -> Result<reqwest::Client, String> {
reqwest::Client::builder()
.connect_timeout(CONNECT_TIMEOUT)
.timeout(attempt_timeout)
.user_agent(USER_AGENT)
.build()
.map_err(|error| error.to_string())
}
impl Publisher for ApiPublisher {
fn prepare(&self, log: &mut Vec<String>) -> Result<(), String> {
match self.configuration(log) {
Some(_) => Ok(()),
None => Err(NO_CLIENT.to_owned()),
}
}
fn publish(&self, request: PublishEventRequest) -> BoxFuture<'static, Result<(), String>> {
let Some(configuration) = self.configuration(&mut Vec::new()).cloned() else {
return Box::pin(ready(Err(NO_CLIENT.to_owned())));
};
Box::pin(async move {
let params = PublishEventParams {
publish_event_request: Some(request),
};
publish_event(&configuration, params)
.await
.map(|_| ())
.map_err(|error| error.to_string())
})
}
}
#[cfg(test)]
mod tests;