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