viewpoint_core/api/response/mod.rs
1//! API response handling.
2
3use std::collections::HashMap;
4
5use bytes::Bytes;
6use reqwest::header::HeaderMap;
7use serde::de::DeserializeOwned;
8
9use super::APIError;
10
11/// Response from an API request.
12///
13/// This struct wraps a reqwest response and provides convenient methods
14/// for extracting the response body in various formats.
15///
16/// # Example
17///
18/// ```no_run
19/// use viewpoint_core::api::{APIRequestContext, APIContextOptions};
20///
21/// # async fn example() -> Result<(), viewpoint_core::api::APIError> {
22/// let api = APIRequestContext::new(APIContextOptions::new()).await?;
23/// let response = api.get("https://api.example.com/users").send().await?;
24///
25/// // Check status
26/// if response.ok() {
27/// // Parse JSON
28/// let data: serde_json::Value = response.json().await?;
29/// println!("Got data: {:?}", data);
30/// }
31/// # Ok(())
32/// # }
33/// ```
34#[derive(Debug)]
35pub struct APIResponse {
36 /// The underlying reqwest response.
37 response: reqwest::Response,
38}
39
40impl APIResponse {
41 /// Create a new API response from a reqwest response.
42 pub(crate) fn new(response: reqwest::Response) -> Self {
43 Self { response }
44 }
45
46 /// Get the HTTP status code.
47 ///
48 /// # Example
49 ///
50 /// ```no_run
51 /// # use viewpoint_core::api::{APIRequestContext, APIContextOptions};
52 /// # async fn example() -> Result<(), viewpoint_core::api::APIError> {
53 /// # let api = APIRequestContext::new(APIContextOptions::new()).await?;
54 /// let response = api.get("https://api.example.com/users").send().await?;
55 /// println!("Status: {}", response.status()); // e.g., 200
56 /// # Ok(())
57 /// # }
58 /// ```
59 pub fn status(&self) -> u16 {
60 self.response.status().as_u16()
61 }
62
63 /// Get the HTTP status code as a `reqwest::StatusCode`.
64 pub fn status_code(&self) -> reqwest::StatusCode {
65 self.response.status()
66 }
67
68 /// Check if the response was successful (status code 2xx).
69 ///
70 /// # Example
71 ///
72 /// ```no_run
73 /// # use viewpoint_core::api::{APIRequestContext, APIContextOptions};
74 /// # async fn example() -> Result<(), viewpoint_core::api::APIError> {
75 /// # let api = APIRequestContext::new(APIContextOptions::new()).await?;
76 /// let response = api.get("https://api.example.com/users").send().await?;
77 /// if response.ok() {
78 /// println!("Request succeeded!");
79 /// }
80 /// # Ok(())
81 /// # }
82 /// ```
83 pub fn ok(&self) -> bool {
84 self.response.status().is_success()
85 }
86
87 /// Get the status text (reason phrase).
88 pub fn status_text(&self) -> &str {
89 self.response
90 .status()
91 .canonical_reason()
92 .unwrap_or("Unknown")
93 }
94
95 /// Get the response headers.
96 ///
97 /// # Example
98 ///
99 /// ```no_run
100 /// # use viewpoint_core::api::{APIRequestContext, APIContextOptions};
101 /// # async fn example() -> Result<(), viewpoint_core::api::APIError> {
102 /// # let api = APIRequestContext::new(APIContextOptions::new()).await?;
103 /// let response = api.get("https://api.example.com/users").send().await?;
104 /// let headers = response.headers();
105 /// if let Some(content_type) = headers.get("content-type") {
106 /// println!("Content-Type: {:?}", content_type);
107 /// }
108 /// # Ok(())
109 /// # }
110 /// ```
111 pub fn headers(&self) -> &HeaderMap {
112 self.response.headers()
113 }
114
115 /// Get response headers as a `HashMap`.
116 pub fn headers_map(&self) -> HashMap<String, String> {
117 self.response
118 .headers()
119 .iter()
120 .filter_map(|(name, value)| {
121 value
122 .to_str()
123 .ok()
124 .map(|v| (name.as_str().to_string(), v.to_string()))
125 })
126 .collect()
127 }
128
129 /// Get a specific header value.
130 pub fn header(&self, name: &str) -> Option<&str> {
131 self.response
132 .headers()
133 .get(name)
134 .and_then(|v| v.to_str().ok())
135 }
136
137 /// Get the final URL after any redirects.
138 pub fn url(&self) -> &str {
139 self.response.url().as_str()
140 }
141
142 /// Parse the response body as JSON.
143 ///
144 /// # Errors
145 ///
146 /// Returns an error if the response body cannot be parsed as JSON.
147 ///
148 /// # Example
149 ///
150 /// ```no_run
151 /// use serde::Deserialize;
152 /// # use viewpoint_core::api::{APIRequestContext, APIContextOptions};
153 ///
154 /// #[derive(Deserialize)]
155 /// struct User {
156 /// id: i32,
157 /// name: String,
158 /// }
159 ///
160 /// # async fn example() -> Result<(), viewpoint_core::api::APIError> {
161 /// # let api = APIRequestContext::new(APIContextOptions::new()).await?;
162 /// let response = api.get("https://api.example.com/users/1").send().await?;
163 /// let user: User = response.json().await?;
164 /// println!("User: {} (id={})", user.name, user.id);
165 /// # Ok(())
166 /// # }
167 /// ```
168 pub async fn json<T: DeserializeOwned>(self) -> Result<T, APIError> {
169 self.response
170 .json()
171 .await
172 .map_err(|e| APIError::JsonError(e.to_string()))
173 }
174
175 /// Get the response body as text.
176 ///
177 /// # Errors
178 ///
179 /// Returns an error if the response body cannot be read as text.
180 ///
181 /// # Example
182 ///
183 /// ```no_run
184 /// # use viewpoint_core::api::{APIRequestContext, APIContextOptions};
185 /// # async fn example() -> Result<(), viewpoint_core::api::APIError> {
186 /// # let api = APIRequestContext::new(APIContextOptions::new()).await?;
187 /// let response = api.get("https://example.com").send().await?;
188 /// let html = response.text().await?;
189 /// println!("HTML length: {} bytes", html.len());
190 /// # Ok(())
191 /// # }
192 /// ```
193 pub async fn text(self) -> Result<String, APIError> {
194 self.response
195 .text()
196 .await
197 .map_err(|e| APIError::ParseError(e.to_string()))
198 }
199
200 /// Get the response body as raw bytes.
201 ///
202 /// # Errors
203 ///
204 /// Returns an error if the response body cannot be read.
205 ///
206 /// # Example
207 ///
208 /// ```no_run
209 /// # use viewpoint_core::api::{APIRequestContext, APIContextOptions};
210 /// # async fn example() -> Result<(), viewpoint_core::api::APIError> {
211 /// # let api = APIRequestContext::new(APIContextOptions::new()).await?;
212 /// let response = api.get("https://example.com/image.png").send().await?;
213 /// let bytes = response.body().await?;
214 /// std::fs::write("image.png", &bytes).expect("Failed to write file");
215 /// # Ok(())
216 /// # }
217 /// ```
218 pub async fn body(self) -> Result<Bytes, APIError> {
219 self.response
220 .bytes()
221 .await
222 .map_err(|e| APIError::ParseError(e.to_string()))
223 }
224
225 /// Get the content length if known.
226 pub fn content_length(&self) -> Option<u64> {
227 self.response.content_length()
228 }
229
230 /// Check if the response indicates a redirect.
231 pub fn is_redirect(&self) -> bool {
232 self.response.status().is_redirection()
233 }
234
235 /// Check if the response indicates a client error (4xx).
236 pub fn is_client_error(&self) -> bool {
237 self.response.status().is_client_error()
238 }
239
240 /// Check if the response indicates a server error (5xx).
241 pub fn is_server_error(&self) -> bool {
242 self.response.status().is_server_error()
243 }
244}
245
246#[cfg(test)]
247mod tests;