serper_sdk/http/
transport.rs1use crate::core::{Result, SerperError, types::ApiKey};
2use reqwest::{Client as ReqwestClient, Method, Response};
7use serde::Serialize;
8use std::collections::HashMap;
9use std::time::Duration;
10
11#[derive(Debug, Clone)]
13pub struct TransportConfig {
14 pub timeout: Duration,
16 pub default_headers: HashMap<String, String>,
18 pub user_agent: String,
20}
21
22impl TransportConfig {
23 pub fn new() -> Self {
25 let mut default_headers = HashMap::new();
26 default_headers.insert("Content-Type".to_string(), "application/json".to_string());
27
28 Self {
29 timeout: Duration::from_secs(30),
30 default_headers,
31 user_agent: format!("serper-sdk/{}", env!("CARGO_PKG_VERSION")),
32 }
33 }
34
35 pub fn with_timeout(mut self, timeout: Duration) -> Self {
37 self.timeout = timeout;
38 self
39 }
40
41 pub fn with_header(mut self, key: String, value: String) -> Self {
43 self.default_headers.insert(key, value);
44 self
45 }
46
47 pub fn with_user_agent(mut self, user_agent: String) -> Self {
49 self.user_agent = user_agent;
50 self
51 }
52}
53
54impl Default for TransportConfig {
55 fn default() -> Self {
56 Self::new()
57 }
58}
59
60#[derive(Debug)]
65pub struct HttpTransport {
66 client: ReqwestClient,
67 config: TransportConfig,
68}
69
70impl HttpTransport {
71 pub fn new() -> Result<Self> {
73 Self::with_config(TransportConfig::new())
74 }
75
76 pub fn with_config(config: TransportConfig) -> Result<Self> {
78 let client = ReqwestClient::builder()
79 .timeout(config.timeout)
80 .user_agent(&config.user_agent)
81 .build()
82 .map_err(SerperError::Request)?;
83
84 Ok(Self { client, config })
85 }
86
87 pub async fn post_json<T: Serialize>(
99 &self,
100 url: &str,
101 api_key: &ApiKey,
102 body: &T,
103 ) -> Result<Response> {
104 let mut request = self
105 .client
106 .request(Method::POST, url)
107 .header("X-API-KEY", api_key.as_str());
108
109 for (key, value) in &self.config.default_headers {
111 if key != "Content-Type" {
112 request = request.header(key, value);
113 }
114 }
115
116 request = request.json(body);
118
119 let response = request.send().await.map_err(SerperError::Request)?;
120
121 if !response.status().is_success() {
123 return Err(SerperError::api_error(format!(
124 "HTTP {} - {}",
125 response.status(),
126 response
127 .status()
128 .canonical_reason()
129 .unwrap_or("Unknown error")
130 )));
131 }
132
133 Ok(response)
134 }
135
136 pub async fn get(&self, url: &str, api_key: &ApiKey) -> Result<Response> {
147 let mut request = self
148 .client
149 .request(Method::GET, url)
150 .header("X-API-KEY", api_key.as_str());
151
152 for (key, value) in &self.config.default_headers {
154 if key != "Content-Type" {
155 request = request.header(key, value);
156 }
157 }
158
159 let response = request.send().await.map_err(SerperError::Request)?;
160
161 if !response.status().is_success() {
162 return Err(SerperError::api_error(format!(
163 "HTTP {} - {}",
164 response.status(),
165 response
166 .status()
167 .canonical_reason()
168 .unwrap_or("Unknown error")
169 )));
170 }
171
172 Ok(response)
173 }
174
175 pub async fn parse_json<T>(&self, response: Response) -> Result<T>
185 where
186 T: serde::de::DeserializeOwned,
187 {
188 response.json().await.map_err(SerperError::Request)
189 }
190
191 pub fn config(&self) -> &TransportConfig {
193 &self.config
194 }
195}
196
197impl Default for HttpTransport {
198 fn default() -> Self {
199 Self::new().expect("Failed to create default HTTP transport")
200 }
201}
202
203pub struct HttpTransportBuilder {
205 config: TransportConfig,
206}
207
208impl HttpTransportBuilder {
209 pub fn new() -> Self {
211 Self {
212 config: TransportConfig::new(),
213 }
214 }
215
216 pub fn timeout(mut self, timeout: Duration) -> Self {
218 self.config = self.config.with_timeout(timeout);
219 self
220 }
221
222 pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
224 self.config = self.config.with_header(key.into(), value.into());
225 self
226 }
227
228 pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
230 self.config = self.config.with_user_agent(user_agent.into());
231 self
232 }
233
234 pub fn build(self) -> Result<HttpTransport> {
236 HttpTransport::with_config(self.config)
237 }
238}
239
240impl Default for HttpTransportBuilder {
241 fn default() -> Self {
242 Self::new()
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 #[test]
251 fn test_transport_config_creation() {
252 let config = TransportConfig::new();
253 assert_eq!(config.timeout, Duration::from_secs(30));
254 assert!(config.default_headers.contains_key("Content-Type"));
255 }
256
257 #[test]
258 fn test_transport_config_builder() {
259 let config = TransportConfig::new()
260 .with_timeout(Duration::from_secs(60))
261 .with_header("Custom-Header".to_string(), "value".to_string())
262 .with_user_agent("custom-agent".to_string());
263
264 assert_eq!(config.timeout, Duration::from_secs(60));
265 assert_eq!(config.user_agent, "custom-agent");
266 assert_eq!(
267 config.default_headers.get("Custom-Header"),
268 Some(&"value".to_string())
269 );
270 }
271
272 #[test]
273 fn test_transport_builder() {
274 let builder = HttpTransportBuilder::new()
275 .timeout(Duration::from_secs(45))
276 .header("Test", "Value")
277 .user_agent("test-agent");
278
279 let transport = builder.build().unwrap();
280 assert_eq!(transport.config().timeout, Duration::from_secs(45));
281 assert_eq!(transport.config().user_agent, "test-agent");
282 }
283
284 #[test]
285 fn test_api_key_validation() {
286 let result = ApiKey::new("valid-key".to_string());
287 assert!(result.is_ok());
288
289 let result = ApiKey::new("".to_string());
290 assert!(result.is_err());
291 }
292}