1use 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#[derive(Debug, Clone, Copy, PartialEq)]
16pub enum NamespaceKind {
17 Properties,
18 Xml,
19 Json,
20 Yaml,
21 Txt,
22}
23
24impl NamespaceKind {
25 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#[cfg(feature = "conf")]
58pub(crate) trait PerformRequest {
59 type Response: PerformResponse;
61
62 fn path(&self) -> String;
64
65 fn method(&self) -> http::Method {
67 http::Method::GET
68 }
69
70 fn queries(
72 &self,
73 ) -> ApolloClientResult<Vec<(std::borrow::Cow<'_, str>, std::borrow::Cow<'_, str>)>> {
74 Ok(vec![])
75 }
76
77 #[allow(unused_mut)]
79 fn request_builder(
80 &self,
81 mut request_builder: reqwest::RequestBuilder,
82 ) -> reqwest::RequestBuilder {
83 #[cfg(all(feature = "auth", feature = "conf"))]
86 {
87 request_builder = self.signature(request_builder);
88 }
89 request_builder
90 }
91
92 #[allow(dead_code)]
94 fn app_id(&self) -> Option<&str> {
95 None
96 }
97
98 #[cfg(all(feature = "auth", feature = "conf"))]
100 fn access_key(&self) -> Option<&str> {
101 None
102 }
103
104 #[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#[cfg(feature = "conf")]
148#[async_trait::async_trait]
149pub(crate) trait PerformResponse: Sized {
150 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#[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#[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#[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}