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 #[cfg(feature = "graphql")]
83 #[cfg_attr(docsrs, doc(cfg(feature = "graphql")))]
84 #[builder(default = "Self::default_graphql_url()")]
85 graphql_url: String,
86
87 #[builder(default = "Self::default_timeout()")]
91 timeout: Duration,
92
93 #[builder(default = "None")]
98 client: Option<Client>,
99}
100
101impl RunpodBuilder {
102 fn default_rest_url() -> String {
104 "https://rest.runpod.io/v1".to_string()
105 }
106
107 #[cfg(feature = "serverless")]
109 #[cfg_attr(docsrs, doc(cfg(feature = "serverless")))]
110 fn default_api_url() -> String {
111 "https://api.runpod.io/v2".to_string()
112 }
113
114 #[cfg(feature = "graphql")]
116 #[cfg_attr(docsrs, doc(cfg(feature = "graphql")))]
117 fn default_graphql_url() -> String {
118 "https://api.runpod.io/graphql".to_string()
119 }
120
121 fn default_timeout() -> Duration {
123 Duration::from_secs(30)
124 }
125
126 fn validate_config(&self) -> Result<(), String> {
128 if let Some(ref api_key) = self.api_key
130 && api_key.trim().is_empty()
131 {
132 return Err("API key cannot be empty".to_string());
133 }
134
135 if let Some(timeout) = self.timeout {
137 if timeout.is_zero() {
138 return Err("Timeout must be greater than 0".to_string());
139 }
140 if timeout > Duration::from_secs(300) {
141 return Err("Timeout cannot exceed 300 seconds (5 minutes)".to_string());
142 }
143 }
144
145 Ok(())
146 }
147
148 pub fn build_client(self) -> Result<RunpodClient> {
164 let config = self.build()?;
165 RunpodClient::new(config)
166 }
167}
168
169impl RunpodConfig {
170 pub fn builder() -> RunpodBuilder {
184 RunpodBuilder::default()
185 }
186
187 pub fn build_client(self) -> Result<RunpodClient> {
201 RunpodClient::new(self)
202 }
203
204 pub fn api_key(&self) -> &str {
206 &self.api_key
207 }
208
209 pub fn masked_api_key(&self) -> String {
214 if self.api_key.len() > 4 {
215 format!("{}****", &self.api_key[..4])
216 } else {
217 "****".to_string()
218 }
219 }
220
221 pub fn rest_url(&self) -> &str {
223 &self.rest_url
224 }
225
226 #[cfg(feature = "serverless")]
228 #[cfg_attr(docsrs, doc(cfg(feature = "serverless")))]
229 pub fn api_url(&self) -> &str {
230 &self.api_url
231 }
232
233 #[cfg(feature = "graphql")]
235 #[cfg_attr(docsrs, doc(cfg(feature = "graphql")))]
236 pub fn graphql_url(&self) -> &str {
237 &self.graphql_url
238 }
239
240 pub fn timeout(&self) -> Duration {
242 self.timeout
243 }
244
245 pub(crate) fn client(&self) -> Option<Client> {
247 self.client.clone()
248 }
249
250 #[cfg_attr(feature = "tracing", tracing::instrument)]
271 pub fn from_env() -> Result<Self> {
272 #[cfg(feature = "tracing")]
273 tracing::debug!(target: TRACING_TARGET_CONFIG, "Loading configuration from environment");
274
275 let api_key = std::env::var("RUNPOD_API_KEY").map_err(|_| {
276 #[cfg(feature = "tracing")]
277 tracing::error!(target: TRACING_TARGET_CONFIG, "RUNPOD_API_KEY environment variable not set");
278
279 RunpodBuilderError::ValidationError(
280 "RUNPOD_API_KEY environment variable not set".to_string(),
281 )
282 })?;
283
284 let mut builder = Self::builder().with_api_key(api_key);
285
286 if let Ok(rest_url) = std::env::var("RUNPOD_REST_URL") {
288 #[cfg(feature = "tracing")]
289 tracing::debug!(target: TRACING_TARGET_CONFIG, rest_url = %rest_url, "Using custom REST URL");
290
291 builder = builder.with_rest_url(rest_url);
292 } else if let Ok(base_url) = std::env::var("RUNPOD_BASE_URL") {
293 #[cfg(feature = "tracing")]
294 tracing::debug!(target: TRACING_TARGET_CONFIG, base_url = %base_url, "Using custom base URL (legacy)");
295
296 builder = builder.with_rest_url(base_url);
297 }
298
299 #[cfg(feature = "serverless")]
301 if let Ok(api_url) = std::env::var("RUNPOD_API_URL") {
302 #[cfg(feature = "tracing")]
303 tracing::debug!(
304 target: TRACING_TARGET_CONFIG,
305 api_url = %api_url,
306 "Using custom API URL"
307 );
308
309 builder = builder.with_api_url(api_url);
310 }
311
312 #[cfg(feature = "graphql")]
314 if let Ok(graphql_url) = std::env::var("RUNPOD_GRAPHQL_URL") {
315 #[cfg(feature = "tracing")]
316 tracing::debug!(
317 target: TRACING_TARGET_CONFIG,
318 graphql_url = %graphql_url,
319 "Using custom GraphQL URL"
320 );
321
322 builder = builder.with_graphql_url(graphql_url);
323 }
324
325 if let Ok(timeout_str) = std::env::var("RUNPOD_TIMEOUT_SECS") {
327 let timeout_secs = timeout_str.parse::<u64>().map_err(|_| {
328 #[cfg(feature = "tracing")]
329 tracing::error!(target: TRACING_TARGET_CONFIG, timeout_str = %timeout_str, "Invalid RUNPOD_TIMEOUT_SECS value");
330
331 RunpodBuilderError::ValidationError(format!(
332 "Invalid RUNPOD_TIMEOUT_SECS value: {}",
333 timeout_str
334 ))
335 })?;
336
337 #[cfg(feature = "tracing")]
338 tracing::debug!(target: TRACING_TARGET_CONFIG, timeout_secs, "Using custom timeout");
339
340 builder = builder.with_timeout(Duration::from_secs(timeout_secs));
341 }
342
343 let config = builder.build()?;
344
345 #[cfg(feature = "tracing")]
346 tracing::info!(target: TRACING_TARGET_CONFIG,
347 rest_url = %config.rest_url(),
348 timeout = ?config.timeout(),
349 "Configuration loaded successfully from environment"
350 );
351
352 Ok(config)
353 }
354}
355
356impl fmt::Debug for RunpodConfig {
357 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358 let mut debug_struct = f.debug_struct("RunpodConfig");
359 debug_struct
360 .field("api_key", &self.masked_api_key())
361 .field("rest_url", &self.rest_url)
362 .field("timeout", &self.timeout);
363
364 #[cfg(feature = "serverless")]
365 debug_struct.field("api_url", &self.api_url);
366
367 #[cfg(feature = "graphql")]
368 debug_struct.field("graphql_url", &self.graphql_url);
369
370 debug_struct.finish()
371 }
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377
378 #[test]
379 fn test_config_builder() -> Result<()> {
380 let config = RunpodConfig::builder().with_api_key("test_key").build()?;
381
382 assert_eq!(config.api_key(), "test_key");
383 assert_eq!(config.rest_url(), "https://rest.runpod.io/v1");
384 #[cfg(feature = "serverless")]
385 assert_eq!(config.api_url(), "https://api.runpod.io/v2");
386 #[cfg(feature = "graphql")]
387 assert_eq!(config.graphql_url(), "https://api.runpod.io/graphql");
388 assert_eq!(config.timeout(), Duration::from_secs(30));
389
390 Ok(())
391 }
392
393 #[test]
394 fn test_config_builder_with_custom_values() -> Result<()> {
395 let config = RunpodConfig::builder()
396 .with_api_key("test_key")
397 .with_rest_url("https://custom.api.com")
398 .with_timeout(Duration::from_secs(60))
399 .build()?;
400
401 assert_eq!(config.api_key(), "test_key");
402 assert_eq!(config.rest_url(), "https://custom.api.com");
403 assert_eq!(config.timeout(), Duration::from_secs(60));
404
405 Ok(())
406 }
407
408 #[test]
409 fn test_config_validation_empty_api_key() {
410 let result = RunpodConfig::builder().with_api_key("").build();
411 assert!(result.is_err());
412 }
413
414 #[test]
415 fn test_config_validation_zero_timeout() {
416 let result = RunpodConfig::builder()
417 .with_api_key("test_key")
418 .with_timeout(Duration::from_secs(0))
419 .build();
420
421 assert!(result.is_err());
422 }
423
424 #[test]
425 fn test_config_validation_excessive_timeout() {
426 let result = RunpodConfig::builder()
427 .with_api_key("test_key")
428 .with_timeout(Duration::from_secs(400))
429 .build();
430
431 assert!(result.is_err());
432 }
433
434 #[test]
435 fn test_config_builder_with_all_options() -> Result<()> {
436 let config = RunpodConfig::builder()
437 .with_api_key("test_key_comprehensive")
438 .with_rest_url("https://api.custom-domain.com/v2")
439 .with_timeout(Duration::from_secs(120))
440 .build()?;
441
442 assert_eq!(config.api_key(), "test_key_comprehensive");
443 assert_eq!(config.rest_url(), "https://api.custom-domain.com/v2");
444 assert_eq!(config.timeout(), Duration::from_secs(120));
445
446 Ok(())
447 }
448
449 #[test]
450 fn test_config_builder_defaults() -> Result<()> {
451 let config = RunpodConfig::builder().with_api_key("test_key").build()?;
452
453 assert_eq!(config.api_key(), "test_key");
454 assert_eq!(config.rest_url(), "https://rest.runpod.io/v1");
455 assert_eq!(config.timeout(), Duration::from_secs(30));
456
457 Ok(())
458 }
459}