runpod_sdk/client/
config.rs1use std::fmt;
7use std::time::Duration;
8
9use derive_builder::Builder;
10use reqwest::Client;
11
12use crate::Result;
13#[cfg(feature = "tracing")]
14use crate::TRACING_TARGET_CONFIG;
15use crate::client::RunpodClient;
16
17#[derive(Clone, Builder)]
53#[builder(
54 name = "RunpodBuilder",
55 pattern = "owned",
56 setter(into, strip_option, prefix = "with"),
57 build_fn(validate = "Self::validate_config")
58)]
59pub struct RunpodConfig {
60 api_key: String,
64
65 #[builder(default = "Self::default_rest_url()")]
69 rest_url: String,
70
71 #[cfg(feature = "serverless")]
75 #[cfg_attr(docsrs, doc(cfg(feature = "serverless")))]
76 #[builder(default = "Self::default_api_url()")]
77 api_url: String,
78
79 #[builder(default = "Self::default_timeout()")]
83 timeout: Duration,
84
85 #[builder(default = "None")]
90 client: Option<Client>,
91}
92
93impl RunpodBuilder {
94 fn default_rest_url() -> String {
96 "https://rest.runpod.io/v1".to_string()
97 }
98
99 #[cfg(feature = "serverless")]
101 #[cfg_attr(docsrs, doc(cfg(feature = "serverless")))]
102 fn default_api_url() -> String {
103 "https://api.runpod.io/v2".to_string()
104 }
105
106 fn default_timeout() -> Duration {
108 Duration::from_secs(30)
109 }
110
111 fn validate_config(&self) -> Result<(), String> {
113 if let Some(ref api_key) = self.api_key
115 && api_key.trim().is_empty()
116 {
117 return Err("API key cannot be empty".to_string());
118 }
119
120 if let Some(timeout) = self.timeout {
122 if timeout.is_zero() {
123 return Err("Timeout must be greater than 0".to_string());
124 }
125 if timeout > Duration::from_secs(300) {
126 return Err("Timeout cannot exceed 300 seconds (5 minutes)".to_string());
127 }
128 }
129
130 Ok(())
131 }
132
133 pub fn build_client(self) -> Result<RunpodClient> {
149 let config = self.build()?;
150 RunpodClient::new(config)
151 }
152}
153
154impl RunpodConfig {
155 pub fn builder() -> RunpodBuilder {
169 RunpodBuilder::default()
170 }
171
172 pub fn build_client(self) -> Result<RunpodClient> {
186 RunpodClient::new(self)
187 }
188
189 pub fn api_key(&self) -> &str {
191 &self.api_key
192 }
193
194 pub fn masked_api_key(&self) -> String {
199 if self.api_key.len() > 4 {
200 format!("{}****", &self.api_key[..4])
201 } else {
202 "****".to_string()
203 }
204 }
205
206 pub fn rest_url(&self) -> &str {
208 &self.rest_url
209 }
210
211 #[cfg(feature = "serverless")]
213 #[cfg_attr(docsrs, doc(cfg(feature = "serverless")))]
214 pub fn api_url(&self) -> &str {
215 &self.api_url
216 }
217
218 pub fn timeout(&self) -> Duration {
220 self.timeout
221 }
222
223 pub(crate) fn client(&self) -> Option<Client> {
225 self.client.clone()
226 }
227
228 #[cfg_attr(feature = "tracing", tracing::instrument)]
249 pub fn from_env() -> Result<Self> {
250 #[cfg(feature = "tracing")]
251 tracing::debug!(target: TRACING_TARGET_CONFIG, "Loading configuration from environment");
252
253 let api_key = std::env::var("RUNPOD_API_KEY").map_err(|_| {
254 #[cfg(feature = "tracing")]
255 tracing::error!(target: TRACING_TARGET_CONFIG, "RUNPOD_API_KEY environment variable not set");
256
257 RunpodBuilderError::ValidationError(
258 "RUNPOD_API_KEY environment variable not set".to_string(),
259 )
260 })?;
261
262 let mut builder = Self::builder().with_api_key(api_key);
263
264 if let Ok(rest_url) = std::env::var("RUNPOD_REST_URL") {
266 #[cfg(feature = "tracing")]
267 tracing::debug!(target: TRACING_TARGET_CONFIG, rest_url = %rest_url, "Using custom REST URL");
268
269 builder = builder.with_rest_url(rest_url);
270 } else if let Ok(base_url) = std::env::var("RUNPOD_BASE_URL") {
271 #[cfg(feature = "tracing")]
272 tracing::debug!(target: TRACING_TARGET_CONFIG, base_url = %base_url, "Using custom base URL (legacy)");
273
274 builder = builder.with_rest_url(base_url);
275 }
276
277 #[cfg(feature = "serverless")]
279 if let Ok(api_url) = std::env::var("RUNPOD_API_URL") {
280 #[cfg(feature = "tracing")]
281 tracing::debug!(
282 target: TRACING_TARGET_CONFIG,
283 api_url = %api_url,
284 "Using custom API URL"
285 );
286
287 builder = builder.with_api_url(api_url);
288 }
289
290 if let Ok(timeout_str) = std::env::var("RUNPOD_TIMEOUT_SECS") {
292 let timeout_secs = timeout_str.parse::<u64>().map_err(|_| {
293 #[cfg(feature = "tracing")]
294 tracing::error!(target: TRACING_TARGET_CONFIG, timeout_str = %timeout_str, "Invalid RUNPOD_TIMEOUT_SECS value");
295
296 RunpodBuilderError::ValidationError(format!(
297 "Invalid RUNPOD_TIMEOUT_SECS value: {}",
298 timeout_str
299 ))
300 })?;
301
302 #[cfg(feature = "tracing")]
303 tracing::debug!(target: TRACING_TARGET_CONFIG, timeout_secs, "Using custom timeout");
304
305 builder = builder.with_timeout(Duration::from_secs(timeout_secs));
306 }
307
308 let config = builder.build()?;
309
310 #[cfg(feature = "tracing")]
311 tracing::info!(target: TRACING_TARGET_CONFIG,
312 rest_url = %config.rest_url(),
313 timeout = ?config.timeout(),
314 "Configuration loaded successfully from environment"
315 );
316
317 Ok(config)
318 }
319}
320
321impl fmt::Debug for RunpodConfig {
322 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
323 let mut debug_struct = f.debug_struct("RunpodConfig");
324 debug_struct
325 .field("api_key", &self.masked_api_key())
326 .field("rest_url", &self.rest_url)
327 .field("timeout", &self.timeout);
328
329 #[cfg(feature = "serverless")]
330 debug_struct.field("api_url", &self.api_url);
331 debug_struct.finish()
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 #[test]
340 fn test_config_builder() -> Result<()> {
341 let config = RunpodConfig::builder().with_api_key("test_key").build()?;
342
343 assert_eq!(config.api_key(), "test_key");
344 assert_eq!(config.rest_url(), "https://rest.runpod.io/v1");
345 #[cfg(feature = "serverless")]
346 assert_eq!(config.api_url(), "https://api.runpod.io/v2");
347 assert_eq!(config.timeout(), Duration::from_secs(30));
348
349 Ok(())
350 }
351
352 #[test]
353 fn test_config_builder_with_custom_values() -> Result<()> {
354 let config = RunpodConfig::builder()
355 .with_api_key("test_key")
356 .with_rest_url("https://custom.api.com")
357 .with_timeout(Duration::from_secs(60))
358 .build()?;
359
360 assert_eq!(config.api_key(), "test_key");
361 assert_eq!(config.rest_url(), "https://custom.api.com");
362 assert_eq!(config.timeout(), Duration::from_secs(60));
363
364 Ok(())
365 }
366
367 #[test]
368 fn test_config_validation_empty_api_key() {
369 let result = RunpodConfig::builder().with_api_key("").build();
370 assert!(result.is_err());
371 }
372
373 #[test]
374 fn test_config_validation_zero_timeout() {
375 let result = RunpodConfig::builder()
376 .with_api_key("test_key")
377 .with_timeout(Duration::from_secs(0))
378 .build();
379
380 assert!(result.is_err());
381 }
382
383 #[test]
384 fn test_config_validation_excessive_timeout() {
385 let result = RunpodConfig::builder()
386 .with_api_key("test_key")
387 .with_timeout(Duration::from_secs(400))
388 .build();
389
390 assert!(result.is_err());
391 }
392
393 #[test]
394 fn test_config_builder_with_all_options() -> Result<()> {
395 let config = RunpodConfig::builder()
396 .with_api_key("test_key_comprehensive")
397 .with_rest_url("https://api.custom-domain.com/v2")
398 .with_timeout(Duration::from_secs(120))
399 .build()?;
400
401 assert_eq!(config.api_key(), "test_key_comprehensive");
402 assert_eq!(config.rest_url(), "https://api.custom-domain.com/v2");
403 assert_eq!(config.timeout(), Duration::from_secs(120));
404
405 Ok(())
406 }
407
408 #[test]
409 fn test_config_builder_defaults() -> Result<()> {
410 let config = RunpodConfig::builder().with_api_key("test_key").build()?;
411
412 assert_eq!(config.api_key(), "test_key");
413 assert_eq!(config.rest_url(), "https://rest.runpod.io/v1");
414 assert_eq!(config.timeout(), Duration::from_secs(30));
415
416 Ok(())
417 }
418}