iplocate/
lib.rs

1//! # IPLocate
2//!
3//! [IPLocate.io] is an internet service for finding data associated with Internet Protocol (IP)
4//! addresses, such as city, country, approximate location, timezone, and more.
5//!
6//! Before starting to use their service, take a look at their [terms of service].
7//!
8//! The `iplocate` crate provides a wrapper for IPLocate API, and it can be handled with ease!
9//!
10//! ```
11//! # extern crate iplocate;
12//! # fn main() -> iplocate::Result<()> {
13//! let ip = "8.8.8.8".parse().unwrap();
14//! let result = iplocate::lookup(ip)?;
15//! if let Some(ref country) = &result.geo_ip.country {
16//!     println!("The IP address {} comes from the {}.", ip, country);
17//! } else {
18//!     println!("The IP address {} does not belong to any country.", ip);
19//! }
20//! # Ok(())
21//! # }
22//! ```
23//!
24//! [IPLocate.io]: https://www.iplocate.io/
25//! [Terms of Service]: https://www.iplocate.io/legal
26extern crate chrono;
27extern crate reqwest;
28extern crate serde;
29#[macro_use]
30extern crate serde_derive;
31extern crate serde_json;
32
33use chrono::format::ParseResult;
34use chrono::offset::Utc;
35use chrono::DateTime;
36use std::fmt::{self, Display, Formatter};
37use std::net::IpAddr;
38use std::str::FromStr;
39
40/// An alias for [`reqwest::Result`] in which it will result in error in case of a failed HTTP
41/// request.
42///
43/// [`reqwest::Result`]: https://docs.rs/reqwest/0.9/reqwest/type.Result.html
44pub type Result<T> = reqwest::Result<T>;
45
46/// The main URL where it will request data from.
47const API_REQUEST_URL: &str = "https://www.iplocate.io/api/lookup";
48
49/// Looks up information for an IP address and returns an [`IpLocate`] on success.
50///
51/// [`IpLocate`]: struct.IpLocate.html
52pub fn lookup(ip: IpAddr) -> Result<IpLocate> {
53    Lookup::new(ip).lookup()
54}
55
56/// This type allows customization of `lookup` function.
57pub struct Lookup<'a> {
58    /// The IP address to be analyzed.
59    ip: IpAddr,
60    /// The response's format.
61    format: Option<Format>,
62    /// The IPLocate API's key.
63    apikey: Option<&'a str>,
64    /// The JSONP callback.
65    callback: Option<&'a str>,
66}
67
68impl<'a> Lookup<'a> {
69    /// Constructs a new `Lookup`.
70    pub fn new(ip: IpAddr) -> Self {
71        Lookup {
72            ip,
73            format: None,
74            apikey: None,
75            callback: None,
76        }
77    }
78
79    /// Sets the response's format.
80    pub fn format(&mut self, value: Format) -> &mut Self {
81        self.format = Some(value);
82        self
83    }
84
85    /// Sets the IPLocate API's key.
86    pub fn apikey(&mut self, value: &'a str) -> &mut Self {
87        self.apikey = Some(value);
88        self
89    }
90
91    /// Sets the JSONP callback.
92    pub fn callback(&mut self, value: &'a str) -> &mut Self {
93        self.callback = Some(value);
94        self
95    }
96
97    /// Requests for data without deserializing its content. Returns a [`String`] on success.
98    ///
99    /// [`String`]: https://doc.rust-lang.org/std/string/struct.String.html
100    pub fn raw_lookup(&self) -> Result<String> {
101        self.request()?.text()
102    }
103
104    /// Requests for data and deserializes its content. Returns an [`IpLocate`] on success.
105    ///
106    /// By default, this method sets `self.format` to be JSON type and `self.callback` to be
107    /// empty. Otherwise, it would panic at runtime.
108    ///
109    /// [`IpLocate`]: struct.IpLocate.html
110    pub fn lookup(&self) -> reqwest::Result<IpLocate> {
111        let mut lookup = Lookup { ..*self };
112
113        // Ensures that the format is of JSON type.
114        if lookup.format.is_some() {
115            lookup.format(Format::Json);
116        }
117        // Ensures that the callback has no value.
118        if lookup.callback.is_some() {
119            lookup.callback = None;
120        }
121
122        let mut response = lookup.request()?;
123
124        let rate_limit = RateLimit::from(&response);
125
126        let geo_ip: GeoIp = response.json()?;
127
128        Ok(IpLocate { rate_limit, geo_ip })
129    }
130
131    /// Make a request to the IPLocate API, looking up information for a given IP address.
132    fn request(&self) -> Result<reqwest::Response> {
133        let mut url = self
134            .format
135            .and_then(|format| Some(format!("{}/{}.{}", API_REQUEST_URL, self.ip, format)))
136            .or(Some(format!("{}/{}", API_REQUEST_URL, self.ip)))
137            .unwrap()
138            .parse::<reqwest::Url>()
139            .unwrap();
140
141        if let Some(callback) = self.callback {
142            url.set_query(Some(&format!("callback={}", callback)));
143        }
144
145        let mut request = reqwest::Request::new(reqwest::Method::GET, url);
146
147        if let Some(apikey) = self.apikey {
148            let headers = request.headers_mut();
149
150            headers.insert("x-api-key", apikey.parse().unwrap());
151        }
152
153        Ok(reqwest::Client::new().execute(request)?)
154    }
155}
156
157/// The format of the data to be returned upon request from the IPLocate API.
158#[derive(Clone, Copy)]
159pub enum Format {
160    Json,
161    Xml,
162    Yaml,
163    Csv,
164}
165
166impl Display for Format {
167    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
168        let format = match *self {
169            Format::Json => "json",
170            Format::Xml => "xml",
171            Format::Yaml => "yaml",
172            Format::Csv => "csv",
173        };
174
175        f.write_str(format)
176    }
177}
178
179/// The preferred return type of a request.
180#[derive(Debug)]
181pub struct IpLocate {
182    pub rate_limit: RateLimit,
183    pub geo_ip: GeoIp,
184}
185
186impl IpLocate {
187    /// Constructs a new `Lookup`.
188    pub fn new<'a>(ip: IpAddr) -> Lookup<'a> {
189        Lookup::new(ip)
190    }
191}
192
193/// This type contains all the data associated with IPLocate API's rate limits.
194#[derive(Debug)]
195pub struct RateLimit {
196    /// The number of requests you can make daily.
197    pub limit: i32,
198    /// The remaining number of requests that you can make on the same day.
199    pub remaining: i32,
200    /// The time that will reset the remaining number of requests back to the IPLocate API's rate
201    /// limit.
202    pub reset: DateTime<Utc>,
203}
204
205impl<'a> From<&'a reqwest::Response> for RateLimit {
206    fn from(response: &'a reqwest::Response) -> Self {
207        let limit: i32;
208        let remaining: i32;
209        let reset: DateTime<Utc>;
210
211        let headers = response.headers();
212
213        let get_header = |name: &str| -> Option<&str> {
214            headers
215                .get(name)
216                .and_then(|value| Some(value.to_str().unwrap()))
217        };
218
219        limit = get_header("x-ratelimit-limit")
220            .and_then(|limit| limit.parse::<i32>().ok())
221            .unwrap();
222        remaining = get_header("x-ratelimit-remaining")
223            .and_then(|remaining| remaining.parse::<i32>().ok())
224            .unwrap();
225        reset = get_header("x-ratelimit-reset")
226            .and_then(|reset| from_raw_custom_datetime(reset).ok())
227            .unwrap();
228
229        RateLimit {
230            limit,
231            remaining,
232            reset,
233        }
234    }
235}
236
237/// This type contains all the data related to a specific IP address.
238#[derive(Debug, Serialize, Deserialize)]
239pub struct GeoIp {
240    pub ip: String,
241    pub country: Option<String>,
242    pub country_code: Option<String>,
243    pub city: Option<String>,
244    pub continent: Option<String>,
245    pub latitude: Option<f64>,
246    pub longitude: Option<f64>,
247    pub time_zone: Option<String>,
248    pub postal_code: Option<String>,
249    pub org: Option<String>,
250    pub asn: Option<String>,
251    pub subdivision: Option<String>,
252    pub subdivision2: Option<String>,
253}
254
255#[doc(hidden)]
256fn from_raw_custom_datetime(s: &str) -> ParseResult<DateTime<Utc>> {
257    let s = &s.split(' ').collect::<Vec<_>>();
258    let s = format!("{}T{}{}", s[0], s[1], s[2]);
259    chrono::DateTime::from_str(&s)
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    fn test_from_raw_custom_datetime() {
268        assert_eq!(
269            Ok(DateTime::from_str("2018-09-21T00:00:00+00:00").unwrap()),
270            from_raw_custom_datetime("2018-09-21 00:00:00 +0000"),
271        );
272    }
273}