1use 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#[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#[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 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}