-
-
Save RandyMcMillan/2c848eb6ee60d644f49e0e0f19c6ceb6 to your computer and use it in GitHub Desktop.
myip.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| use anyhow::{anyhow, Result}; | |
| use serde::Deserialize; | |
| use async_trait::async_trait; // Using async-trait from your list | |
| #[derive(Debug, Deserialize, Clone)] | |
| struct SimpleIpResponse { | |
| ip: String, | |
| } | |
| // 1. Define a trait for the fetching logic | |
| #[async_trait] | |
| trait IpFetcher: Send + Sync { | |
| async fn fetch_ip(&self, url: &str) -> Result<String>; | |
| } | |
| // 2. Real implementation using reqwest | |
| struct HttpFetcher { | |
| client: reqwest::Client, | |
| } | |
| #[async_trait] | |
| impl IpFetcher for HttpFetcher { | |
| async fn fetch_ip(&self, url: &str) -> Result<String> { | |
| let res = self.client.get(url).send().await?.json::<SimpleIpResponse>().await?; | |
| Ok(res.ip) | |
| } | |
| } | |
| #[derive(Debug)] | |
| enum Provider { | |
| MyIp, | |
| GetJsonIp, | |
| Ipify, | |
| IpQuery, | |
| } | |
| impl Provider { | |
| fn url(&self) -> &'static str { | |
| match self { | |
| Provider::MyIp => "https://api.myip.com", | |
| Provider::GetJsonIp => "https://getjsonip.com", | |
| Provider::Ipify => "https://api.ipify.org?format=json", | |
| Provider::IpQuery => "https://api.ipquery.io", | |
| } | |
| } | |
| } | |
| // 3. IpClient now takes a generic Fetcher | |
| struct IpClient<T: IpFetcher> { | |
| fetcher: T, | |
| providers: Vec<Provider>, | |
| } | |
| impl<T: IpFetcher> IpClient<T> { | |
| async fn get_ip_with_fallback(&self) -> Result<String> { | |
| for provider in &self.providers { | |
| match self.fetcher.fetch_ip(provider.url()).await { | |
| Ok(ip) => return Ok(ip), | |
| Err(_) => continue, // Silently try next on failure | |
| } | |
| } | |
| Err(anyhow!("All IP providers failed")) | |
| } | |
| } | |
| // --- Testing Module --- | |
| #[cfg(test)] | |
| mod tests { | |
| use super::*; | |
| use std::sync::atomic::{AtomicUsize, Ordering}; | |
| use std::sync::Arc; | |
| // A mock fetcher that returns errors or successes based on a counter | |
| struct MockFetcher { | |
| success_at_index: usize, | |
| call_count: Arc<AtomicUsize>, | |
| } | |
| #[async_trait] | |
| impl IpFetcher for MockFetcher { | |
| async fn fetch_ip(&self, _url: &str) -> Result<String> { | |
| let count = self.call_count.fetch_add(1, Ordering::SeqCst); | |
| if count == self.success_at_index { | |
| Ok("127.0.0.1".to_string()) | |
| } else { | |
| Err(anyhow!("Mock failure")) | |
| } | |
| } | |
| } | |
| #[tokio::test] | |
| async fn test_fallback_logic() { | |
| let call_counter = Arc::new(AtomicUsize::new(0)); | |
| // We want it to fail twice and succeed on the 3rd attempt (index 2) | |
| let mock = MockFetcher { | |
| success_at_index: 2, | |
| call_count: Arc::clone(&call_counter), | |
| }; | |
| let client = IpClient { | |
| fetcher: mock, | |
| providers: vec![Provider::Ipify, Provider::MyIp, Provider::IpQuery], | |
| }; | |
| let result = client.get_ip_with_fallback().await; | |
| assert!(result.is_ok()); | |
| assert_eq!(result.unwrap(), "127.0.0.1"); | |
| // Verify it actually tried 3 times | |
| assert_eq!(call_counter.load(Ordering::SeqCst), 3); | |
| } | |
| #[tokio::test] | |
| async fn test_all_fail() { | |
| let mock = MockFetcher { | |
| success_at_index: 99, // Never reached | |
| call_count: Arc::new(AtomicUsize::new(0)), | |
| }; | |
| let client = IpClient { | |
| fetcher: mock, | |
| providers: vec![Provider::Ipify], | |
| }; | |
| let result = client.get_ip_with_fallback().await; | |
| assert!(result.is_err()); | |
| } | |
| } | |
| #[tokio::main] | |
| async fn main() -> Result<()> { | |
| // Production usage | |
| let http_fetcher = HttpFetcher { | |
| client: reqwest::Client::builder() | |
| .timeout(std::time::Duration::from_secs(5)) | |
| .build()?, | |
| }; | |
| let ip_client = IpClient { | |
| fetcher: http_fetcher, | |
| providers: vec![ | |
| Provider::Ipify, | |
| Provider::MyIp, | |
| Provider::IpQuery, | |
| Provider::GetJsonIp, | |
| ], | |
| }; | |
| let ip = ip_client.get_ip_with_fallback().await?; | |
| println!("External IP: {}", ip); | |
| Ok(()) | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=5cd804e3c8b42856a52975a6d471c0a7