everymap_core/client/
mod.rs1mod macros;
2
3use crate::auth::AuthProvider;
4use crate::error::{EveryMapError, EveryMapResult};
5use async_trait::async_trait;
6use std::sync::Arc;
7use std::time::Instant;
8
9const VERBOSE_BODY_LIMIT: usize = 10_240;
11
12const ERROR_BODY_LIMIT: usize = 256;
14
15const RETRY_AFTER_HEADER: &str = "retry-after";
17
18const DESERIALIZATION_ERROR: &str = "DESERIALIZATION_ERROR";
20
21const UNKNOWN_STATUS: &str = "Unknown";
23
24#[async_trait]
29pub trait HttpClient: Send + Sync {
30 async fn send(&self, builder: reqwest::RequestBuilder) -> EveryMapResult<reqwest::Response>;
32}
33
34pub struct ProviderClient {
41 http_client: reqwest::Client,
42 auth_provider: Arc<dyn AuthProvider>,
43 verbose: bool,
44 provider_name: &'static str,
45}
46
47impl ProviderClient {
48 pub fn new(auth_provider: Arc<dyn AuthProvider>, provider_name: &'static str) -> Self {
50 Self {
51 http_client: reqwest::Client::new(),
52 auth_provider,
53 verbose: false,
54 provider_name,
55 }
56 }
57
58 pub fn with_client_builder(
60 builder: reqwest::ClientBuilder,
61 auth_provider: Arc<dyn AuthProvider>,
62 provider_name: &'static str,
63 ) -> EveryMapResult<Self> {
64 Ok(Self {
65 http_client: builder.build()?,
66 auth_provider,
67 verbose: false,
68 provider_name,
69 })
70 }
71
72 pub fn set_verbose(&mut self, verbose: bool) {
74 self.verbose = verbose;
75 }
76
77 pub fn is_verbose(&self) -> bool {
79 self.verbose
80 }
81
82 pub fn provider_name(&self) -> &'static str {
84 self.provider_name
85 }
86
87 pub fn build_request(&self, method: reqwest::Method, url: &str) -> reqwest::RequestBuilder {
89 self.http_client.request(method, url)
90 }
91
92 pub async fn request(
97 &self,
98 builder: reqwest::RequestBuilder,
99 ) -> EveryMapResult<reqwest::Response> {
100 let start = Instant::now();
101 let builder = self.auth_provider.apply(builder).await?;
102 let response: reqwest::Response = builder.send().await?;
103 let elapsed = start.elapsed();
104 let status = response.status();
105
106 if self.verbose {
107 let url = response.url().to_string();
108 eprintln!(
109 "[VERBOSE] {} {} — {} ({:.0}ms)",
110 status.as_u16(),
111 redact_api_key(&url),
112 status.canonical_reason().unwrap_or(UNKNOWN_STATUS),
113 elapsed.as_secs_f64() * 1000.0
114 );
115 }
116
117 if status.is_success() {
118 Ok(response)
119 } else {
120 let status_code = status.as_u16();
121 let status_text = status
122 .canonical_reason()
123 .unwrap_or(UNKNOWN_STATUS)
124 .to_string();
125 let retry_after = if status_code == 429 {
126 response
127 .headers()
128 .get(RETRY_AFTER_HEADER)
129 .and_then(|v| v.to_str().ok())
130 .and_then(|v| v.parse::<u64>().ok())
131 } else {
132 None
133 };
134 let body = response.text().await.unwrap_or_default();
135
136 if self.verbose {
137 let snippet = truncate_str(&body, VERBOSE_BODY_LIMIT);
138 eprintln!("[VERBOSE] Error response body:\n{}", snippet);
139 }
140
141 if status_code == 429 {
142 Err(EveryMapError::rate_limited(self.provider_name, retry_after))
143 } else {
144 Err(EveryMapError::http_with_body(
145 status_code,
146 format!("HTTP error: {}", status_text),
147 body,
148 ))
149 }
150 }
151 }
152
153 pub async fn request_json<T: serde::de::DeserializeOwned>(
159 &self,
160 builder: reqwest::RequestBuilder,
161 ) -> EveryMapResult<T> {
162 let start = Instant::now();
163 let response = self.request(builder).await?;
164 let body = response.text().await.map_err(EveryMapError::ClientError)?;
165
166 if self.verbose {
167 let snippet = truncate_str(&body, VERBOSE_BODY_LIMIT);
168 eprintln!(
169 "[VERBOSE] Response body ({:.0}ms, {} bytes):\n{}",
170 start.elapsed().as_secs_f64() * 1000.0,
171 body.len(),
172 snippet
173 );
174 }
175
176 serde_json::from_str::<T>(&body).map_err(|e| {
177 let snippet = truncate_str(&body, ERROR_BODY_LIMIT);
178 EveryMapError::provider(
179 self.provider_name,
180 DESERIALIZATION_ERROR,
181 format!(
182 "Failed to deserialize response: {}\nResponse body (first {} bytes): {}",
183 e, ERROR_BODY_LIMIT, snippet
184 ),
185 )
186 })
187 }
188
189 pub async fn post_json<T: serde::de::DeserializeOwned>(
191 &self,
192 url: &str,
193 body: &serde_json::Value,
194 ) -> EveryMapResult<T> {
195 let builder = self.build_request(reqwest::Method::POST, url).json(body);
196 self.request_json(builder).await
197 }
198}
199
200const REDACTED_PARAMS: &[&str] = &["apiKey", "key", "access_token"];
202
203pub fn redact_api_key(url: &str) -> String {
206 let mut result = url.to_string();
207 for param in REDACTED_PARAMS {
208 let prefix = format!("{}=", param);
209 if let Some(start) = result.find(&prefix) {
210 let val_start = start + prefix.len();
211 let val_end = result[val_start..]
212 .find('&')
213 .map(|i| val_start + i)
214 .unwrap_or(result.len());
215 result.replace_range(val_start..val_end, "***");
216 }
217 }
218 result
219}
220
221pub fn truncate_str(s: &str, limit: usize) -> String {
223 if s.len() <= limit {
224 s.to_string()
225 } else {
226 let end = s
227 .char_indices()
228 .take_while(|(byte_index, _)| *byte_index < limit)
229 .last()
230 .map(|(byte_index, char_value)| byte_index + char_value.len_utf8())
231 .unwrap_or(limit.min(s.len()));
232 format!("{}...\n[truncated, {} bytes total]", &s[..end], s.len())
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 #[test]
241 fn test_redact_api_key_here() {
242 let url = "https://router.hereapi.com/v8/routes?apiKey=secret123&origin=52.5,13.3";
243 assert_eq!(
244 redact_api_key(url),
245 "https://router.hereapi.com/v8/routes?apiKey=***&origin=52.5,13.3"
246 );
247 }
248
249 #[test]
250 fn test_redact_api_key_google() {
251 let url = "https://maps.googleapis.com/maps/api/geocode/json?key=secret123&address=Berlin";
252 assert_eq!(
253 redact_api_key(url),
254 "https://maps.googleapis.com/maps/api/geocode/json?key=***&address=Berlin"
255 );
256 }
257
258 #[test]
259 fn test_redact_api_key_mapbox() {
260 let url = "https://api.mapbox.com/geocode/v6/forward?q=test&access_token=pk.secret123";
261 assert_eq!(
262 redact_api_key(url),
263 "https://api.mapbox.com/geocode/v6/forward?q=test&access_token=***"
264 );
265 }
266
267 #[test]
268 fn test_redact_no_key() {
269 let url = "https://example.com/api?foo=bar";
270 assert_eq!(redact_api_key(url), url);
271 }
272
273 #[test]
274 fn test_truncate_str_short() {
275 let s = "hello";
276 assert_eq!(truncate_str(s, 100), "hello");
277 }
278
279 #[test]
280 fn test_truncate_str_long() {
281 let s = "a".repeat(20000);
282 let truncated = truncate_str(&s, 1024);
283 assert!(truncated.len() < s.len());
284 assert!(truncated.contains("[truncated"));
285 }
286
287 #[test]
288 fn test_provider_client_new() {
289 let auth: Arc<dyn AuthProvider> = Arc::new(crate::auth::ApiKeyProvider::new(
290 "test".to_string(),
291 "key".to_string(),
292 ));
293 let client = ProviderClient::new(auth, "test");
294 assert_eq!(client.provider_name(), "test");
295 assert!(!client.is_verbose());
296 }
297
298 #[test]
299 fn test_provider_client_verbose() {
300 let auth: Arc<dyn AuthProvider> = Arc::new(crate::auth::ApiKeyProvider::new(
301 "test".to_string(),
302 "key".to_string(),
303 ));
304 let mut client = ProviderClient::new(auth, "test");
305 client.set_verbose(true);
306 assert!(client.is_verbose());
307 }
308}