use std::time::Duration;
use async_trait::async_trait;
use olai_http::CloudClient;
use url::Url;
use crate::config::DEFAULT_REQUEST_TIMEOUT;
use crate::event::RunEvent;
use crate::transport::{Transport, TransportError};
#[derive(Clone)]
pub struct CloudClientTransport {
client: CloudClient,
endpoint: Url,
batch_endpoint: Url,
timeout: Duration,
}
impl std::fmt::Debug for CloudClientTransport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CloudClientTransport")
.field("endpoint", &self.endpoint)
.field("batch_endpoint", &self.batch_endpoint)
.field("timeout", &self.timeout)
.finish_non_exhaustive()
}
}
impl CloudClientTransport {
pub fn new(client: CloudClient, endpoint: Url) -> Self {
let batch_endpoint = default_batch_endpoint(&endpoint);
Self {
client,
endpoint,
batch_endpoint,
timeout: DEFAULT_REQUEST_TIMEOUT,
}
}
pub fn with_token(endpoint: Url, token: impl ToString) -> Self {
Self::new(CloudClient::new_with_token(token), endpoint)
}
pub fn unauthenticated(endpoint: Url) -> Self {
Self::new(CloudClient::new_unauthenticated(), endpoint)
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn with_batch_endpoint(mut self, batch_endpoint: Url) -> Self {
self.batch_endpoint = batch_endpoint;
self
}
}
fn default_batch_endpoint(endpoint: &Url) -> Url {
let mut batch = endpoint.clone();
if let Ok(mut segments) = batch.path_segments_mut() {
segments.pop_if_empty().push("batch");
}
batch
}
#[async_trait]
impl Transport for CloudClientTransport {
async fn emit(&self, event: &RunEvent) -> Result<(), TransportError> {
self.client
.post(self.endpoint.clone())
.timeout(self.timeout)
.json(event)
.send()
.await
.map_err(|e| TransportError::Other(e.to_string()))?;
Ok(())
}
async fn emit_batch(&self, events: &[RunEvent]) -> Result<(), TransportError> {
match events {
[] => Ok(()),
[event] => self.emit(event).await,
events => {
self.client
.post(self.batch_endpoint.clone())
.timeout(self.timeout)
.json(events)
.send()
.await
.map_err(|e| TransportError::Other(e.to_string()))?;
Ok(())
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn batch_endpoint_appends_batch_segment() {
let endpoint = Url::parse("http://localhost:8091/api/v1/lineage").unwrap();
let batch = default_batch_endpoint(&endpoint);
assert_eq!(batch.as_str(), "http://localhost:8091/api/v1/lineage/batch");
}
#[test]
fn batch_endpoint_handles_trailing_slash() {
let endpoint = Url::parse("http://localhost:8091/api/v1/lineage/").unwrap();
let batch = default_batch_endpoint(&endpoint);
assert_eq!(batch.as_str(), "http://localhost:8091/api/v1/lineage/batch");
}
fn endpoint() -> Url {
Url::parse("http://localhost:8091/api/v1/lineage").unwrap()
}
#[test]
fn new_defaults_batch_endpoint_and_timeout() {
let t = CloudClientTransport::unauthenticated(endpoint());
assert_eq!(t.endpoint.as_str(), "http://localhost:8091/api/v1/lineage");
assert_eq!(
t.batch_endpoint.as_str(),
"http://localhost:8091/api/v1/lineage/batch",
"batch endpoint defaults to endpoint + /batch"
);
assert_eq!(t.timeout, DEFAULT_REQUEST_TIMEOUT);
}
#[test]
fn with_token_carries_the_same_endpoint_defaults() {
let t = CloudClientTransport::with_token(endpoint(), "secret");
assert_eq!(
t.batch_endpoint.as_str(),
"http://localhost:8091/api/v1/lineage/batch"
);
assert_eq!(t.timeout, DEFAULT_REQUEST_TIMEOUT);
}
#[test]
fn with_timeout_and_with_batch_endpoint_override_defaults() {
let custom_batch = Url::parse("http://localhost:8091/bulk").unwrap();
let t = CloudClientTransport::unauthenticated(endpoint())
.with_timeout(Duration::from_secs(5))
.with_batch_endpoint(custom_batch.clone());
assert_eq!(t.timeout, Duration::from_secs(5));
assert_eq!(t.batch_endpoint, custom_batch);
assert_eq!(t.endpoint, endpoint());
}
#[tokio::test]
async fn emit_batch_of_nothing_is_a_no_op() {
let t = CloudClientTransport::unauthenticated(endpoint());
assert!(t.emit_batch(&[]).await.is_ok());
}
}