use std::fmt;
use std::time::Duration;
use derive_builder::Builder;
use reqwest::Client;
use crate::Result;
#[cfg(feature = "tracing")]
use crate::TRACING_TARGET_CONFIG;
use crate::client::RunpodClient;
#[derive(Clone, Builder)]
#[builder(
name = "RunpodBuilder",
pattern = "owned",
setter(into, strip_option, prefix = "with"),
build_fn(validate = "Self::validate_config")
)]
pub struct RunpodConfig {
api_key: String,
#[builder(default = "Self::default_rest_url()")]
rest_url: String,
#[cfg(feature = "serverless")]
#[cfg_attr(docsrs, doc(cfg(feature = "serverless")))]
#[builder(default = "Self::default_api_url()")]
api_url: String,
#[builder(default = "Self::default_timeout()")]
timeout: Duration,
#[builder(default = "None")]
client: Option<Client>,
}
impl RunpodBuilder {
fn default_rest_url() -> String {
"https://rest.runpod.io/v1".to_string()
}
#[cfg(feature = "serverless")]
#[cfg_attr(docsrs, doc(cfg(feature = "serverless")))]
fn default_api_url() -> String {
"https://api.runpod.io/v2".to_string()
}
fn default_timeout() -> Duration {
Duration::from_secs(30)
}
fn validate_config(&self) -> Result<(), String> {
if let Some(ref api_key) = self.api_key
&& api_key.trim().is_empty()
{
return Err("API key cannot be empty".to_string());
}
if let Some(timeout) = self.timeout {
if timeout.is_zero() {
return Err("Timeout must be greater than 0".to_string());
}
if timeout > Duration::from_secs(300) {
return Err("Timeout cannot exceed 300 seconds (5 minutes)".to_string());
}
}
Ok(())
}
pub fn build_client(self) -> Result<RunpodClient> {
let config = self.build()?;
RunpodClient::new(config)
}
}
impl RunpodConfig {
pub fn builder() -> RunpodBuilder {
RunpodBuilder::default()
}
pub fn build_client(self) -> Result<RunpodClient> {
RunpodClient::new(self)
}
pub fn api_key(&self) -> &str {
&self.api_key
}
pub fn masked_api_key(&self) -> String {
if self.api_key.len() > 4 {
format!("{}****", &self.api_key[..4])
} else {
"****".to_string()
}
}
pub fn rest_url(&self) -> &str {
&self.rest_url
}
#[cfg(feature = "serverless")]
#[cfg_attr(docsrs, doc(cfg(feature = "serverless")))]
pub fn api_url(&self) -> &str {
&self.api_url
}
pub fn timeout(&self) -> Duration {
self.timeout
}
pub(crate) fn client(&self) -> Option<Client> {
self.client.clone()
}
#[cfg_attr(feature = "tracing", tracing::instrument)]
pub fn from_env() -> Result<Self> {
#[cfg(feature = "tracing")]
tracing::debug!(target: TRACING_TARGET_CONFIG, "Loading configuration from environment");
let api_key = std::env::var("RUNPOD_API_KEY").map_err(|_| {
#[cfg(feature = "tracing")]
tracing::error!(target: TRACING_TARGET_CONFIG, "RUNPOD_API_KEY environment variable not set");
RunpodBuilderError::ValidationError(
"RUNPOD_API_KEY environment variable not set".to_string(),
)
})?;
let mut builder = Self::builder().with_api_key(api_key);
if let Ok(rest_url) = std::env::var("RUNPOD_REST_URL") {
#[cfg(feature = "tracing")]
tracing::debug!(target: TRACING_TARGET_CONFIG, rest_url = %rest_url, "Using custom REST URL");
builder = builder.with_rest_url(rest_url);
}
#[cfg(feature = "serverless")]
if let Ok(api_url) = std::env::var("RUNPOD_API_URL") {
#[cfg(feature = "tracing")]
tracing::debug!(
target: TRACING_TARGET_CONFIG,
api_url = %api_url,
"Using custom API URL"
);
builder = builder.with_api_url(api_url);
}
if let Ok(timeout_str) = std::env::var("RUNPOD_TIMEOUT_SECS") {
let timeout_secs = timeout_str.parse::<u64>().map_err(|_| {
#[cfg(feature = "tracing")]
tracing::error!(target: TRACING_TARGET_CONFIG, timeout_str = %timeout_str, "Invalid RUNPOD_TIMEOUT_SECS value");
RunpodBuilderError::ValidationError(format!(
"Invalid RUNPOD_TIMEOUT_SECS value: {}",
timeout_str
))
})?;
#[cfg(feature = "tracing")]
tracing::debug!(target: TRACING_TARGET_CONFIG, timeout_secs, "Using custom timeout");
builder = builder.with_timeout(Duration::from_secs(timeout_secs));
}
let config = builder.build()?;
#[cfg(feature = "tracing")]
tracing::info!(target: TRACING_TARGET_CONFIG,
rest_url = %config.rest_url(),
timeout = ?config.timeout(),
"Configuration loaded successfully from environment"
);
Ok(config)
}
}
impl fmt::Debug for RunpodConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut debug_struct = f.debug_struct("RunpodConfig");
debug_struct
.field("api_key", &self.masked_api_key())
.field("rest_url", &self.rest_url)
.field("timeout", &self.timeout);
#[cfg(feature = "serverless")]
debug_struct.field("api_url", &self.api_url);
debug_struct.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_builder() -> Result<()> {
let config = RunpodConfig::builder().with_api_key("test_key").build()?;
assert_eq!(config.api_key(), "test_key");
assert_eq!(config.rest_url(), "https://rest.runpod.io/v1");
#[cfg(feature = "serverless")]
assert_eq!(config.api_url(), "https://api.runpod.io/v2");
assert_eq!(config.timeout(), Duration::from_secs(30));
Ok(())
}
#[test]
fn test_config_builder_with_custom_values() -> Result<()> {
let config = RunpodConfig::builder()
.with_api_key("test_key")
.with_rest_url("https://custom.api.com")
.with_timeout(Duration::from_secs(60))
.build()?;
assert_eq!(config.api_key(), "test_key");
assert_eq!(config.rest_url(), "https://custom.api.com");
assert_eq!(config.timeout(), Duration::from_secs(60));
Ok(())
}
#[test]
fn test_config_validation_empty_api_key() {
let result = RunpodConfig::builder().with_api_key("").build();
assert!(result.is_err());
}
#[test]
fn test_config_validation_zero_timeout() {
let result = RunpodConfig::builder()
.with_api_key("test_key")
.with_timeout(Duration::from_secs(0))
.build();
assert!(result.is_err());
}
#[test]
fn test_config_validation_excessive_timeout() {
let result = RunpodConfig::builder()
.with_api_key("test_key")
.with_timeout(Duration::from_secs(400))
.build();
assert!(result.is_err());
}
#[test]
fn test_config_builder_with_all_options() -> Result<()> {
let config = RunpodConfig::builder()
.with_api_key("test_key_comprehensive")
.with_rest_url("https://api.custom-domain.com/v2")
.with_timeout(Duration::from_secs(120))
.build()?;
assert_eq!(config.api_key(), "test_key_comprehensive");
assert_eq!(config.rest_url(), "https://api.custom-domain.com/v2");
assert_eq!(config.timeout(), Duration::from_secs(120));
Ok(())
}
#[test]
fn test_config_builder_defaults() -> Result<()> {
let config = RunpodConfig::builder().with_api_key("test_key").build()?;
assert_eq!(config.api_key(), "test_key");
assert_eq!(config.rest_url(), "https://rest.runpod.io/v1");
assert_eq!(config.timeout(), Duration::from_secs(30));
Ok(())
}
}