use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT};
use crate::error::{ApiErrorResponse, Error, Result};
use crate::types::{Asn, BulkResponse, Carrier, Currency, IpInfo, Threat, TimeZone};
const DEFAULT_BASE_URL: &str = "https://api.ipdata.co";
const EU_BASE_URL: &str = "https://eu-api.ipdata.co";
const BULK_LIMIT: usize = 100;
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Clone)]
pub struct IpData {
client: reqwest::Client,
api_key: String,
base_url: String,
}
impl IpData {
pub fn new(api_key: impl Into<String>) -> Self {
Self::with_base_url(api_key, DEFAULT_BASE_URL)
}
pub fn eu(api_key: impl Into<String>) -> Self {
Self::with_base_url(api_key, EU_BASE_URL)
}
pub fn with_base_url(api_key: impl Into<String>, base_url: impl Into<String>) -> Self {
let mut headers = HeaderMap::new();
let ua = format!("ipdata-rust/{VERSION}");
headers.insert(USER_AGENT, HeaderValue::from_str(&ua).unwrap());
let client = reqwest::Client::builder()
.default_headers(headers)
.build()
.expect("failed to build HTTP client");
Self {
client,
api_key: api_key.into(),
base_url: base_url.into().trim_end_matches('/').to_string(),
}
}
pub async fn lookup(&self, ip: &str) -> Result<IpInfo> {
validate_ip(ip)?;
let url = format!("{}/{}", self.base_url, ip);
self.get(&url, &[]).await
}
pub async fn lookup_self(&self) -> Result<IpInfo> {
self.get(&self.base_url, &[]).await
}
pub async fn lookup_fields(&self, ip: &str, fields: &[&str]) -> Result<IpInfo> {
validate_ip(ip)?;
let url = format!("{}/{}", self.base_url, ip);
self.get(&url, fields).await
}
pub async fn lookup_field(
&self,
ip: &str,
field: &str,
) -> Result<serde_json::Value> {
validate_ip(ip)?;
let url = format!("{}/{}/{}", self.base_url, ip, field);
let resp = self
.client
.get(&url)
.query(&[("api-key", &self.api_key)])
.send()
.await?;
let status = resp.status();
if !status.is_success() {
return Err(parse_error(status.as_u16(), resp).await);
}
Ok(resp.json().await?)
}
pub async fn asn(&self, ip: &str) -> Result<Asn> {
validate_ip(ip)?;
let url = format!("{}/{}/asn", self.base_url, ip);
self.get_typed(&url).await
}
pub async fn carrier(&self, ip: &str) -> Result<Carrier> {
validate_ip(ip)?;
let url = format!("{}/{}/carrier", self.base_url, ip);
self.get_typed(&url).await
}
pub async fn currency(&self, ip: &str) -> Result<Currency> {
validate_ip(ip)?;
let url = format!("{}/{}/currency", self.base_url, ip);
self.get_typed(&url).await
}
pub async fn time_zone(&self, ip: &str) -> Result<TimeZone> {
validate_ip(ip)?;
let url = format!("{}/{}/time_zone", self.base_url, ip);
self.get_typed(&url).await
}
pub async fn threat(&self, ip: &str) -> Result<Threat> {
validate_ip(ip)?;
let url = format!("{}/{}/threat", self.base_url, ip);
self.get_typed(&url).await
}
pub async fn bulk(&self, ips: &[&str]) -> Result<Vec<IpInfo>> {
if ips.is_empty() {
return Err(Error::BulkEmpty);
}
if ips.len() > BULK_LIMIT {
return Err(Error::BulkLimitExceeded);
}
for ip in ips {
validate_ip(ip)?;
}
let url = format!("{}/bulk", self.base_url);
let resp = self
.client
.post(&url)
.query(&[("api-key", &self.api_key)])
.json(&ips)
.send()
.await?;
let status = resp.status();
if !status.is_success() {
return Err(parse_error(status.as_u16(), resp).await);
}
let bulk: BulkResponse = resp.json().await?;
Ok(bulk.responses)
}
async fn get_typed<T: serde::de::DeserializeOwned>(&self, url: &str) -> Result<T> {
let resp = self
.client
.get(url)
.query(&[("api-key", &self.api_key)])
.send()
.await?;
let status = resp.status();
if !status.is_success() {
return Err(parse_error(status.as_u16(), resp).await);
}
Ok(resp.json().await?)
}
async fn get(&self, url: &str, fields: &[&str]) -> Result<IpInfo> {
let mut req = self.client.get(url).query(&[("api-key", &self.api_key)]);
if !fields.is_empty() {
let fields_str = fields.join(",");
req = req.query(&[("fields", &fields_str)]);
}
let resp = req.send().await?;
let status = resp.status();
if !status.is_success() {
return Err(parse_error(status.as_u16(), resp).await);
}
Ok(resp.json().await?)
}
}
fn validate_ip(ip: &str) -> Result<()> {
ip.parse::<std::net::IpAddr>()
.map(|_| ())
.map_err(|_| Error::InvalidIp(ip.to_string()))
}
async fn parse_error(status: u16, resp: reqwest::Response) -> Error {
let message = match resp.json::<ApiErrorResponse>().await {
Ok(body) => body.message,
Err(_) => format!("request failed with status {status}"),
};
Error::Api { status, message }
}