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 }
271
272 #[cfg(feature = "serverless")]
274 if let Ok(api_url) = std::env::var("RUNPOD_API_URL") {
275 #[cfg(feature = "tracing")]
276 tracing::debug!(
277 target: TRACING_TARGET_CONFIG,
278 api_url = %api_url,
279 "Using custom API URL"
280 );
281
282 builder = builder.with_api_url(api_url);
283 }
284
285 if let Ok(timeout_str) = std::env::var("RUNPOD_TIMEOUT_SECS") {
287 let timeout_secs = timeout_str.parse::<u64>().map_err(|_| {
288 #[cfg(feature = "tracing")]
289 tracing::error!(target: TRACING_TARGET_CONFIG, timeout_str = %timeout_str, "Invalid RUNPOD_TIMEOUT_SECS value");
290
291 RunpodBuilderError::ValidationError(format!(
292 "Invalid RUNPOD_TIMEOUT_SECS value: {}",
293 timeout_str
294 ))
295 })?;
296
297 #[cfg(feature = "tracing")]
298 tracing::debug!(target: TRACING_TARGET_CONFIG, timeout_secs, "Using custom timeout");
299
300 builder = builder.with_timeout(Duration::from_secs(timeout_secs));
301 }
302
303 let config = builder.build()?;
304
305 #[cfg(feature = "tracing")]
306 tracing::info!(target: TRACING_TARGET_CONFIG,
307 rest_url = %config.rest_url(),
308 timeout = ?config.timeout(),
309 "Configuration loaded successfully from environment"
310 );
311
312 Ok(config)
313 }
314}
315
316impl fmt::Debug for RunpodConfig {
317 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
318 let mut debug_struct = f.debug_struct("RunpodConfig");
319 debug_struct
320 .field("api_key", &self.masked_api_key())
321 .field("rest_url", &self.rest_url)
322 .field("timeout", &self.timeout);
323
324 #[cfg(feature = "serverless")]
325 debug_struct.field("api_url", &self.api_url);
326 debug_struct.finish()
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 #[test]
335 fn test_config_builder() -> Result<()> {
336 let config = RunpodConfig::builder().with_api_key("test_key").build()?;
337
338 assert_eq!(config.api_key(), "test_key");
339 assert_eq!(config.rest_url(), "https://rest.runpod.io/v1");
340 #[cfg(feature = "serverless")]
341 assert_eq!(config.api_url(), "https://api.runpod.io/v2");
342 assert_eq!(config.timeout(), Duration::from_secs(30));
343
344 Ok(())
345 }
346
347 #[test]
348 fn test_config_builder_with_custom_values() -> Result<()> {
349 let config = RunpodConfig::builder()
350 .with_api_key("test_key")
351 .with_rest_url("https://custom.api.com")
352 .with_timeout(Duration::from_secs(60))
353 .build()?;
354
355 assert_eq!(config.api_key(), "test_key");
356 assert_eq!(config.rest_url(), "https://custom.api.com");
357 assert_eq!(config.timeout(), Duration::from_secs(60));
358
359 Ok(())
360 }
361
362 #[test]
363 fn test_config_validation_empty_api_key() {
364 let result = RunpodConfig::builder().with_api_key("").build();
365 assert!(result.is_err());
366 }
367
368 #[test]
369 fn test_config_validation_zero_timeout() {
370 let result = RunpodConfig::builder()
371 .with_api_key("test_key")
372 .with_timeout(Duration::from_secs(0))
373 .build();
374
375 assert!(result.is_err());
376 }
377
378 #[test]
379 fn test_config_validation_excessive_timeout() {
380 let result = RunpodConfig::builder()
381 .with_api_key("test_key")
382 .with_timeout(Duration::from_secs(400))
383 .build();
384
385 assert!(result.is_err());
386 }
387
388 #[test]
389 fn test_config_builder_with_all_options() -> Result<()> {
390 let config = RunpodConfig::builder()
391 .with_api_key("test_key_comprehensive")
392 .with_rest_url("https://api.custom-domain.com/v2")
393 .with_timeout(Duration::from_secs(120))
394 .build()?;
395
396 assert_eq!(config.api_key(), "test_key_comprehensive");
397 assert_eq!(config.rest_url(), "https://api.custom-domain.com/v2");
398 assert_eq!(config.timeout(), Duration::from_secs(120));
399
400 Ok(())
401 }
402
403 #[test]
404 fn test_config_builder_defaults() -> Result<()> {
405 let config = RunpodConfig::builder().with_api_key("test_key").build()?;
406
407 assert_eq!(config.api_key(), "test_key");
408 assert_eq!(config.rest_url(), "https://rest.runpod.io/v1");
409 assert_eq!(config.timeout(), Duration::from_secs(30));
410
411 Ok(())
412 }
413}