use std::time::Duration;
use crate::error::Error;
#[derive(Debug, Clone)]
pub struct CyclesConfig {
pub base_url: String,
pub api_key: String,
pub tenant: Option<String>,
pub workspace: Option<String>,
pub app: Option<String>,
pub workflow: Option<String>,
pub agent: Option<String>,
pub toolset: Option<String>,
pub connect_timeout: Duration,
pub read_timeout: Duration,
pub retry_enabled: bool,
pub retry_max_attempts: u32,
pub retry_initial_delay: Duration,
pub retry_multiplier: f64,
pub retry_max_delay: Duration,
}
impl CyclesConfig {
pub fn from_env() -> Result<Self, Error> {
Self::from_env_with_prefix("CYCLES_")
}
pub fn from_env_with_prefix(prefix: &str) -> Result<Self, Error> {
let base_url = std::env::var(format!("{prefix}BASE_URL")).map_err(|_| {
Error::Config(format!("{prefix}BASE_URL environment variable is required"))
})?;
let api_key = std::env::var(format!("{prefix}API_KEY")).map_err(|_| {
Error::Config(format!("{prefix}API_KEY environment variable is required"))
})?;
let env_opt = |name: &str| std::env::var(format!("{prefix}{name}")).ok();
let env_duration_ms = |name: &str, default_ms: u64| -> Duration {
env_opt(name)
.and_then(|v| v.parse::<u64>().ok())
.map_or(Duration::from_millis(default_ms), Duration::from_millis)
};
let env_u32 = |name: &str, default: u32| -> u32 {
env_opt(name)
.and_then(|v| v.parse().ok())
.unwrap_or(default)
};
let env_f64 = |name: &str, default: f64| -> f64 {
env_opt(name)
.and_then(|v| v.parse().ok())
.unwrap_or(default)
};
Ok(Self {
base_url,
api_key,
tenant: env_opt("TENANT"),
workspace: env_opt("WORKSPACE"),
app: env_opt("APP"),
workflow: env_opt("WORKFLOW"),
agent: env_opt("AGENT"),
toolset: env_opt("TOOLSET"),
connect_timeout: env_duration_ms("CONNECT_TIMEOUT", 2_000),
read_timeout: env_duration_ms("READ_TIMEOUT", 5_000),
retry_enabled: env_opt("RETRY_ENABLED").is_none_or(|v| v.to_lowercase() != "false"),
retry_max_attempts: env_u32("RETRY_MAX_ATTEMPTS", 5),
retry_initial_delay: env_duration_ms("RETRY_INITIAL_DELAY", 500),
retry_multiplier: env_f64("RETRY_MULTIPLIER", 2.0),
retry_max_delay: env_duration_ms("RETRY_MAX_DELAY", 30_000),
})
}
}
#[must_use = "builder does nothing until .build() is called"]
pub struct CyclesClientBuilder {
config: CyclesConfig,
http_client: Option<reqwest::Client>,
}
impl CyclesClientBuilder {
pub fn new(api_key: impl Into<String>, base_url: impl Into<String>) -> Self {
Self {
config: CyclesConfig {
base_url: base_url.into(),
api_key: api_key.into(),
tenant: None,
workspace: None,
app: None,
workflow: None,
agent: None,
toolset: None,
connect_timeout: Duration::from_millis(2_000),
read_timeout: Duration::from_millis(5_000),
retry_enabled: true,
retry_max_attempts: 5,
retry_initial_delay: Duration::from_millis(500),
retry_multiplier: 2.0,
retry_max_delay: Duration::from_secs(30),
},
http_client: None,
}
}
pub fn tenant(mut self, tenant: impl Into<String>) -> Self {
self.config.tenant = Some(tenant.into());
self
}
pub fn workspace(mut self, workspace: impl Into<String>) -> Self {
self.config.workspace = Some(workspace.into());
self
}
pub fn app(mut self, app: impl Into<String>) -> Self {
self.config.app = Some(app.into());
self
}
pub fn workflow(mut self, workflow: impl Into<String>) -> Self {
self.config.workflow = Some(workflow.into());
self
}
pub fn agent(mut self, agent: impl Into<String>) -> Self {
self.config.agent = Some(agent.into());
self
}
pub fn toolset(mut self, toolset: impl Into<String>) -> Self {
self.config.toolset = Some(toolset.into());
self
}
pub fn connect_timeout(mut self, timeout: Duration) -> Self {
self.config.connect_timeout = timeout;
self
}
pub fn read_timeout(mut self, timeout: Duration) -> Self {
self.config.read_timeout = timeout;
self
}
pub fn retry_enabled(mut self, enabled: bool) -> Self {
self.config.retry_enabled = enabled;
self
}
pub fn retry_max_attempts(mut self, max: u32) -> Self {
self.config.retry_max_attempts = max;
self
}
pub fn http_client(mut self, client: reqwest::Client) -> Self {
self.http_client = Some(client);
self
}
pub fn build(self) -> crate::CyclesClient {
crate::CyclesClient::from_builder(self.config, self.http_client)
}
#[cfg(feature = "blocking")]
pub fn build_blocking(self) -> Result<crate::BlockingCyclesClient, crate::Error> {
crate::BlockingCyclesClient::new(self.config)
}
}