apollo_client/
meta.rs

1//! Common api metadata.
2
3use crate::errors::{ApolloClientResult, ApolloResponseError};
4use reqwest::Response;
5use std::{fmt, fmt::Display, time::Duration};
6
7#[allow(dead_code)]
8pub(crate) const DEFAULT_CLUSTER_NAME: &str = "default";
9#[allow(dead_code)]
10pub(crate) const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
11#[allow(dead_code)]
12pub(crate) const DEFAULT_NOTIFY_TIMEOUT: Duration = Duration::from_secs(90);
13
14/// Kind of a configuration namespace.
15#[derive(Debug, Clone, Copy, PartialEq)]
16pub enum NamespaceKind {
17    Properties,
18    Xml,
19    Json,
20    Yaml,
21    Txt,
22}
23
24impl NamespaceKind {
25    /// Infer the configuration namespace kind.
26    pub fn infer_namespace_kind(namespace_name: &str) -> Self {
27        if namespace_name.ends_with(".xml") {
28            NamespaceKind::Xml
29        } else if namespace_name.ends_with(".json") {
30            NamespaceKind::Json
31        } else if namespace_name.ends_with(".yml") || namespace_name.ends_with(".yaml") {
32            NamespaceKind::Yaml
33        } else if namespace_name.ends_with(".txt") {
34            NamespaceKind::Txt
35        } else {
36            NamespaceKind::Properties
37        }
38    }
39}
40
41impl Display for NamespaceKind {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
43        Display::fmt(
44            match self {
45                NamespaceKind::Properties => "properties",
46                NamespaceKind::Xml => "xml",
47                NamespaceKind::Json => "json",
48                NamespaceKind::Yaml => "yaml",
49                NamespaceKind::Txt => "txt",
50            },
51            f,
52        )
53    }
54}
55
56/// Common api request trait.
57#[cfg(feature = "conf")]
58pub(crate) trait PerformRequest {
59    /// The returned response after request is success.
60    type Response: PerformResponse;
61
62    /// Url path.
63    fn path(&self) -> String;
64
65    /// Request method.
66    fn method(&self) -> http::Method {
67        http::Method::GET
68    }
69
70    /// Url queries.
71    fn queries(
72        &self,
73    ) -> ApolloClientResult<Vec<(std::borrow::Cow<'_, str>, std::borrow::Cow<'_, str>)>> {
74        Ok(vec![])
75    }
76
77    /// Handle extras operator, such as set request body.
78    #[allow(unused_mut)]
79    fn request_builder(
80        &self,
81        mut request_builder: reqwest::RequestBuilder,
82    ) -> reqwest::RequestBuilder {
83        //FIXME
84        //see issue #15701 <https://github.com/rust-lang/rust/issues/15701>
85        #[cfg(all(feature = "auth", feature = "conf"))]
86        {
87            request_builder = self.signature(request_builder);
88        }
89        request_builder
90    }
91
92    /// AppId
93    #[allow(dead_code)]
94    fn app_id(&self) -> Option<&str> {
95        None
96    }
97
98    /// Access key
99    #[cfg(all(feature = "auth", feature = "conf"))]
100    fn access_key(&self) -> Option<&str> {
101        None
102    }
103
104    /// make `Authorization` header
105    ///
106    /// # Documentation
107    ///
108    /// https://www.apolloconfig.com/#/zh/usage/other-language-client-user-guide?id=_15-%e9%85%8d%e7%bd%ae%e8%ae%bf%e9%97%ae%e5%af%86%e9%92%a5
109    #[cfg(all(feature = "auth", feature = "conf"))]
110    fn signature(&self, mut request_builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
111        use base64::{engine::general_purpose::STANDARD, Engine};
112        use hmac::{Mac, SimpleHmac};
113        use sha1::Sha1;
114        type HmacWithSha1 = SimpleHmac<Sha1>;
115        if let (Some(app_id), Some(access_key)) = (self.app_id(), self.access_key()) {
116            let ts = chrono::Utc::now().timestamp_millis();
117            let mut url = self.path();
118            if let Ok(queries) = self.queries() {
119                if !queries.is_empty() {
120                    url += "?";
121                    for (idx, (key, val)) in queries.into_iter().enumerate() {
122                        url += &format!(
123                            "{}{}={}",
124                            if idx > 0 { "&" } else { "" },
125                            urlencoding::encode(&key),
126                            urlencoding::encode(&val)
127                        );
128                    }
129                }
130            }
131            if let Ok(mut hmac) = HmacWithSha1::new_from_slice(access_key.as_bytes()) {
132                hmac.update(format!("{}\n{}", ts, url).as_bytes());
133                let sign = STANDARD.encode(hmac.finalize().into_bytes());
134                request_builder = request_builder
135                    .header(
136                        reqwest::header::AUTHORIZATION,
137                        format!("Apollo {}:{}", app_id, sign),
138                    )
139                    .header("Timestamp", ts);
140            }
141        }
142        request_builder
143    }
144}
145
146/// Common api response trait.
147#[cfg(feature = "conf")]
148#[async_trait::async_trait]
149pub(crate) trait PerformResponse: Sized {
150    /// Create Self from response.
151    async fn from_response(response: Response) -> ApolloClientResult<Self>;
152}
153
154#[cfg(feature = "conf")]
155#[async_trait::async_trait]
156impl PerformResponse for () {
157    async fn from_response(_response: Response) -> ApolloClientResult<Self> {
158        Ok(())
159    }
160}
161
162#[cfg(feature = "conf")]
163#[async_trait::async_trait]
164impl PerformResponse for ini::Properties {
165    async fn from_response(response: Response) -> ApolloClientResult<Self> {
166        let content = response.text().await?;
167        let i = ini::Ini::load_from_str(&content)?;
168        Ok(i.section(None::<&'static str>).cloned().unwrap_or_default())
169    }
170}
171
172/// Create request url from base url, mainly path and queries.
173#[cfg(feature = "conf")]
174pub(crate) fn handle_url(
175    request: &impl PerformRequest,
176    base_url: url::Url,
177) -> ApolloClientResult<url::Url> {
178    let mut url = base_url;
179    let path = &request.path();
180    let query = &request.queries()?;
181
182    url.path_segments_mut()
183        .map_err(|_| crate::errors::ApolloClientError::UrlCannotBeABase)?
184        .extend(path.split('/').skip_while(|s| s.is_empty()));
185    if !query.is_empty() {
186        url.query_pairs_mut().extend_pairs(query);
187    }
188
189    Ok(url)
190}
191
192/// Validate response is successful or not.
193#[allow(dead_code)]
194pub(crate) async fn validate_response(response: Response) -> ApolloClientResult<Response> {
195    ApolloResponseError::from_response(response)
196        .await
197        .map_err(Into::into)
198}
199
200/// Implement PerformResponse for response struct which content type is `application/json`.
201#[allow(unused_macros)]
202macro_rules! implement_json_perform_response_for {
203    ($t:ty) => {
204        #[async_trait::async_trait]
205        impl $crate::meta::PerformResponse for $t {
206            async fn from_response(
207                response: ::reqwest::Response,
208            ) -> $crate::errors::ApolloClientResult<Self> {
209                Ok(response.json().await?)
210            }
211        }
212    };
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[test]
220    fn test_infer_namespace_kind() {
221        assert_eq!(
222            NamespaceKind::infer_namespace_kind("foo.properties"),
223            NamespaceKind::Properties
224        );
225        assert_eq!(
226            NamespaceKind::infer_namespace_kind("foo.xml"),
227            NamespaceKind::Xml
228        );
229        assert_eq!(
230            NamespaceKind::infer_namespace_kind("foo.yaml"),
231            NamespaceKind::Yaml
232        );
233        assert_eq!(
234            NamespaceKind::infer_namespace_kind("foo.yml"),
235            NamespaceKind::Yaml
236        );
237        assert_eq!(
238            NamespaceKind::infer_namespace_kind("foo.json"),
239            NamespaceKind::Json
240        );
241        assert_eq!(
242            NamespaceKind::infer_namespace_kind("foo.txt"),
243            NamespaceKind::Txt
244        );
245        assert_eq!(
246            NamespaceKind::infer_namespace_kind("foo"),
247            NamespaceKind::Properties
248        );
249    }
250}