ip-api-api 0.4.0

IP-API.com Api
Documentation
//! https://members.ip-api.com/docs/batch

use core::ops::Deref;

use http_api_client_endpoint::{
    Body, Endpoint, MIME_APPLICATION_JSON, Request, Response,
    http::{
        Method,
        header::{ACCEPT, CONTENT_TYPE},
    },
};
use serde::Deserialize;
use serde_json::{Map, Value};
use url::Url;

use crate::{
    endpoints::{
        URL_BASE, URL_BASE_PRO, common::EndpointError, helper::get_n_from_headers_by_key,
        json::JsonResponseBodyJson,
    },
    objects::rate_limit::{RESPONSE_HEADER_KEY_X_RL, RESPONSE_HEADER_KEY_X_TTL, RateLimit},
    types::lang::Lang,
};

pub const MAX_QUERY: usize = 100;

//
#[derive(Debug, Clone)]
pub struct Batch {
    pub queries: Vec<BatchQuery>,
    pub key: Option<Box<str>>,
    pub fields: Option<Box<str>>,
    pub lang: Option<Lang>,
}

#[derive(Debug, Clone)]
pub struct BatchQuery {
    pub query: Box<str>,
    pub fields: Option<Box<str>>,
    pub lang: Option<Lang>,
}
impl BatchQuery {
    pub fn new(query: impl AsRef<str>) -> Self {
        Self {
            query: query.as_ref().into(),
            fields: None,
            lang: None,
        }
    }

    pub fn fields(mut self, fields: impl AsRef<str>) -> Self {
        self.fields = Some(fields.as_ref().into());
        self
    }

    pub fn lang(mut self, lang: Lang) -> Self {
        self.lang = Some(lang);
        self
    }
}

impl Batch {
    pub fn new(queries: Vec<BatchQuery>, key: Option<Box<str>>) -> Self {
        if queries.len() > MAX_QUERY {
            debug_assert!(false, "containing up to 100 IP addresses or objects");
        }

        Self {
            queries,
            key,
            fields: None,
            lang: None,
        }
    }

    pub fn fields(mut self, fields: impl AsRef<str>) -> Self {
        self.fields = Some(fields.as_ref().into());
        self
    }

    pub fn lang(mut self, lang: Lang) -> Self {
        self.lang = Some(lang);
        self
    }
}

impl Endpoint for Batch {
    type RenderRequestError = EndpointError;

    type ParseResponseOutput = (BatchResponseBodyJson, Option<RateLimit>);
    type ParseResponseError = EndpointError;

    fn render_request(&self) -> Result<Request<Body>, Self::RenderRequestError> {
        let url = format!(
            "{}/batch",
            if self.key.is_some() {
                URL_BASE_PRO
            } else {
                URL_BASE
            },
        );
        let mut url = Url::parse(url.as_str()).map_err(EndpointError::MakeRequestUrlFailed)?;

        if let Some(key) = &self.key {
            url.query_pairs_mut().append_pair("key", key);
        }
        if let Some(fields) = &self.fields {
            url.query_pairs_mut().append_pair("fields", fields);
        }
        if let Some(lang) = &self.lang {
            url.query_pairs_mut()
                .append_pair("lang", lang.to_string().as_str());
        }

        let body_array = self
            .queries
            .iter()
            .map(|x| {
                if x.fields.is_none() && x.lang.is_none() {
                    Value::String(x.query.to_string())
                } else {
                    let mut map = Map::new();
                    map.insert("query".to_owned(), Value::String(x.query.to_string()));
                    if let Some(fields) = &x.fields {
                        map.insert("fields".to_owned(), Value::String(fields.to_string()));
                    }
                    if let Some(lang) = &x.lang {
                        map.insert("lang".to_owned(), Value::String(lang.to_string()));
                    }
                    Value::Object(map)
                }
            })
            .collect::<Vec<_>>();

        let body =
            serde_json::to_vec(&body_array).map_err(EndpointError::SerRequestBodyJsonFailed)?;

        let request = Request::builder()
            .method(Method::POST)
            .uri(url.as_str())
            .header(CONTENT_TYPE, MIME_APPLICATION_JSON)
            .header(ACCEPT, MIME_APPLICATION_JSON)
            .body(body)
            .map_err(EndpointError::MakeRequestFailed)?;

        Ok(request)
    }

    fn parse_response(
        &self,
        response: Response<Body>,
    ) -> Result<Self::ParseResponseOutput, Self::ParseResponseError> {
        let json = serde_json::from_slice(response.body())
            .map_err(EndpointError::DeResponseBodyJsonFailed)?;

        let rate_limit = if self.key.is_some() {
            None
        } else {
            Some(RateLimit {
                remaining: get_n_from_headers_by_key(response.headers(), RESPONSE_HEADER_KEY_X_RL)
                    .ok(),
                seconds_until_reset: get_n_from_headers_by_key(
                    response.headers(),
                    RESPONSE_HEADER_KEY_X_TTL,
                )
                .ok(),
            })
        };

        Ok((json, rate_limit))
    }
}

//
//
//
#[derive(Deserialize, Debug, Clone)]
pub struct BatchResponseBodyJson(pub Vec<JsonResponseBodyJson>);

impl Deref for BatchResponseBodyJson {
    type Target = Vec<JsonResponseBodyJson>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use serde_json::json;

    #[test]
    fn test_render_request() {
        let batch = Batch::new(vec![BatchQuery::new("24.48.0.1")], None);
        let req = batch.render_request().unwrap();
        assert_eq!(req.uri(), "http://ip-api.com/batch");
        assert_eq!(req.body(), br#"["24.48.0.1"]"#);

        //
        let batch = Batch::new(
            vec![
                BatchQuery::new("24.48.0.1"),
                BatchQuery::new("8.8.8.8")
                    .fields("country,query")
                    .lang(Lang::EN),
            ],
            Some("foo".into()),
        );
        let req = batch.render_request().unwrap();
        assert_eq!(req.uri(), "https://pro.ip-api.com/batch?key=foo");
        assert_eq!(
            req.body(),
            json! {
                [
                    "24.48.0.1",
                    {"query":"8.8.8.8", "fields":"country,query", "lang":"en"}
                ]
            }
            .to_string()
            .as_bytes()
        );

        let batch = batch.fields("status,message,country,query");
        let req = batch.render_request().unwrap();
        assert_eq!(
            req.uri(),
            "https://pro.ip-api.com/batch?key=foo&fields=status%2Cmessage%2Ccountry%2Cquery"
        );

        let batch = batch.lang(Lang::EN);
        let req = batch.render_request().unwrap();
        assert_eq!(
            req.uri(),
            "https://pro.ip-api.com/batch?key=foo&fields=status%2Cmessage%2Ccountry%2Cquery&lang=en"
        );
    }

    #[test]
    fn test_de_response_body_json() {
        match serde_json::from_str::<BatchResponseBodyJson>(include_str!(
            "../../tests/response_body_json_files/batch_simple.json"
        )) {
            Ok(json) => {
                assert_eq!(json.len(), 3);
                match &json[0] {
                    JsonResponseBodyJson::Success(ok_json) => {
                        assert_eq!(ok_json.query.to_string(), "208.80.152.201")
                    }
                    x => panic!("{:?}", x),
                }
                match &json[1] {
                    JsonResponseBodyJson::Success(ok_json) => {
                        assert_eq!(ok_json.query.to_string(), "8.8.8.8")
                    }
                    x => panic!("{:?}", x),
                }
                match &json[2] {
                    JsonResponseBodyJson::Success(ok_json) => {
                        assert_eq!(ok_json.query.to_string(), "24.48.0.1")
                    }
                    x => panic!("{:?}", x),
                }
            }
            ret => panic!("{:?}", ret),
        }

        match serde_json::from_str::<BatchResponseBodyJson>(include_str!(
            "../../tests/response_body_json_files/batch_simple_with_part_err.json"
        )) {
            Ok(json) => {
                assert_eq!(json.len(), 2);
                match &json[0] {
                    JsonResponseBodyJson::Success(ok_json) => {
                        assert_eq!(ok_json.query.to_string(), "208.80.152.201")
                    }
                    x => panic!("{:?}", x),
                }
                match &json[1] {
                    JsonResponseBodyJson::Fail(err_json) => {
                        assert_eq!(err_json.query, "2".into())
                    }
                    x => panic!("{:?}", x),
                }
            }
            ret => panic!("{:?}", ret),
        }
    }
}