Skip to content

Instantly share code, notes, and snippets.

@RandyMcMillan
Forked from rust-play/playground.rs
Last active January 26, 2026 05:15
Show Gist options
  • Select an option

  • Save RandyMcMillan/2c848eb6ee60d644f49e0e0f19c6ceb6 to your computer and use it in GitHub Desktop.

Select an option

Save RandyMcMillan/2c848eb6ee60d644f49e0e0f19c6ceb6 to your computer and use it in GitHub Desktop.
myip.rs
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(())
}
@RandyMcMillan
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment