1use crate::{ConfigError, ConfigResult};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Default, Deserialize, Serialize)]
8pub struct ProvidersConfig {
9 #[serde(default)]
12 pub anthropic_api_key: Option<String>,
13
14 #[serde(default)]
16 pub openai_api_key: Option<String>,
17
18 #[serde(default)]
20 pub groq_api_key: Option<String>,
21
22 #[serde(default)]
24 pub perplexity_api_key: Option<String>,
25
26 #[serde(default)]
29 pub alchemy_api_key: Option<String>,
30
31 #[serde(default)]
33 pub infura_api_key: Option<String>,
34
35 #[serde(default)]
37 pub quicknode_api_key: Option<String>,
38
39 #[serde(default)]
41 pub moralis_api_key: Option<String>,
42
43 #[serde(default)]
46 pub lifi_api_key: Option<String>,
47
48 #[serde(default)]
50 pub one_inch_api_key: Option<String>,
51
52 #[serde(default)]
54 pub zerox_api_key: Option<String>,
55
56 #[serde(default)]
59 pub dexscreener_api_key: Option<String>,
60
61 #[serde(default)]
63 pub coingecko_api_key: Option<String>,
64
65 #[serde(default)]
67 pub coinmarketcap_api_key: Option<String>,
68
69 #[serde(default)]
71 pub pump_api_key: Option<String>,
72
73 #[serde(default)]
75 pub pump_api_url: Option<String>,
76
77 #[serde(default)]
79 pub jupiter_api_url: Option<String>,
80
81 #[serde(default)]
84 pub twitter_bearer_token: Option<String>,
85
86 #[serde(default)]
88 pub exa_api_key: Option<String>,
89
90 #[serde(default)]
92 pub serper_api_key: Option<String>,
93
94 #[serde(default)]
97 pub lunarcrush_api_key: Option<String>,
98
99 #[serde(default)]
101 pub newsapi_key: Option<String>,
102
103 #[serde(default)]
105 pub cryptopanic_key: Option<String>,
106
107 #[serde(default)]
110 pub pocket_universe_api_key: Option<String>,
111
112 #[serde(default)]
114 pub trenchbot_api_key: Option<String>,
115
116 #[serde(default)]
118 pub rugcheck_api_key: Option<String>,
119
120 #[serde(default)]
122 pub tweetscout_api_key: Option<String>,
123
124 #[serde(default)]
126 pub faster100x_api_key: Option<String>,
127}
128
129impl ProvidersConfig {
130 pub fn has_ai_provider(&self, provider: AiProvider) -> bool {
132 match provider {
133 AiProvider::Anthropic => self.anthropic_api_key.is_some(),
134 AiProvider::OpenAI => self.openai_api_key.is_some(),
135 AiProvider::Groq => self.groq_api_key.is_some(),
136 AiProvider::Perplexity => self.perplexity_api_key.is_some(),
137 }
138 }
139
140 pub fn get_ai_key(&self, provider: AiProvider) -> Option<&str> {
142 match provider {
143 AiProvider::Anthropic => self.anthropic_api_key.as_deref(),
144 AiProvider::OpenAI => self.openai_api_key.as_deref(),
145 AiProvider::Groq => self.groq_api_key.as_deref(),
146 AiProvider::Perplexity => self.perplexity_api_key.as_deref(),
147 }
148 }
149
150 pub fn has_blockchain_provider(&self, provider: BlockchainProvider) -> bool {
152 match provider {
153 BlockchainProvider::Alchemy => self.alchemy_api_key.is_some(),
154 BlockchainProvider::Infura => self.infura_api_key.is_some(),
155 BlockchainProvider::QuickNode => self.quicknode_api_key.is_some(),
156 BlockchainProvider::Moralis => self.moralis_api_key.is_some(),
157 }
158 }
159
160 pub fn get_blockchain_key(&self, provider: BlockchainProvider) -> Option<&str> {
162 match provider {
163 BlockchainProvider::Alchemy => self.alchemy_api_key.as_deref(),
164 BlockchainProvider::Infura => self.infura_api_key.as_deref(),
165 BlockchainProvider::QuickNode => self.quicknode_api_key.as_deref(),
166 BlockchainProvider::Moralis => self.moralis_api_key.as_deref(),
167 }
168 }
169
170 pub fn has_data_provider(&self, provider: DataProvider) -> bool {
172 match provider {
173 DataProvider::DexScreener => self.dexscreener_api_key.is_some(),
174 DataProvider::CoinGecko => self.coingecko_api_key.is_some(),
175 DataProvider::CoinMarketCap => self.coinmarketcap_api_key.is_some(),
176 DataProvider::Twitter => self.twitter_bearer_token.is_some(),
177 DataProvider::LunarCrush => self.lunarcrush_api_key.is_some(),
178 }
179 }
180
181 pub fn validate_config(&self) -> ConfigResult<()> {
183 if let Some(ref key) = self.anthropic_api_key {
185 if key.is_empty() {
186 return Err(ConfigError::validation("ANTHROPIC_API_KEY cannot be empty"));
187 }
188 }
189
190 if let Some(ref token) = self.twitter_bearer_token {
191 if !token.is_empty() && !token.starts_with("Bearer ") {
192 return Err(ConfigError::validation(
193 "TWITTER_BEARER_TOKEN must start with 'Bearer ' if it is set",
194 ));
195 }
196 }
197
198 Ok(())
200 }
201}
202
203#[derive(Debug, Clone, Copy, PartialEq, Eq)]
205pub enum AiProvider {
206 Anthropic,
208 OpenAI,
210 Groq,
212 Perplexity,
214}
215
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
218pub enum BlockchainProvider {
219 Alchemy,
221 Infura,
223 QuickNode,
225 Moralis,
227}
228
229#[derive(Debug, Clone, Copy, PartialEq, Eq)]
231pub enum DataProvider {
232 DexScreener,
234 CoinGecko,
236 CoinMarketCap,
238 Twitter,
240 LunarCrush,
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn test_providers_config_default() {
250 let config = ProvidersConfig::default();
251
252 assert!(config.anthropic_api_key.is_none());
254 assert!(config.openai_api_key.is_none());
255 assert!(config.groq_api_key.is_none());
256 assert!(config.perplexity_api_key.is_none());
257 assert!(config.alchemy_api_key.is_none());
258 assert!(config.infura_api_key.is_none());
259 assert!(config.quicknode_api_key.is_none());
260 assert!(config.moralis_api_key.is_none());
261 assert!(config.lifi_api_key.is_none());
262 assert!(config.one_inch_api_key.is_none());
263 assert!(config.zerox_api_key.is_none());
264 assert!(config.dexscreener_api_key.is_none());
265 assert!(config.coingecko_api_key.is_none());
266 assert!(config.coinmarketcap_api_key.is_none());
267 assert!(config.pump_api_key.is_none());
268 assert!(config.twitter_bearer_token.is_none());
269 assert!(config.exa_api_key.is_none());
270 assert!(config.serper_api_key.is_none());
271 assert!(config.lunarcrush_api_key.is_none());
272 assert!(config.newsapi_key.is_none());
273 assert!(config.pocket_universe_api_key.is_none());
274 assert!(config.trenchbot_api_key.is_none());
275 assert!(config.rugcheck_api_key.is_none());
276 assert!(config.tweetscout_api_key.is_none());
277 assert!(config.faster100x_api_key.is_none());
278 }
279
280 #[test]
281 fn test_has_ai_provider_when_keys_are_none_should_return_false() {
282 let config = ProvidersConfig::default();
283
284 assert!(!config.has_ai_provider(AiProvider::Anthropic));
285 assert!(!config.has_ai_provider(AiProvider::OpenAI));
286 assert!(!config.has_ai_provider(AiProvider::Groq));
287 assert!(!config.has_ai_provider(AiProvider::Perplexity));
288 }
289
290 #[test]
291 fn test_has_ai_provider_when_keys_are_some_should_return_true() {
292 let config = ProvidersConfig {
293 anthropic_api_key: Some("test_anthropic_key".to_string()),
294 openai_api_key: Some("test_openai_key".to_string()),
295 groq_api_key: Some("test_groq_key".to_string()),
296 perplexity_api_key: Some("test_perplexity_key".to_string()),
297 ..Default::default()
298 };
299
300 assert!(config.has_ai_provider(AiProvider::Anthropic));
301 assert!(config.has_ai_provider(AiProvider::OpenAI));
302 assert!(config.has_ai_provider(AiProvider::Groq));
303 assert!(config.has_ai_provider(AiProvider::Perplexity));
304 }
305
306 #[test]
307 fn test_get_ai_key_when_keys_are_none_should_return_none() {
308 let config = ProvidersConfig::default();
309
310 assert!(config.get_ai_key(AiProvider::Anthropic).is_none());
311 assert!(config.get_ai_key(AiProvider::OpenAI).is_none());
312 assert!(config.get_ai_key(AiProvider::Groq).is_none());
313 assert!(config.get_ai_key(AiProvider::Perplexity).is_none());
314 }
315
316 #[test]
317 fn test_get_ai_key_when_keys_are_some_should_return_key() {
318 let config = ProvidersConfig {
319 anthropic_api_key: Some("test_anthropic_key".to_string()),
320 openai_api_key: Some("test_openai_key".to_string()),
321 groq_api_key: Some("test_groq_key".to_string()),
322 perplexity_api_key: Some("test_perplexity_key".to_string()),
323 ..Default::default()
324 };
325
326 assert_eq!(
327 config.get_ai_key(AiProvider::Anthropic),
328 Some("test_anthropic_key")
329 );
330 assert_eq!(
331 config.get_ai_key(AiProvider::OpenAI),
332 Some("test_openai_key")
333 );
334 assert_eq!(config.get_ai_key(AiProvider::Groq), Some("test_groq_key"));
335 assert_eq!(
336 config.get_ai_key(AiProvider::Perplexity),
337 Some("test_perplexity_key")
338 );
339 }
340
341 #[test]
342 fn test_has_blockchain_provider_when_keys_are_none_should_return_false() {
343 let config = ProvidersConfig::default();
344
345 assert!(!config.has_blockchain_provider(BlockchainProvider::Alchemy));
346 assert!(!config.has_blockchain_provider(BlockchainProvider::Infura));
347 assert!(!config.has_blockchain_provider(BlockchainProvider::QuickNode));
348 assert!(!config.has_blockchain_provider(BlockchainProvider::Moralis));
349 }
350
351 #[test]
352 fn test_has_blockchain_provider_when_keys_are_some_should_return_true() {
353 let config = ProvidersConfig {
354 alchemy_api_key: Some("test_alchemy_key".to_string()),
355 infura_api_key: Some("test_infura_key".to_string()),
356 quicknode_api_key: Some("test_quicknode_key".to_string()),
357 moralis_api_key: Some("test_moralis_key".to_string()),
358 ..Default::default()
359 };
360
361 assert!(config.has_blockchain_provider(BlockchainProvider::Alchemy));
362 assert!(config.has_blockchain_provider(BlockchainProvider::Infura));
363 assert!(config.has_blockchain_provider(BlockchainProvider::QuickNode));
364 assert!(config.has_blockchain_provider(BlockchainProvider::Moralis));
365 }
366
367 #[test]
368 fn test_get_blockchain_key_when_keys_are_none_should_return_none() {
369 let config = ProvidersConfig::default();
370
371 assert!(config
372 .get_blockchain_key(BlockchainProvider::Alchemy)
373 .is_none());
374 assert!(config
375 .get_blockchain_key(BlockchainProvider::Infura)
376 .is_none());
377 assert!(config
378 .get_blockchain_key(BlockchainProvider::QuickNode)
379 .is_none());
380 assert!(config
381 .get_blockchain_key(BlockchainProvider::Moralis)
382 .is_none());
383 }
384
385 #[test]
386 fn test_get_blockchain_key_when_keys_are_some_should_return_key() {
387 let config = ProvidersConfig {
388 alchemy_api_key: Some("test_alchemy_key".to_string()),
389 infura_api_key: Some("test_infura_key".to_string()),
390 quicknode_api_key: Some("test_quicknode_key".to_string()),
391 moralis_api_key: Some("test_moralis_key".to_string()),
392 ..Default::default()
393 };
394
395 assert_eq!(
396 config.get_blockchain_key(BlockchainProvider::Alchemy),
397 Some("test_alchemy_key")
398 );
399 assert_eq!(
400 config.get_blockchain_key(BlockchainProvider::Infura),
401 Some("test_infura_key")
402 );
403 assert_eq!(
404 config.get_blockchain_key(BlockchainProvider::QuickNode),
405 Some("test_quicknode_key")
406 );
407 assert_eq!(
408 config.get_blockchain_key(BlockchainProvider::Moralis),
409 Some("test_moralis_key")
410 );
411 }
412
413 #[test]
414 fn test_has_data_provider_when_keys_are_none_should_return_false() {
415 let config = ProvidersConfig::default();
416
417 assert!(!config.has_data_provider(DataProvider::DexScreener));
418 assert!(!config.has_data_provider(DataProvider::CoinGecko));
419 assert!(!config.has_data_provider(DataProvider::CoinMarketCap));
420 assert!(!config.has_data_provider(DataProvider::Twitter));
421 assert!(!config.has_data_provider(DataProvider::LunarCrush));
422 }
423
424 #[test]
425 fn test_has_data_provider_when_keys_are_some_should_return_true() {
426 let config = ProvidersConfig {
427 dexscreener_api_key: Some("test_dexscreener_key".to_string()),
428 coingecko_api_key: Some("test_coingecko_key".to_string()),
429 coinmarketcap_api_key: Some("test_coinmarketcap_key".to_string()),
430 twitter_bearer_token: Some("test_twitter_token".to_string()),
431 lunarcrush_api_key: Some("test_lunarcrush_key".to_string()),
432 ..Default::default()
433 };
434
435 assert!(config.has_data_provider(DataProvider::DexScreener));
436 assert!(config.has_data_provider(DataProvider::CoinGecko));
437 assert!(config.has_data_provider(DataProvider::CoinMarketCap));
438 assert!(config.has_data_provider(DataProvider::Twitter));
439 assert!(config.has_data_provider(DataProvider::LunarCrush));
440 }
441
442 #[test]
443 fn test_validate_when_all_keys_are_none_should_return_ok() {
444 let config = ProvidersConfig::default();
445 assert!(config.validate_config().is_ok());
446 }
447
448 #[test]
449 fn test_validate_when_anthropic_key_is_empty_should_return_err() {
450 let config = ProvidersConfig {
451 anthropic_api_key: Some("".to_string()),
452 ..Default::default()
453 };
454
455 let result = config.validate_config();
456 assert!(result.is_err());
457 if let Err(error) = result {
458 let error_message = format!("{}", error);
459 assert!(error_message.contains("ANTHROPIC_API_KEY cannot be empty"));
460 }
461 }
462
463 #[test]
464 fn test_validate_when_anthropic_key_is_valid_should_return_ok() {
465 let config = ProvidersConfig {
466 anthropic_api_key: Some("valid_key".to_string()),
467 ..Default::default()
468 };
469 assert!(config.validate_config().is_ok());
470 }
471
472 #[test]
473 fn test_validate_when_twitter_token_has_bearer_prefix_should_return_ok() {
474 let config = ProvidersConfig {
475 twitter_bearer_token: Some("Bearer valid_token".to_string()),
476 ..Default::default()
477 };
478 assert!(config.validate_config().is_ok());
479 }
480
481 #[test]
482 fn test_validate_when_twitter_token_missing_bearer_prefix_should_return_err() {
483 let config = ProvidersConfig {
484 twitter_bearer_token: Some("valid_token_without_bearer".to_string()),
485 ..Default::default()
486 };
487 let result = config.validate_config();
489 assert!(result.is_err());
490 if let Err(error) = result {
491 let error_message = format!("{}", error);
492 assert!(error_message.contains("TWITTER_BEARER_TOKEN must start with 'Bearer '"));
493 }
494 }
495
496 #[test]
497 fn test_validate_when_twitter_token_is_empty_should_return_ok() {
498 let config = ProvidersConfig {
499 twitter_bearer_token: Some("".to_string()),
500 ..Default::default()
501 };
502 assert!(config.validate_config().is_ok());
503 }
504
505 #[test]
506 fn test_ai_provider_debug_display() {
507 assert_eq!(format!("{:?}", AiProvider::Anthropic), "Anthropic");
509 assert_eq!(format!("{:?}", AiProvider::OpenAI), "OpenAI");
510 assert_eq!(format!("{:?}", AiProvider::Groq), "Groq");
511 assert_eq!(format!("{:?}", AiProvider::Perplexity), "Perplexity");
512 }
513
514 #[test]
515 fn test_ai_provider_clone_and_copy() {
516 let provider = AiProvider::Anthropic;
517 let cloned = provider.clone();
518 let copied = provider;
519
520 assert_eq!(provider, cloned);
521 assert_eq!(provider, copied);
522 }
523
524 #[test]
525 fn test_ai_provider_equality() {
526 assert_eq!(AiProvider::Anthropic, AiProvider::Anthropic);
527 assert_ne!(AiProvider::Anthropic, AiProvider::OpenAI);
528 assert_ne!(AiProvider::OpenAI, AiProvider::Groq);
529 assert_ne!(AiProvider::Groq, AiProvider::Perplexity);
530 }
531
532 #[test]
533 fn test_blockchain_provider_debug_display() {
534 assert_eq!(format!("{:?}", BlockchainProvider::Alchemy), "Alchemy");
535 assert_eq!(format!("{:?}", BlockchainProvider::Infura), "Infura");
536 assert_eq!(format!("{:?}", BlockchainProvider::QuickNode), "QuickNode");
537 assert_eq!(format!("{:?}", BlockchainProvider::Moralis), "Moralis");
538 }
539
540 #[test]
541 fn test_blockchain_provider_clone_and_copy() {
542 let provider = BlockchainProvider::Alchemy;
543 let cloned = provider.clone();
544 let copied = provider;
545
546 assert_eq!(provider, cloned);
547 assert_eq!(provider, copied);
548 }
549
550 #[test]
551 fn test_blockchain_provider_equality() {
552 assert_eq!(BlockchainProvider::Alchemy, BlockchainProvider::Alchemy);
553 assert_ne!(BlockchainProvider::Alchemy, BlockchainProvider::Infura);
554 assert_ne!(BlockchainProvider::Infura, BlockchainProvider::QuickNode);
555 assert_ne!(BlockchainProvider::QuickNode, BlockchainProvider::Moralis);
556 }
557
558 #[test]
559 fn test_data_provider_debug_display() {
560 assert_eq!(format!("{:?}", DataProvider::DexScreener), "DexScreener");
561 assert_eq!(format!("{:?}", DataProvider::CoinGecko), "CoinGecko");
562 assert_eq!(
563 format!("{:?}", DataProvider::CoinMarketCap),
564 "CoinMarketCap"
565 );
566 assert_eq!(format!("{:?}", DataProvider::Twitter), "Twitter");
567 assert_eq!(format!("{:?}", DataProvider::LunarCrush), "LunarCrush");
568 }
569
570 #[test]
571 fn test_data_provider_clone_and_copy() {
572 let provider = DataProvider::DexScreener;
573 let cloned = provider.clone();
574 let copied = provider;
575
576 assert_eq!(provider, cloned);
577 assert_eq!(provider, copied);
578 }
579
580 #[test]
581 fn test_data_provider_equality() {
582 assert_eq!(DataProvider::DexScreener, DataProvider::DexScreener);
583 assert_ne!(DataProvider::DexScreener, DataProvider::CoinGecko);
584 assert_ne!(DataProvider::CoinGecko, DataProvider::CoinMarketCap);
585 assert_ne!(DataProvider::Twitter, DataProvider::LunarCrush);
586 }
587
588 #[test]
589 fn test_providers_config_debug_display() {
590 let config = ProvidersConfig {
591 anthropic_api_key: Some("test_key".to_string()),
592 ..Default::default()
593 };
594 let debug_str = format!("{:?}", config);
595 assert!(debug_str.contains("ProvidersConfig"));
596 assert!(debug_str.contains("anthropic_api_key"));
597 }
598
599 #[test]
600 fn test_providers_config_clone() {
601 let config = ProvidersConfig {
602 anthropic_api_key: Some("test_key".to_string()),
603 openai_api_key: Some("openai_key".to_string()),
604 ..Default::default()
605 };
606 let cloned = config.clone();
607
608 assert_eq!(config.anthropic_api_key, cloned.anthropic_api_key);
609 assert_eq!(config.openai_api_key, cloned.openai_api_key);
610 assert_eq!(config.groq_api_key, cloned.groq_api_key);
611 }
612
613 #[test]
614 fn test_serde_serialization_deserialization() {
615 let config = ProvidersConfig {
616 anthropic_api_key: Some("anthropic_test".to_string()),
617 openai_api_key: Some("openai_test".to_string()),
618 twitter_bearer_token: Some("Bearer twitter_test".to_string()),
619 ..Default::default()
620 };
621
622 let serialized = serde_json::to_string(&config).unwrap();
624 assert!(serialized.contains("anthropic_test"));
625 assert!(serialized.contains("openai_test"));
626 assert!(serialized.contains("Bearer twitter_test"));
627
628 let deserialized: ProvidersConfig = serde_json::from_str(&serialized).unwrap();
630 assert_eq!(config.anthropic_api_key, deserialized.anthropic_api_key);
631 assert_eq!(config.openai_api_key, deserialized.openai_api_key);
632 assert_eq!(
633 config.twitter_bearer_token,
634 deserialized.twitter_bearer_token
635 );
636 }
637
638 #[test]
639 fn test_serde_default_fields() {
640 let json = "{}";
642 let config: ProvidersConfig = serde_json::from_str(json).unwrap();
643
644 assert!(config.anthropic_api_key.is_none());
645 assert!(config.openai_api_key.is_none());
646 assert!(config.groq_api_key.is_none());
647 assert!(config.perplexity_api_key.is_none());
648 }
649
650 #[test]
651 fn test_mixed_provider_configuration() {
652 let config = ProvidersConfig {
653 anthropic_api_key: Some("anthropic_key".to_string()),
654 alchemy_api_key: Some("alchemy_key".to_string()),
655 dexscreener_api_key: Some("dex_key".to_string()),
656 ..Default::default()
658 };
659
660 assert!(config.has_ai_provider(AiProvider::Anthropic));
662 assert!(!config.has_ai_provider(AiProvider::OpenAI));
663
664 assert!(config.has_blockchain_provider(BlockchainProvider::Alchemy));
666 assert!(!config.has_blockchain_provider(BlockchainProvider::Infura));
667
668 assert!(config.has_data_provider(DataProvider::DexScreener));
670 assert!(!config.has_data_provider(DataProvider::CoinGecko));
671 }
672}