1extern 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
40pub type Result<T> = reqwest::Result<T>;
45
46const API_REQUEST_URL: &str = "https://www.iplocate.io/api/lookup";
48
49pub fn lookup(ip: IpAddr) -> Result<IpLocate> {
53 Lookup::new(ip).lookup()
54}
55
56pub struct Lookup<'a> {
58 ip: IpAddr,
60 format: Option<Format>,
62 apikey: Option<&'a str>,
64 callback: Option<&'a str>,
66}
67
68impl<'a> Lookup<'a> {
69 pub fn new(ip: IpAddr) -> Self {
71 Lookup {
72 ip,
73 format: None,
74 apikey: None,
75 callback: None,
76 }
77 }
78
79 pub fn format(&mut self, value: Format) -> &mut Self {
81 self.format = Some(value);
82 self
83 }
84
85 pub fn apikey(&mut self, value: &'a str) -> &mut Self {
87 self.apikey = Some(value);
88 self
89 }
90
91 pub fn callback(&mut self, value: &'a str) -> &mut Self {
93 self.callback = Some(value);
94 self
95 }
96
97 pub fn raw_lookup(&self) -> Result<String> {
101 self.request()?.text()
102 }
103
104 pub fn lookup(&self) -> reqwest::Result<IpLocate> {
111 let mut lookup = Lookup { ..*self };
112
113 if lookup.format.is_some() {
115 lookup.format(Format::Json);
116 }
117 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 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#[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#[derive(Debug)]
181pub struct IpLocate {
182 pub rate_limit: RateLimit,
183 pub geo_ip: GeoIp,
184}
185
186impl IpLocate {
187 pub fn new<'a>(ip: IpAddr) -> Lookup<'a> {
189 Lookup::new(ip)
190 }
191}
192
193#[derive(Debug)]
195pub struct RateLimit {
196 pub limit: i32,
198 pub remaining: i32,
200 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#[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}