sage_runtime/tools/
http.rs1use std::collections::HashMap;
6use std::time::Duration;
7
8use crate::error::SageResult;
9
10#[derive(Debug, Clone)]
12pub struct HttpConfig {
13 pub timeout_secs: u64,
15 pub user_agent: String,
17}
18
19impl Default for HttpConfig {
20 fn default() -> Self {
21 Self {
22 timeout_secs: 30,
23 user_agent: format!("sage-agent/{}", env!("CARGO_PKG_VERSION")),
24 }
25 }
26}
27
28impl HttpConfig {
29 pub fn from_env() -> Self {
34 let timeout_secs = std::env::var("SAGE_HTTP_TIMEOUT")
35 .ok()
36 .and_then(|s| s.parse().ok())
37 .unwrap_or(30);
38
39 Self {
40 timeout_secs,
41 ..Default::default()
42 }
43 }
44}
45
46#[derive(Debug, Clone)]
50pub struct HttpResponse {
51 pub status: i64,
53 pub body: String,
55 pub headers: HashMap<String, String>,
57}
58
59#[derive(Debug, Clone)]
63pub struct HttpClient {
64 client: reqwest::Client,
65}
66
67impl HttpClient {
68 pub fn new() -> Self {
70 Self::with_config(HttpConfig::default())
71 }
72
73 pub fn from_env() -> Self {
75 Self::with_config(HttpConfig::from_env())
76 }
77
78 pub fn with_config(config: HttpConfig) -> Self {
80 let client = reqwest::Client::builder()
81 .timeout(Duration::from_secs(config.timeout_secs))
82 .user_agent(&config.user_agent)
83 .build()
84 .expect("failed to build HTTP client");
85
86 Self { client }
87 }
88
89 pub async fn get(&self, url: String) -> SageResult<HttpResponse> {
97 let response = self.client.get(url).send().await?;
98
99 let status = response.status().as_u16() as i64;
100 let headers = response
101 .headers()
102 .iter()
103 .map(|(k, v)| {
104 (
105 k.as_str().to_string(),
106 v.to_str().unwrap_or_default().to_string(),
107 )
108 })
109 .collect();
110 let body = response.text().await?;
111
112 Ok(HttpResponse {
113 status,
114 body,
115 headers,
116 })
117 }
118
119 pub async fn post(&self, url: String, body: String) -> SageResult<HttpResponse> {
128 let response = self
129 .client
130 .post(url)
131 .header("Content-Type", "application/json")
132 .body(body)
133 .send()
134 .await?;
135
136 let status = response.status().as_u16() as i64;
137 let headers = response
138 .headers()
139 .iter()
140 .map(|(k, v)| {
141 (
142 k.as_str().to_string(),
143 v.to_str().unwrap_or_default().to_string(),
144 )
145 })
146 .collect();
147 let response_body = response.text().await?;
148
149 Ok(HttpResponse {
150 status,
151 body: response_body,
152 headers,
153 })
154 }
155}
156
157impl Default for HttpClient {
158 fn default() -> Self {
159 Self::new()
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn http_config_defaults() {
169 let config = HttpConfig::default();
170 assert_eq!(config.timeout_secs, 30);
171 assert!(config.user_agent.starts_with("sage-agent/"));
172 }
173
174 #[test]
175 fn http_client_creates() {
176 let client = HttpClient::new();
177 drop(client);
179 }
180
181 #[tokio::test]
182 async fn http_get_works() {
183 if std::env::var("CI").is_ok() {
185 return;
186 }
187
188 let client = HttpClient::new();
189 let response = client.get("https://httpbin.org/get".to_string()).await;
190 assert!(response.is_ok());
191 let response = response.unwrap();
192 assert_eq!(response.status, 200);
193 assert!(!response.body.is_empty());
194 }
195}