ip_api_api/endpoints/
batch.rs

1//! https://members.ip-api.com/docs/batch
2
3use core::ops::Deref;
4
5use http_api_client_endpoint::{
6    http::{
7        header::{ACCEPT, CONTENT_TYPE},
8        Method,
9    },
10    Body, Endpoint, Request, Response, MIME_APPLICATION_JSON,
11};
12use serde::Deserialize;
13use serde_json::{Map, Value};
14use url::Url;
15
16use crate::{
17    endpoints::{
18        common::EndpointError, helper::get_n_from_headers_by_key, json::JsonResponseBodyJson,
19        URL_BASE, URL_BASE_PRO,
20    },
21    objects::rate_limit::{RateLimit, RESPONSE_HEADER_KEY_X_RL, RESPONSE_HEADER_KEY_X_TTL},
22    types::lang::Lang,
23};
24
25pub const MAX_QUERY: usize = 100;
26
27//
28#[derive(Debug, Clone)]
29pub struct Batch {
30    pub queries: Vec<BatchQuery>,
31    pub key: Option<Box<str>>,
32    pub fields: Option<Box<str>>,
33    pub lang: Option<Lang>,
34}
35
36#[derive(Debug, Clone)]
37pub struct BatchQuery {
38    pub query: Box<str>,
39    pub fields: Option<Box<str>>,
40    pub lang: Option<Lang>,
41}
42impl BatchQuery {
43    pub fn new(query: impl AsRef<str>) -> Self {
44        Self {
45            query: query.as_ref().into(),
46            fields: None,
47            lang: None,
48        }
49    }
50
51    pub fn fields(mut self, fields: impl AsRef<str>) -> Self {
52        self.fields = Some(fields.as_ref().into());
53        self
54    }
55
56    pub fn lang(mut self, lang: Lang) -> Self {
57        self.lang = Some(lang);
58        self
59    }
60}
61
62impl Batch {
63    pub fn new(queries: Vec<BatchQuery>, key: Option<Box<str>>) -> Self {
64        if queries.len() > MAX_QUERY {
65            debug_assert!(false, "containing up to 100 IP addresses or objects");
66        }
67
68        Self {
69            queries,
70            key,
71            fields: None,
72            lang: None,
73        }
74    }
75
76    pub fn fields(mut self, fields: impl AsRef<str>) -> Self {
77        self.fields = Some(fields.as_ref().into());
78        self
79    }
80
81    pub fn lang(mut self, lang: Lang) -> Self {
82        self.lang = Some(lang);
83        self
84    }
85}
86
87impl Endpoint for Batch {
88    type RenderRequestError = EndpointError;
89
90    type ParseResponseOutput = (BatchResponseBodyJson, Option<RateLimit>);
91    type ParseResponseError = EndpointError;
92
93    fn render_request(&self) -> Result<Request<Body>, Self::RenderRequestError> {
94        let url = format!(
95            "{}/batch",
96            if self.key.is_some() {
97                URL_BASE_PRO
98            } else {
99                URL_BASE
100            },
101        );
102        let mut url = Url::parse(url.as_str()).map_err(EndpointError::MakeRequestUrlFailed)?;
103
104        if let Some(key) = &self.key {
105            url.query_pairs_mut().append_pair("key", key);
106        }
107        if let Some(fields) = &self.fields {
108            url.query_pairs_mut().append_pair("fields", fields);
109        }
110        if let Some(lang) = &self.lang {
111            url.query_pairs_mut()
112                .append_pair("lang", lang.to_string().as_str());
113        }
114
115        let body_array = self
116            .queries
117            .iter()
118            .map(|x| {
119                if x.fields.is_none() && x.lang.is_none() {
120                    Value::String(x.query.to_string())
121                } else {
122                    let mut map = Map::new();
123                    map.insert("query".to_owned(), Value::String(x.query.to_string()));
124                    if let Some(fields) = &x.fields {
125                        map.insert("fields".to_owned(), Value::String(fields.to_string()));
126                    }
127                    if let Some(lang) = &x.lang {
128                        map.insert("lang".to_owned(), Value::String(lang.to_string()));
129                    }
130                    Value::Object(map)
131                }
132            })
133            .collect::<Vec<_>>();
134
135        let body =
136            serde_json::to_vec(&body_array).map_err(EndpointError::SerRequestBodyJsonFailed)?;
137
138        let request = Request::builder()
139            .method(Method::POST)
140            .uri(url.as_str())
141            .header(CONTENT_TYPE, MIME_APPLICATION_JSON)
142            .header(ACCEPT, MIME_APPLICATION_JSON)
143            .body(body)
144            .map_err(EndpointError::MakeRequestFailed)?;
145
146        Ok(request)
147    }
148
149    fn parse_response(
150        &self,
151        response: Response<Body>,
152    ) -> Result<Self::ParseResponseOutput, Self::ParseResponseError> {
153        let json = serde_json::from_slice(response.body())
154            .map_err(EndpointError::DeResponseBodyJsonFailed)?;
155
156        let rate_limit = if self.key.is_some() {
157            None
158        } else {
159            Some(RateLimit {
160                remaining: get_n_from_headers_by_key(response.headers(), RESPONSE_HEADER_KEY_X_RL)
161                    .ok(),
162                seconds_until_reset: get_n_from_headers_by_key(
163                    response.headers(),
164                    RESPONSE_HEADER_KEY_X_TTL,
165                )
166                .ok(),
167            })
168        };
169
170        Ok((json, rate_limit))
171    }
172}
173
174//
175//
176//
177#[derive(Deserialize, Debug, Clone)]
178pub struct BatchResponseBodyJson(pub Vec<JsonResponseBodyJson>);
179
180impl Deref for BatchResponseBodyJson {
181    type Target = Vec<JsonResponseBodyJson>;
182
183    fn deref(&self) -> &Self::Target {
184        &self.0
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    use serde_json::json;
193
194    #[test]
195    fn test_render_request() {
196        let batch = Batch::new(vec![BatchQuery::new("24.48.0.1")], None);
197        let req = batch.render_request().unwrap();
198        assert_eq!(req.uri(), "http://ip-api.com/batch");
199        assert_eq!(req.body(), br#"["24.48.0.1"]"#);
200
201        //
202        let batch = Batch::new(
203            vec![
204                BatchQuery::new("24.48.0.1"),
205                BatchQuery::new("8.8.8.8")
206                    .fields("country,query")
207                    .lang(Lang::EN),
208            ],
209            Some("foo".into()),
210        );
211        let req = batch.render_request().unwrap();
212        assert_eq!(req.uri(), "https://pro.ip-api.com/batch?key=foo");
213        assert_eq!(
214            req.body(),
215            json! {
216                [
217                    "24.48.0.1",
218                    {"query":"8.8.8.8", "fields":"country,query", "lang":"en"}
219                ]
220            }
221            .to_string()
222            .as_bytes()
223        );
224
225        let batch = batch.fields("status,message,country,query");
226        let req = batch.render_request().unwrap();
227        assert_eq!(
228            req.uri(),
229            "https://pro.ip-api.com/batch?key=foo&fields=status%2Cmessage%2Ccountry%2Cquery"
230        );
231
232        let batch = batch.lang(Lang::EN);
233        let req = batch.render_request().unwrap();
234        assert_eq!(
235            req.uri(),
236            "https://pro.ip-api.com/batch?key=foo&fields=status%2Cmessage%2Ccountry%2Cquery&lang=en"
237        );
238    }
239
240    #[test]
241    fn test_de_response_body_json() {
242        match serde_json::from_str::<BatchResponseBodyJson>(include_str!(
243            "../../tests/response_body_json_files/batch_simple.json"
244        )) {
245            Ok(json) => {
246                assert_eq!(json.len(), 3);
247                match &json[0] {
248                    JsonResponseBodyJson::Success(ok_json) => {
249                        assert_eq!(ok_json.query.to_string(), "208.80.152.201")
250                    }
251                    x => panic!("{:?}", x),
252                }
253                match &json[1] {
254                    JsonResponseBodyJson::Success(ok_json) => {
255                        assert_eq!(ok_json.query.to_string(), "8.8.8.8")
256                    }
257                    x => panic!("{:?}", x),
258                }
259                match &json[2] {
260                    JsonResponseBodyJson::Success(ok_json) => {
261                        assert_eq!(ok_json.query.to_string(), "24.48.0.1")
262                    }
263                    x => panic!("{:?}", x),
264                }
265            }
266            ret => panic!("{:?}", ret),
267        }
268
269        match serde_json::from_str::<BatchResponseBodyJson>(include_str!(
270            "../../tests/response_body_json_files/batch_simple_with_part_err.json"
271        )) {
272            Ok(json) => {
273                assert_eq!(json.len(), 2);
274                match &json[0] {
275                    JsonResponseBodyJson::Success(ok_json) => {
276                        assert_eq!(ok_json.query.to_string(), "208.80.152.201")
277                    }
278                    x => panic!("{:?}", x),
279                }
280                match &json[1] {
281                    JsonResponseBodyJson::Fail(err_json) => {
282                        assert_eq!(err_json.query, "2".into())
283                    }
284                    x => panic!("{:?}", x),
285                }
286            }
287            ret => panic!("{:?}", ret),
288        }
289    }
290}