apolloconfig 0.1.0

apolloconfig
Documentation
//! [Ref](https://www.apolloconfig.com/#/en/usage/other-language-client-user-guide?id=_12-reading-configuration-from-apollo-via-http-interface-with-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_json::{Map, Value};
use url::Url;

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

//
pub type ConfigfilesJsonContents = Vec<u8>;

//
#[derive(Debug, Clone)]
pub struct ConfigfilesJson {
    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 client_ip: Option<Box<str>>,
}

impl ConfigfilesJson {
    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,
            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 client_ip(mut self, value: impl AsRef<str>) -> Self {
        self.client_ip = Some(value.as_ref().into());
        self
    }
}

impl Endpoint for ConfigfilesJson {
    type RenderRequestError = EndpointError;

    type ParseResponseOutput =
        EndpointRet<(ConfigfilesJsonConfigurations, ConfigfilesJsonContents)>;
    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!(
            "/configfiles/json/{}/{}/{}",
            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(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(EndpointRet::Ok((
                ConfigfilesJsonConfigurations::from_contents(
                    response.body(),
                    self.namespace_name.as_deref(),
                )
                .map_err(EndpointError::DeResponseBodyOkJsonFailed)?,
                response.body().to_owned(),
            ))),
            status => match serde_json::from_slice::<ResponseBodyErrJson>(response.body()) {
                Ok(err_json) => Ok(EndpointRet::Other((status, Ok(err_json)))),
                Err(_) => Ok(EndpointRet::Other((
                    status,
                    Err(response.body().to_owned()),
                ))),
            },
        }
    }
}

//
//
//
#[derive(Debug, Clone)]
pub struct ConfigfilesJsonConfigurations {
    pub namespace_name: Box<str>,
    configurations: Map<String, Value>,
}

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

    pub fn from_contents(
        contents: impl AsRef<[u8]>,
        namespace_name: Option<&str>,
    ) -> Result<Self, serde_json::Error> {
        let contents = contents.as_ref();

        serde_json::from_slice(contents).map(|x| Self {
            namespace_name: namespace_name.unwrap_or(NAMESPACE_NAME_DEFAULT).into(),
            configurations: x,
        })
    }
}

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

    #[test]
    fn test_configurations_from_contents() {
        //
        let content = include_str!(
            "../../tests/response_body_json_files/configfiles_json_application_ok.json"
        );
        match ConfigfilesJsonConfigurations::from_contents(content.as_bytes(), None) {
            Ok(c) => {
                let c = c.configurations().unwrap();

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

        //
        let content = include_str!(
            "../../tests/response_body_json_files/configfiles_json_namespace_json_ok.json"
        );
        match ConfigfilesJsonConfigurations::from_contents(
            content.as_bytes(),
            Some("my_namespace.json"),
        ) {
            Ok(c) => {
                let c = c.configurations().unwrap();

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