Skip to content

Commit

Permalink
Merge pull request #30 from ipinfo/faizan/add-single-lookup-support
Browse files Browse the repository at this point in the history
Adds single lookup support
  • Loading branch information
UmanShahzad authored Feb 14, 2023
2 parents 7c7cb06 + bcfd18d commit 121e653
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 32 deletions.
21 changes: 15 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,17 @@ use ipinfo::{IpInfo, IpInfoConfig};
fn main() {
let config = IpInfoConfig { token: Some("my token".to_string()), ..Default::default() };
let mut ipinfo = IpInfo::new(config).expect("should construct");
let res = ipinfo.lookup(&["8.8.8.8", "4.2.2.4"]);

// ability to lookup details for single ip
let res = ipinfo.lookup("8.8.8.8");
match res {
ok(r) => println!("{}: {:?}", "8.8.8.8", r.hostname),
Err(e) => println!("error occurred: {}", &e.to_string()),
}

// or batch ips to lookup at once
let res_batch = ipinfo.lookup_batch(&["8.8.8.8", "4.2.2.4"]);
match res_batch {
Ok(r) => println!("{}: {:?}", "8.8.8.8", r["8.8.8.8"].hostname),
Err(e) => println!("error occurred: {}", &e.to_string()),
}
Expand All @@ -57,11 +65,12 @@ When looking up an IP address, the `response` includes `country_name` which is t
which includes code and symbol of a country's currency and `continent` which includes code and name of the continent.

```rust
println!("{}: {}", "8.8.8.8", r["8.8.8.8"].country_name) // United States
println!("{}: {:?}", "8.8.8.8", r["8.8.8.8"].is_eu) // Some(false)
println!("{}: {:?}", "8.8.8.8", r["8.8.8.8"].country_flag) // Some(CountryFlag { emoji: "🇺🇸", unicode: "U+1F1FA U+1F1F8" })
println!("{}: {:?}", "8.8.8.8", r["8.8.8.8"].country_currency) // Some(CountryCurrency { code: "USD", symbol: "$" })
println!("{}: {:?}", "8.8.8.8", r["8.8.8.8"].continent) // Some(Continent { code: "NA", name: "North America" })
let r = ipinfo.lookup("8.8.8.8");
println!("{}: {}", "8.8.8.8", r.country_name) // United States
println!("{}: {:?}", "8.8.8.8", r.is_eu) // Some(false)
println!("{}: {:?}", "8.8.8.8", r.country_flag) // Some(CountryFlag { emoji: "🇺🇸", unicode: "U+1F1FA U+1F1F8" })
println!("{}: {:?}", "8.8.8.8", r.country_currency) // Some(CountryCurrency { code: "USD", symbol: "$" })
println!("{}: {:?}", "8.8.8.8", r.continent) // Some(Continent { code: "NA", name: "North America" })
```

It is possible to return the country name in other languages, change the EU countries and change the flag emoji or unicode by setting the paths of `countries_file_path`, `eu_file_path`, `country_flags_file_path`, `country_currencies_file_path` and `continents_file_path` when creating the `IPinfo` client.
Expand Down
106 changes: 82 additions & 24 deletions src/ipinfo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,9 @@ impl IpInfo {
/// use ipinfo::IpInfo;
///
/// let mut ipinfo = IpInfo::new(Default::default()).expect("should construct");
/// let res = ipinfo.lookup(&["8.8.8.8"]).expect("should run");
/// let res = ipinfo.lookup_batch(&["8.8.8.8"]).expect("should run");
/// ```
pub fn lookup(
pub fn lookup_batch(
&mut self,
ips: &[&str],
) -> Result<HashMap<String, IpDetails>, IpError> {
Expand Down Expand Up @@ -207,19 +207,8 @@ impl IpInfo {

// Add country_name and EU status to response
for detail in details.to_owned() {
let mut_details = details.get_mut(&detail.0).unwrap();
let country = &mut_details.country;
if !country.is_empty() {
let country_name = self.countries.get(&mut_details.country).unwrap();
mut_details.country_name = Some(country_name.to_string());
mut_details.is_eu = Some(self.eu.contains(country));
let country_flag = self.country_flags.get(&mut_details.country).unwrap();
mut_details.country_flag = Some(country_flag.to_owned());
let country_currency = self.country_currencies.get(&mut_details.country).unwrap();
mut_details.country_currency = Some(country_currency.to_owned());
let continent = self.continents.get(&mut_details.country).unwrap();
mut_details.continent = Some(continent.to_owned());
}
let mut mut_details = details.get_mut(&detail.0).unwrap();
self.populate_static_details(&mut mut_details);
}

// Update cache
Expand All @@ -235,6 +224,76 @@ impl IpInfo {
Ok(details)
}

/// looks up IPDetails for a single IP Address
///
/// # Example
///
/// ```no_run
/// use ipinfo::IpInfo;
///
/// let mut ipinfo = IpInfo::new(Default::default()).expect("should construct");
/// let res = ipinfo.lookup("8.8.8.8").expect("should run");
/// ```
pub fn lookup(
&mut self,
ip: &str,
) -> Result<IpDetails, IpError> {
// Check for cache hit
let cached_detail = self.cache.get(&ip.to_string());

if !cached_detail.is_none() {
return Ok(cached_detail.unwrap().clone());
}

// lookup in case of a cache miss
let response = self
.client
.get(&format!("{}/{}", self.url, ip))
.headers(Self::construct_headers())
.bearer_auth(&self.token.as_ref().unwrap_or(&"".to_string()))
.send()?;

// Check if we exhausted our request quota
if let reqwest::StatusCode::TOO_MANY_REQUESTS = response.status() {
return Err(err!(RateLimitExceededError));
}

// Acquire response
let raw_resp = response.error_for_status()?.text()?;

// Parse the response
let resp: serde_json::Value = serde_json::from_str(&raw_resp)?;

// Return if an error occurred
if let Some(e) = resp["error"].as_str() {
return Err(err!(IpRequestError, e));
}

// Parse the results and add additional country details
let mut details: IpDetails = serde_json::from_str(&raw_resp)?;
self.populate_static_details(&mut details);

// update cache
self.cache.put(ip.to_string(), details.clone());

Ok(details)
}

// Add country details and EU status to response
fn populate_static_details(&self, details: &mut IpDetails) {
if !&details.country.is_empty() {
let country_name = self.countries.get(&details.country).unwrap();
details.country_name = Some(country_name.to_string());
details.is_eu = Some(self.eu.contains(&details.country));
let country_flag = self.country_flags.get(&details.country).unwrap();
details.country_flag = Some(country_flag.to_owned());
let country_currency = self.country_currencies.get(&details.country).unwrap();
details.country_currency = Some(country_currency.to_owned());
let continent = self.continents.get(&details.country).unwrap();
details.continent = Some(continent.to_owned());
}
}

/// Construct API request headers.
fn construct_headers() -> HeaderMap {
let mut headers = HeaderMap::new();
Expand Down Expand Up @@ -292,19 +351,18 @@ mod tests {
fn request_single_ip() {
let mut ipinfo = get_ipinfo_client();

let details = ipinfo.lookup(&["66.87.125.72"]).expect("should lookup");
let details = ipinfo.lookup("66.87.125.72").expect("should lookup");

assert!(details.contains_key("66.87.125.72"));
assert_eq!(details.len(), 1);
assert_eq!(details.ip, "66.87.125.72");
}

#[test]
fn request_single_ip_no_token() {
fn request_no_token() {
let mut ipinfo =
IpInfo::new(Default::default()).expect("should construct");

assert_eq!(
ipinfo.lookup(&["8.8.8.8"]).err().unwrap().kind(),
ipinfo.lookup_batch(&["8.8.8.8"]).err().unwrap().kind(),
IpErrorKind::IpRequestError
);
}
Expand All @@ -314,7 +372,7 @@ mod tests {
let mut ipinfo = get_ipinfo_client();

let details = ipinfo
.lookup(&["8.8.8.8", "4.2.2.4"])
.lookup_batch(&["8.8.8.8", "4.2.2.4"])
.expect("should lookup");

// Assert successful lookup
Expand Down Expand Up @@ -352,15 +410,15 @@ mod tests {
let mut ipinfo = get_ipinfo_client();

// Populate the cache with 8.8.8.8
let details = ipinfo.lookup(&["8.8.8.8"]).expect("should lookup");
let details = ipinfo.lookup_batch(&["8.8.8.8"]).expect("should lookup");

// Assert 1 result
assert!(details.contains_key("8.8.8.8"));
assert_eq!(details.len(), 1);

// Should have a cache hit for 8.8.8.8 and query for 4.2.2.4
let details = ipinfo
.lookup(&["4.2.2.4", "8.8.8.8"])
.lookup_batch(&["4.2.2.4", "8.8.8.8"])
.expect("should lookup");

// Assert 2 results
Expand Down
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@
//!
//! // Setup IpInfo structure and start looking up IP addresses.
//! let mut ipinfo = IpInfo::new(config).expect("should construct");
//! let res = ipinfo.lookup(&["8.8.8.8", "4.2.2.4"]);
//! let res = ipinfo.lookup("8.8.8.8");
//!
//! match res {
//! Ok(r) => println!("{}: {}", "8.8.8.8", r["8.8.8.8"].hostname.as_ref().unwrap()),
//! Ok(r) => println!("{}: {}", "8.8.8.8", r.hostname.as_ref().unwrap()),
//! Err(e) => println!("error occurred: {}", &e.to_string()),
//! }
//! }
Expand Down

0 comments on commit 121e653

Please sign in to comment.