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;