apolloconfig 0.1.0

apolloconfig
Documentation
//! [Ref](https://www.apolloconfig.com/#/en/usage/other-language-client-user-guide?id=_13-reading-configuration-from-apollo-via-http-interface-without-cache)

use apolloconfig_sig::AuthSignature;
use http_api_client_endpoint::{
    http::{
        header::{ACCEPT, USER_AGENT},
        Method, StatusCode,
    },
    Body, Endpoint, Request, Response, MIME_APPLICATION_JSON,
};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use url::Url;

use crate::{
    endpoints::{common::EndpointError, CLUSTER_NAME_DEFAULT, NAMESPACE_NAME_DEFAULT},
    objects::{
        configurations::{Configurations, ConfigurationsFromMapError},
        ResponseBodyErrJson,
    },
    types::NamespaceFormat,
};

//
#[derive(Debug, Clone)]
pub struct Configs {
    pub config_server_url: Box<str>,
    pub access_key_secret: Box<str>,
    pub app_id: Box<str>,
    pub cluster_name: Option<Box<str>>,
    pub namespace_name: Option<Box<str>>,
    pub release_key: Option<Box<str>>,
    pub client_ip: Option<Box<str>>,
}

impl Configs {
    pub fn new(
        config_server_url: impl AsRef<str>,
        access_key_secret: impl AsRef<str>,
        app_id: impl AsRef<str>,
    ) -> Self {
        Self {
            config_server_url: config_server_url.as_ref().into(),
            access_key_secret: access_key_secret.as_ref().into(),
            app_id: app_id.as_ref().into(),
            cluster_name: None,
            namespace_name: None,
            release_key: None,
            client_ip: None,
        }
    }

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

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

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

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

impl Endpoint for Configs {
    type RenderRequestError = EndpointError;

    type ParseResponseOutput = ConfigsEndpointRet;
    type ParseResponseError = EndpointError;

    fn render_request(&self) -> Result<Request<Body>, Self::RenderRequestError> {
        let mut url =
            Url::parse(&self.config_server_url).map_err(EndpointError::MakeRequestUrlFailed)?;

        let path = format!(
            "/configs/{}/{}/{}",
            self.app_id,
            self.cluster_name.as_deref().unwrap_or(CLUSTER_NAME_DEFAULT),
            self.namespace_name
                .as_deref()
                .unwrap_or(NAMESPACE_NAME_DEFAULT)
        );
        url.set_path(path.as_str());

        if let Some(release_key) = &self.release_key {
            url.query_pairs_mut()
                .append_pair("releaseKey", release_key.to_string().as_str());
        }
        if let Some(client_ip) = &self.client_ip {
            url.query_pairs_mut()
                .append_pair("ip", client_ip.to_string().as_str());
        }

        let auth_headers = AuthSignature
            .http_headers(url.as_str(), &self.app_id, &self.access_key_secret)
            .map_err(EndpointError::MakeSignatureFailed)?;

        let request_builder = Request::builder()
            .method(Method::GET)
            .uri(url.as_str())
            .header(USER_AGENT, "apolloconfig-rs")
            .header(ACCEPT, MIME_APPLICATION_JSON);
        let request_builder = auth_headers
            .into_iter()
            .flat_map(|(k, v)| k.map(|k| (k, v)))
            .fold(request_builder, |request_builder, (k, v)| {
                request_builder.header(k, v)
            });

        let request = request_builder
            .body(vec![])
            .map_err(EndpointError::MakeRequestFailed)?;

        Ok(request)
    }

    fn parse_response(
        &self,
        response: Response<Body>,
    ) -> Result<Self::ParseResponseOutput, Self::ParseResponseError> {
        let status = response.status();
        match status {
            StatusCode::OK => Ok(ConfigsEndpointRet::Ok(
                serde_json::from_slice(response.body())
                    .map_err(EndpointError::DeResponseBodyOkJsonFailed)?,
            )),
            StatusCode::NOT_MODIFIED => {
                if self.release_key.is_none() {
                    return Err(EndpointError::Other(
                        "server error because release_key is none".into(),
                    ));
                }
                Ok(ConfigsEndpointRet::NotChanged)
            }
            status => match serde_json::from_slice::<ResponseBodyErrJson>(response.body()) {
                Ok(err_json) => Ok(ConfigsEndpointRet::Other((status, Ok(err_json)))),
                Err(_) => Ok(ConfigsEndpointRet::Other((
                    status,
                    Err(response.body().to_owned()),
                ))),
            },
        }
    }
}

//
//
//
#[derive(Debug, Clone)]
pub enum ConfigsEndpointRet {
    Ok(ConfigsResponseBodyOkJson),
    NotChanged,
    Other((StatusCode, Result<ResponseBodyErrJson, Body>)),
}

//
//
//
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ConfigsResponseBodyOkJson {
    #[serde(rename = "appId")]
    pub app_id: Box<str>,
    #[serde(rename = "cluster")]
    pub cluster_name: Box<str>,
    #[serde(rename = "namespaceName")]
    pub namespace_name: Box<str>,
    configurations: Map<String, Value>,
    #[serde(rename = "releaseKey")]
    pub release_key: Box<str>,
}

impl ConfigsResponseBodyOkJson {
    pub fn configurations(&self) -> Result<Configurations, ConfigurationsFromMapError> {
        Configurations::from_map(
            &self.configurations,
            NamespaceFormat::from_namespace_name(Some(&self.namespace_name)),
        )
    }
}

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

    #[test]
    fn test_de_response_body_ok_json() {
        //
        let content =
            include_str!("../../tests/response_body_json_files/configs_application_ok.json");
        match serde_json::from_str::<ConfigsResponseBodyOkJson>(content) {
            Ok(ok_json) => {
                let c = ok_json.configurations().unwrap();

                match &c {
                    Configurations::Properties(m) => {
                        assert_eq!(m.get("content").unwrap().as_str().unwrap(), "content");
                    }
                    _ => panic!("{:?}", c),
                }

                let m1: Map<String, Value> = serde_json::from_str(content).unwrap();
                let m2: Map<String, Value> =
                    serde_json::from_str(serde_json::to_string(&c).unwrap().as_str()).unwrap();

                assert_eq!(m1.get("configurations").unwrap().as_object().unwrap(), &m2);
            }
            Err(err) => panic!("{}", err),
        }

        //
        let content =
            include_str!("../../tests/response_body_json_files/configs_namespace_json_ok.json");
        match serde_json::from_str::<ConfigsResponseBodyOkJson>(content) {
            Ok(ok_json) => {
                let c = ok_json.configurations().unwrap();

                match &c {
                    Configurations::Json(v) => {
                        assert_eq!(
                            v.as_object()
                                .unwrap()
                                .get("firstName")
                                .unwrap()
                                .as_str()
                                .unwrap(),
                            "John"
                        );
                    }
                    _ => panic!("{:?}", c),
                }

                let m1: Map<String, Value> = serde_json::from_str(content).unwrap();
                let m2: Map<String, Value> =
                    serde_json::from_str(serde_json::to_string(&c).unwrap().as_str()).unwrap();
                assert_eq!(
                    serde_json::from_str::<Map<String, Value>>(
                        m1.get("configurations")
                            .unwrap()
                            .as_object()
                            .unwrap()
                            .get("content")
                            .unwrap()
                            .as_str()
                            .unwrap()
                    )
                    .unwrap(),
                    serde_json::from_str::<Map<String, Value>>(
                        m2.get("content").unwrap().as_str().unwrap()
                    )
                    .unwrap()
                );
            }
            Err(err) => panic!("{}", err),
        }

        //
        let content = include_str!(
            "../../tests/response_body_json_files/configs_namespace_properties_ok.json"
        );
        match serde_json::from_str::<ConfigsResponseBodyOkJson>(content) {
            Ok(ok_json) => {
                let c = ok_json.configurations().unwrap();

                match &c {
                    Configurations::Properties(m) => {
                        assert_eq!(m.get("k").unwrap().as_str().unwrap(), "v");
                    }
                    _ => panic!("{:?}", c),
                }
            }
            Err(err) => panic!("{}", err),
        }

        //
        let content =
            include_str!("../../tests/response_body_json_files/configs_namespace_txt_ok.json");
        match serde_json::from_str::<ConfigsResponseBodyOkJson>(content) {
            Ok(ok_json) => {
                let c = ok_json.configurations().unwrap();

                match &c {
                    Configurations::Txt(m) => {
                        assert_eq!(*m, "This is Content.".into());
                    }
                    _ => panic!("{:?}", c),
                }

                let m1: Map<String, Value> = serde_json::from_str(content).unwrap();
                let m2: Map<String, Value> =
                    serde_json::from_str(serde_json::to_string(&c).unwrap().as_str()).unwrap();
                assert_eq!(
                    m1.get("configurations")
                        .unwrap()
                        .as_object()
                        .unwrap()
                        .get("content")
                        .unwrap()
                        .as_str()
                        .unwrap(),
                    m2.get("content").unwrap().as_str().unwrap()
                );
            }
            Err(err) => panic!("{}", err),
        }

        //
        let content =
            include_str!("../../tests/response_body_json_files/configs_namespace_xml_ok.json");
        match serde_json::from_str::<ConfigsResponseBodyOkJson>(content) {
            Ok(ok_json) => {
                let c = ok_json.configurations().unwrap();

                match &c {
                    Configurations::Other(m) => {
                        assert!(m.contains("ISO-8859-1"));
                    }
                    _ => panic!("{:?}", c),
                }

                let m1: Map<String, Value> = serde_json::from_str(content).unwrap();
                let m2: Map<String, Value> =
                    serde_json::from_str(serde_json::to_string(&c).unwrap().as_str()).unwrap();
                assert_eq!(
                    m1.get("configurations")
                        .unwrap()
                        .as_object()
                        .unwrap()
                        .get("content")
                        .unwrap()
                        .as_str()
                        .unwrap(),
                    m2.get("content").unwrap().as_str().unwrap()
                );
            }
            Err(err) => panic!("{}", err),
        }

        //
        let content =
            include_str!("../../tests/response_body_json_files/configs_namespace_yml_ok.json");
        match serde_json::from_str::<ConfigsResponseBodyOkJson>(content) {
            Ok(ok_json) => {
                let c = ok_json.configurations().unwrap();

                match &c {
                    Configurations::Other(m) => {
                        assert!(m.contains("name: John Smith"));
                    }
                    _ => panic!("{:?}", c),
                }

                let m1: Map<String, Value> = serde_json::from_str(content).unwrap();
                let m2: Map<String, Value> =
                    serde_json::from_str(serde_json::to_string(&c).unwrap().as_str()).unwrap();
                assert_eq!(
                    m1.get("configurations")
                        .unwrap()
                        .as_object()
                        .unwrap()
                        .get("content")
                        .unwrap()
                        .as_str()
                        .unwrap(),
                    m2.get("content").unwrap().as_str().unwrap()
                );
            }
            Err(err) => panic!("{}", err),
        }
    }
}