polyoxide_core/
response.rs1use std::fmt;
5
6use bytes::Bytes;
7use http::{HeaderMap, StatusCode};
8use polyoxide_rt::BoxFuture;
9use serde::de::DeserializeOwned;
10use url::Url;
11
12use crate::error::ApiError;
13use crate::transport::TransportError;
14
15pub struct Response {
25 status: StatusCode,
26 headers: HeaderMap,
27 url: Url,
28 body: BoxFuture<Result<Bytes, TransportError>>,
29}
30
31impl Response {
32 pub fn new(
37 status: StatusCode,
38 headers: HeaderMap,
39 url: Url,
40 body: BoxFuture<Result<Bytes, TransportError>>,
41 ) -> Self {
42 Self {
43 status,
44 headers,
45 url,
46 body,
47 }
48 }
49
50 pub fn status(&self) -> StatusCode {
52 self.status
53 }
54
55 pub fn headers(&self) -> &HeaderMap {
57 &self.headers
58 }
59
60 pub fn url(&self) -> &Url {
62 &self.url
63 }
64
65 pub async fn bytes(self) -> Result<Bytes, ApiError> {
71 self.body.await.map_err(ApiError::from)
72 }
73
74 pub async fn text(self) -> Result<String, ApiError> {
81 let bytes = self.bytes().await?;
82 Ok(String::from_utf8_lossy(&bytes).into_owned())
83 }
84
85 pub async fn json<T: DeserializeOwned>(self) -> Result<T, ApiError> {
92 let bytes = self.bytes().await?;
93 serde_json::from_slice(&bytes).map_err(ApiError::from)
94 }
95}
96
97impl fmt::Debug for Response {
100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101 let header_names: Vec<&str> = self.headers.keys().map(|name| name.as_str()).collect();
102 f.debug_struct("Response")
103 .field("status", &self.status)
104 .field("url", &self.url.as_str())
105 .field("headers", &header_names)
106 .finish_non_exhaustive()
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use http::HeaderValue;
113 use polyoxide_venue::{Class, Classify};
114
115 use super::*;
116 use crate::transport::TransportKind;
117
118 fn url() -> Url {
119 Url::parse("https://api.example.test/v1/rows").unwrap()
120 }
121
122 fn answering(body: &'static [u8]) -> Response {
124 let mut headers = HeaderMap::new();
125 headers.insert("x-served-by", HeaderValue::from_static("test"));
126 Response::new(
127 StatusCode::OK,
128 headers,
129 url(),
130 Box::pin(async move { Ok(Bytes::from_static(body)) }),
131 )
132 }
133
134 fn cut_off() -> Response {
136 Response::new(
137 StatusCode::OK,
138 HeaderMap::new(),
139 url(),
140 Box::pin(async {
141 Err::<Bytes, _>(TransportError::new(TransportKind::Body, "cut off"))
142 }),
143 )
144 }
145
146 #[test]
147 fn the_head_reads_without_the_body() {
148 let response = answering(b"{}");
149 assert_eq!(response.status(), StatusCode::OK);
150 assert_eq!(response.url(), &url());
151 assert_eq!(
152 response
153 .headers()
154 .get("x-served-by")
155 .and_then(|value| value.to_str().ok()),
156 Some("test")
157 );
158 }
159
160 #[tokio::test]
161 async fn bytes_text_and_json_read_the_same_body() {
162 const BODY: &[u8] = br#"{"rows":[1,2]}"#;
163 assert_eq!(
164 answering(BODY).bytes().await.unwrap(),
165 Bytes::from_static(BODY)
166 );
167 assert_eq!(answering(BODY).text().await.unwrap(), r#"{"rows":[1,2]}"#);
168 assert_eq!(
169 answering(BODY).json::<serde_json::Value>().await.unwrap(),
170 serde_json::json!({"rows": [1, 2]})
171 );
172 }
173
174 #[tokio::test]
175 async fn text_replaces_invalid_utf8() {
176 assert_eq!(answering(b"h\xFFi").text().await.unwrap(), "h\u{FFFD}i");
177 }
178
179 #[tokio::test]
180 async fn a_body_that_is_not_json_is_a_serialization_error_of_the_decode_class() {
181 let err = answering(b"not json")
182 .json::<serde_json::Value>()
183 .await
184 .unwrap_err();
185 assert!(matches!(err, ApiError::Serialization(_)), "{err:?}");
186 assert_eq!(err.class(), Class::Decode);
187 }
188
189 #[tokio::test]
190 async fn a_body_that_fails_to_arrive_is_the_transport_error_whichever_way_it_is_read() {
191 for err in [
192 cut_off().bytes().await.unwrap_err(),
193 cut_off().text().await.unwrap_err(),
194 cut_off().json::<serde_json::Value>().await.unwrap_err(),
195 ] {
196 assert!(
197 matches!(&err, ApiError::Transport(e) if e.kind() == TransportKind::Body),
198 "{err:?}"
199 );
200 assert_eq!(err.class(), Class::Network);
201 }
202 }
203
204 #[test]
205 fn debug_names_the_headers_and_never_their_values() {
206 let mut headers = HeaderMap::new();
207 headers.insert("authorization", HeaderValue::from_static("Bearer s3cr3t"));
208 let response = Response::new(
209 StatusCode::OK,
210 headers,
211 url(),
212 Box::pin(async { Ok(Bytes::new()) }),
213 );
214 let debug = format!("{response:?}");
215 assert!(debug.contains("authorization"), "{debug}");
216 assert!(!debug.contains("s3cr3t"), "{debug}");
217 assert!(debug.contains("200"), "{debug}");
218 }
219}